Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 82937743 authored by Matías Hernández's avatar Matías Hernández Committed by Android (Google) Code Review
Browse files

Merge "Populate new fields in time- and calendar-schedule modes" into main

parents 13b1479b a5504bae
Loading
Loading
Loading
Loading
+262 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.service.notification;

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AutomaticZenRule;
import android.app.Flags;
import android.content.Context;
import android.service.notification.ZenModeConfig.EventInfo;
import android.service.notification.ZenModeConfig.ScheduleInfo;
import android.service.notification.ZenModeConfig.ZenRule;
import android.text.format.DateFormat;
import android.util.Log;

import com.android.internal.R;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import java.util.Objects;

/**
 * Helper methods for schedule-type (system-owned) rules.
 * @hide
 */
public final class SystemZenRules {

    private static final String TAG = "SystemZenRules";

    public static final String PACKAGE_ANDROID = "android";

    /** Updates existing system-owned rules to use the new Modes fields (type, etc). */
    @FlaggedApi(Flags.FLAG_MODES_API)
    public static void maybeUpgradeRules(Context context, ZenModeConfig config) {
        for (ZenRule rule : config.automaticRules.values()) {
            if (isSystemOwnedRule(rule) && rule.type == AutomaticZenRule.TYPE_UNKNOWN) {
                upgradeSystemProviderRule(context, rule);
            }
        }
    }

    /**
     * Returns whether the rule corresponds to a system ConditionProviderService (i.e. it is owned
     * by the "android" package).
     */
    public static boolean isSystemOwnedRule(ZenRule rule) {
        return PACKAGE_ANDROID.equals(rule.pkg);
    }

    @FlaggedApi(Flags.FLAG_MODES_API)
    private static void upgradeSystemProviderRule(Context context, ZenRule rule) {
        ScheduleInfo scheduleInfo = ZenModeConfig.tryParseScheduleConditionId(rule.conditionId);
        if (scheduleInfo != null) {
            rule.type = AutomaticZenRule.TYPE_SCHEDULE_TIME;
            rule.triggerDescription = getTriggerDescriptionForScheduleTime(context, scheduleInfo);
            return;
        }
        EventInfo eventInfo = ZenModeConfig.tryParseEventConditionId(rule.conditionId);
        if (eventInfo != null) {
            rule.type = AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
            rule.triggerDescription = getTriggerDescriptionForScheduleEvent(context, eventInfo);
            return;
        }
        Log.wtf(TAG, "Couldn't determine type of system-owned ZenRule " + rule);
    }

    /**
     * Updates the {@link ZenRule#triggerDescription} of the system-owned rule based on the schedule
     * or event condition encoded in its {@link ZenRule#conditionId}.
     *
     * @return {@code true} if the trigger description was updated.
     */
    public static boolean updateTriggerDescription(Context context, ZenRule rule) {
        ScheduleInfo scheduleInfo = ZenModeConfig.tryParseScheduleConditionId(rule.conditionId);
        if (scheduleInfo != null) {
            return updateTriggerDescription(rule,
                    getTriggerDescriptionForScheduleTime(context, scheduleInfo));
        }
        EventInfo eventInfo = ZenModeConfig.tryParseEventConditionId(rule.conditionId);
        if (eventInfo != null) {
            return updateTriggerDescription(rule,
                    getTriggerDescriptionForScheduleEvent(context, eventInfo));
        }
        Log.wtf(TAG, "Couldn't determine type of system-owned ZenRule " + rule);
        return false;
    }

    private static boolean updateTriggerDescription(ZenRule rule, String triggerDescription) {
        if (!Objects.equals(rule.triggerDescription, triggerDescription)) {
            rule.triggerDescription = triggerDescription;
            return true;
        }
        return false;
    }

    /**
     * Returns a suitable trigger description for a time-schedule rule (e.g. "Mon-Fri, 8:00-10:00"),
     * using the Context's current locale.
     */
    @Nullable
    public static String getTriggerDescriptionForScheduleTime(Context context,
            @NonNull ScheduleInfo schedule) {
        final StringBuilder sb = new StringBuilder();
        String daysSummary = getShortDaysSummary(context, schedule);
        if (daysSummary == null) {
            // no use outputting times without dates
            return null;
        }
        sb.append(daysSummary);
        sb.append(context.getString(R.string.zen_mode_trigger_summary_divider_text));
        sb.append(context.getString(
                R.string.zen_mode_trigger_summary_range_symbol_combination,
                timeString(context, schedule.startHour, schedule.startMinute),
                timeString(context, schedule.endHour, schedule.endMinute)));

        return sb.toString();
    }

    /**
     * Returns an ordered summarized list of the days on which this schedule applies, with
     * adjacent days grouped together ("Sun-Wed" instead of "Sun,Mon,Tue,Wed").
     */
    @Nullable
    private static String getShortDaysSummary(Context context, @NonNull ScheduleInfo schedule) {
        // Compute a list of days with contiguous days grouped together, for example: "Sun-Thu" or
        // "Sun-Mon,Wed,Fri"
        final int[] days = schedule.days;
        if (days != null && days.length > 0) {
            final StringBuilder sb = new StringBuilder();
            final Calendar cStart = Calendar.getInstance(getLocale(context));
            final Calendar cEnd = Calendar.getInstance(getLocale(context));
            int[] daysOfWeek = getDaysOfWeekForLocale(cStart);
            // the i for loop goes through days in order as determined by locale. as we walk through
            // the days of the week, keep track of "start" and "last seen"  as indicators for
            // what's contiguous, and initialize them to something not near actual indices
            int startDay = Integer.MIN_VALUE;
            int lastSeenDay = Integer.MIN_VALUE;
            for (int i = 0; i < daysOfWeek.length; i++) {
                final int day = daysOfWeek[i];

                // by default, output if this day is *not* included in the schedule, and thus
                // ends a previously existing block. if this day is included in the schedule
                // after all (as will be determined in the inner for loop), then output will be set
                // to false.
                boolean output = (i == lastSeenDay + 1);
                for (int j = 0; j < days.length; j++) {
                    if (day == days[j]) {
                        // match for this day in the schedule (indicated by counter i)
                        if (i == lastSeenDay + 1) {
                            // contiguous to the block we're walking through right now, record it
                            // (specifically, i, the day index) and move on to the next day
                            lastSeenDay = i;
                            output = false;
                        } else {
                            // it's a match, but not 1 past the last match, we are starting a new
                            // block
                            startDay = i;
                            lastSeenDay = i;
                        }

                        // if there is a match on the last day, also make sure to output at the end
                        // of this loop, and mark the day as the last day we'll have seen in the
                        // scheduled days.
                        if (i == daysOfWeek.length - 1) {
                            output = true;
                        }
                        break;
                    }
                }

                // output in either of 2 cases: this day is not a match, so has ended any previous
                // block, or this day *is* a match but is the last day of the week, so we need to
                // summarize
                if (output) {
                    // either describe just the single day if startDay == lastSeenDay, or
                    // output "startDay - lastSeenDay" as a group
                    if (sb.length() > 0) {
                        sb.append(
                                context.getString(R.string.zen_mode_trigger_summary_divider_text));
                    }

                    SimpleDateFormat dayFormat = new SimpleDateFormat("EEE", getLocale(context));
                    if (startDay == lastSeenDay) {
                        // last group was only one day
                        cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
                        sb.append(dayFormat.format(cStart.getTime()));
                    } else {
                        // last group was a contiguous group of days, so group them together
                        cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
                        cEnd.set(Calendar.DAY_OF_WEEK, daysOfWeek[lastSeenDay]);
                        sb.append(context.getString(
                                R.string.zen_mode_trigger_summary_range_symbol_combination,
                                dayFormat.format(cStart.getTime()),
                                dayFormat.format(cEnd.getTime())));
                    }
                }
            }

            if (sb.length() > 0) {
                return sb.toString();
            }
        }
        return null;
    }

    /**
     * Convenience method for representing the specified time in string format.
     */
    private static String timeString(Context context, int hour, int minute) {
        final Calendar c = Calendar.getInstance(getLocale(context));
        c.set(Calendar.HOUR_OF_DAY, hour);
        c.set(Calendar.MINUTE, minute);
        return DateFormat.getTimeFormat(context).format(c.getTime());
    }

    private static int[] getDaysOfWeekForLocale(Calendar c) {
        int[] daysOfWeek = new int[7];
        int currentDay = c.getFirstDayOfWeek();
        for (int i = 0; i < daysOfWeek.length; i++) {
            if (currentDay > 7) currentDay = 1;
            daysOfWeek[i] = currentDay;
            currentDay++;
        }
        return daysOfWeek;
    }

    private static Locale getLocale(Context context) {
        return context.getResources().getConfiguration().getLocales().get(0);
    }

    /**
     * Returns a suitable trigger description for a calendar-schedule rule (either the name of the
     * calendar, or a message indicating all calendars are included).
     */
    public static String getTriggerDescriptionForScheduleEvent(Context context,
            @NonNull EventInfo event) {
        if (event.calName != null) {
            return event.calName;
        } else {
            return context.getResources().getString(
                    R.string.zen_mode_trigger_event_calendar_any);
        }
    }

    private SystemZenRules() {}
}
+10 −0
Original line number Diff line number Diff line
@@ -1773,7 +1773,12 @@ public class ZenModeConfig implements Parcelable {
        return true;
    }

    /**
     * Returns the {@link ScheduleInfo} encoded in the condition id, or {@code null} if it could not
     * be decoded.
     */
    @UnsupportedAppUsage
    @Nullable
    public static ScheduleInfo tryParseScheduleConditionId(Uri conditionId) {
        final boolean isSchedule =  conditionId != null
                && Condition.SCHEME.equals(conditionId.getScheme())
@@ -1882,6 +1887,11 @@ public class ZenModeConfig implements Parcelable {
        return tryParseEventConditionId(conditionId) != null;
    }

    /**
     * Returns the {@link EventInfo} encoded in the condition id, or {@code null} if it could not be
     * decoded.
     */
    @Nullable
    public static EventInfo tryParseEventConditionId(Uri conditionId) {
        final boolean isEvent = conditionId != null
                && Condition.SCHEME.equals(conditionId.getScheme())
+7 −0
Original line number Diff line number Diff line
@@ -5312,6 +5312,13 @@
    <!-- Zen mode - Condition summary when a rule is deactivated due to a call to setInterruptionFilter(). [CHAR_LIMIT=NONE] -->
    <string name="zen_mode_implicit_deactivated">Off</string>

    <!-- [CHAR LIMIT=40] General divider text when concatenating multiple items in a text summary, used for the trigger description of a Zen mode -->
    <string name="zen_mode_trigger_summary_divider_text">,\u0020</string>
    <!-- [CHAR LIMIT=40] General template for a symbolic start - end range in a text summary, used for the trigger description of a Zen mode -->
    <string name="zen_mode_trigger_summary_range_symbol_combination"><xliff:g id="start" example="Sun">%1$s</xliff:g> - <xliff:g id="end" example="Thu">%2$s</xliff:g></string>
    <!-- [CHAR LIMIT=40] Event-based rule calendar option value for any calendar, used for the trigger description of a Zen mode -->
    <string name="zen_mode_trigger_event_calendar_any">Any calendar</string>

    <!-- Indication that the current volume and other effects (vibration) are being suppressed by a third party, such as a notification listener. [CHAR LIMIT=30] -->
    <string name="muted_by"><xliff:g id="third_party">%1$s</xliff:g> is muting some sounds</string>

+4 −0
Original line number Diff line number Diff line
@@ -2609,6 +2609,10 @@
  <java-symbol type="string" name="zen_mode_implicit_trigger_description" />
  <java-symbol type="string" name="zen_mode_implicit_activated" />
  <java-symbol type="string" name="zen_mode_implicit_deactivated" />
  <java-symbol type="string" name="zen_mode_trigger_summary_divider_text" />
  <java-symbol type="string" name="zen_mode_trigger_summary_range_symbol_combination" />
  <java-symbol type="string" name="zen_mode_trigger_event_calendar_any" />

  <java-symbol type="string" name="display_rotation_camera_compat_toast_after_rotation" />
  <java-symbol type="string" name="display_rotation_camera_compat_toast_in_multi_window" />
  <java-symbol type="array" name="config_system_condition_providers" />
+1 −1
Original line number Diff line number Diff line
@@ -1794,7 +1794,7 @@ public class NotificationManagerService extends SystemService {
            if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) {
                // update system notification channels
                SystemNotificationChannels.createAll(context);
                mZenModeHelper.updateDefaultZenRules(Binder.getCallingUid());
                mZenModeHelper.updateZenRulesOnLocaleChange();
                mPreferencesHelper.onLocaleChanged(context, ActivityManager.getCurrentUser());
            }
        }
Loading