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

Commit b5e4fabf authored by Yuri Lin's avatar Yuri Lin Committed by Android (Google) Code Review
Browse files

Merge "Change zen schedules page to use switches."

parents 0ca89079 a91e2dae
Loading
Loading
Loading
Loading
+6 −27
Original line number Diff line number Diff line
@@ -42,7 +42,6 @@ import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settingslib.core.AbstractPreferenceController;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@@ -56,8 +55,7 @@ public class ZenModeScheduleRuleSettings extends ZenModeRuleSettingsBase {

    public static final String ACTION = Settings.ACTION_ZEN_MODE_SCHEDULE_RULE_SETTINGS;

    // per-instance to ensure we're always using the current locale
    private final SimpleDateFormat mDayFormat = new SimpleDateFormat("EEE");
    private final ZenRuleScheduleHelper mScheduleHelper = new ZenRuleScheduleHelper();

    private Preference mDays;
    private TimePickerPreference mStart;
@@ -149,31 +147,12 @@ public class ZenModeScheduleRuleSettings extends ZenModeRuleSettingsBase {
    }

    private void updateDays() {
        // Compute an ordered, delimited list of day names based on the persisted user config.
        final int[] days = mSchedule.days;
        if (days != null && days.length > 0) {
            final StringBuilder sb = new StringBuilder();
            final Calendar c = Calendar.getInstance();
            int[] daysOfWeek = ZenModeScheduleDaysSelection.getDaysOfWeekForLocale(c);
            for (int i = 0; i < daysOfWeek.length; i++) {
                final int day = daysOfWeek[i];
                for (int j = 0; j < days.length; j++) {
                    if (day == days[j]) {
                        c.set(Calendar.DAY_OF_WEEK, day);
                        if (sb.length() > 0) {
                            sb.append(mContext.getString(R.string.summary_divider_text));
                        }
                        sb.append(mDayFormat.format(c.getTime()));
                        break;
                    }
                }
            }
            if (sb.length() > 0) {
                mDays.setSummary(sb);
        String desc = mScheduleHelper.getDaysDescription(mContext, mSchedule);
        if (desc != null) {
            mDays.setSummary(desc);
            mDays.notifyDependencyChange(false);
            return;
        }
        }
        mDays.setSummary(R.string.zen_mode_schedule_rule_days_none);
        mDays.notifyDependencyChange(true);
    }
+39 −67
Original line number Diff line number Diff line
@@ -23,22 +23,20 @@ import android.content.Intent;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageManager;
import android.service.notification.ZenModeConfig;
import android.view.View;
import android.widget.CheckBox;
import android.service.notification.ZenModeConfig.ScheduleInfo;

import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;

import com.android.settings.R;
import com.android.settings.utils.ManagedServiceSettings;
import com.android.settings.utils.ZenServiceListing;
import com.android.settingslib.PrimarySwitchPreference;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.widget.TwoTargetPreference;

import java.util.Map;

public class ZenRulePreference extends TwoTargetPreference {
public class ZenRulePreference extends PrimarySwitchPreference {
    private static final ManagedServiceSettings.Config CONFIG =
            ZenModeAutomationSettings.getConditionProviderConfig();
    final String mId;
@@ -53,14 +51,13 @@ public class ZenRulePreference extends TwoTargetPreference {
    CharSequence mName;

    private Intent mIntent;
    private boolean mChecked;
    private CheckBox mCheckBox;

    private final ZenRuleScheduleHelper mScheduleHelper = new ZenRuleScheduleHelper();

    public ZenRulePreference(Context context,
            final Map.Entry<String, AutomaticZenRule> ruleEntry,
            Fragment parent, MetricsFeatureProvider metricsProvider) {
        super(context);
        setLayoutResource(R.layout.preference_checkable_two_target);
        mBackend = ZenModeBackend.getInstance(context);
        mContext = context;
        mRule = ruleEntry.getValue();
@@ -72,50 +69,11 @@ public class ZenRulePreference extends TwoTargetPreference {
        mServiceListing.reloadApprovedServices();
        mPref = this;
        mMetricsFeatureProvider = metricsProvider;
        mChecked = mRule.isEnabled();
        setAttributes(mRule);
        setWidgetLayoutResource(getSecondTargetResId());
    }

    protected int getSecondTargetResId() {
        if (mIntent != null) {
            return R.layout.zen_rule_widget;
        }
        return 0;
    }

    @Override
    public void onBindViewHolder(PreferenceViewHolder view) {
        super.onBindViewHolder(view);
        View settingsWidget = view.findViewById(android.R.id.widget_frame);
        View divider = view.findViewById(R.id.two_target_divider);
        if (mIntent != null) {
            divider.setVisibility(View.VISIBLE);
            settingsWidget.setVisibility(View.VISIBLE);
            settingsWidget.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mContext.startActivity(mIntent);
                }
            });
        } else {
            divider.setVisibility(View.GONE);
            settingsWidget.setVisibility(View.GONE);
            settingsWidget.setOnClickListener(null);
        }

        View checkboxContainer = view.findViewById(R.id.checkbox_container);
        if (checkboxContainer != null) {
            checkboxContainer.setOnClickListener(mOnCheckBoxClickListener);
        }
        mCheckBox = (CheckBox) view.findViewById(com.android.internal.R.id.checkbox);
        if (mCheckBox != null) {
            mCheckBox.setChecked(mChecked);
        }
    }

    public boolean isChecked() {
        return mChecked;
        // initialize the checked state of the preference
        super.setChecked(mRule.isEnabled());
    }

    public void updatePreference(AutomaticZenRule rule) {
@@ -126,33 +84,23 @@ public class ZenRulePreference extends TwoTargetPreference {

        if (mRule.isEnabled() != rule.isEnabled()) {
            setChecked(rule.isEnabled());
            setSummary(computeRuleSummary(rule));
        }

        setSummary(computeRuleSummary(rule));
        mRule = rule;
    }

    @Override
    public void onClick() {
        mOnCheckBoxClickListener.onClick(null);
    }

    private void setChecked(boolean checked) {
        mChecked = checked;
        if (mCheckBox != null) {
            mCheckBox.setChecked(checked);
        }
        mContext.startActivity(mIntent);
    }

    private View.OnClickListener mOnCheckBoxClickListener = new View.OnClickListener() {
    @Override
        public void onClick(View v) {
            mRule.setEnabled(!mChecked);
    public void setChecked(boolean checked) {
        mRule.setEnabled(checked);
        mBackend.updateZenRule(mId, mRule);
            setChecked(mRule.isEnabled());
        setAttributes(mRule);
        super.setChecked(checked);
    }
    };

    protected void setAttributes(AutomaticZenRule rule) {
        final boolean isSchedule = ZenModeConfig.isValidScheduleConditionId(
@@ -178,6 +126,30 @@ public class ZenRulePreference extends TwoTargetPreference {
    }

    private String computeRuleSummary(AutomaticZenRule rule) {
        if (rule != null) {
            // handle schedule-based rules
            ScheduleInfo schedule =
                    ZenModeConfig.tryParseScheduleConditionId(rule.getConditionId());
            if (schedule != null) {
                String desc = mScheduleHelper.getDaysAndTimeSummary(mContext, schedule);
                return (desc != null) ? desc :
                        mContext.getResources().getString(
                                R.string.zen_mode_schedule_rule_days_none);
            }

            // handle event-based rules
            ZenModeConfig.EventInfo event =
                    ZenModeConfig.tryParseEventConditionId(rule.getConditionId());
            if (event != null) {
                if (event.calName != null) {
                    return event.calName;
                } else {
                    return mContext.getResources().getString(
                            R.string.zen_mode_event_rule_calendar_any);
                }
            }
        }

        return (rule == null || !rule.isEnabled())
                ? mContext.getResources().getString(R.string.switch_off_text)
                : mContext.getResources().getString(R.string.switch_on_text);
+191 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 com.android.settings.notification.zen;

import android.content.Context;
import android.service.notification.ZenModeConfig.ScheduleInfo;
import android.text.format.DateFormat;

import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;

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

/**
 * Helper class for shared functionality regarding descriptions of custom zen rule schedules.
 */
public class ZenRuleScheduleHelper {
    // per-instance to ensure we're always using the current locale
    private SimpleDateFormat mDayFormat;

    // Default constructor, which will use the current locale.
    public ZenRuleScheduleHelper() {
        mDayFormat = new SimpleDateFormat("EEE");
    }

    // Constructor for tests to provide an explicit locale
    @VisibleForTesting
    public ZenRuleScheduleHelper(Locale locale) {
        mDayFormat = new SimpleDateFormat("EEE", locale);
    }

    /**
     * Returns an ordered, comma-separated list of the days that a schedule applies, or null if no
     * days.
     */
    public String getDaysDescription(Context context, ScheduleInfo schedule) {
        // Compute an ordered, delimited list of day names based on the persisted user config.
        final int[] days = schedule.days;
        if (days != null && days.length > 0) {
            final StringBuilder sb = new StringBuilder();
            final Calendar c = Calendar.getInstance();
            int[] daysOfWeek = ZenModeScheduleDaysSelection.getDaysOfWeekForLocale(c);
            for (int i = 0; i < daysOfWeek.length; i++) {
                final int day = daysOfWeek[i];
                for (int j = 0; j < days.length; j++) {
                    if (day == days[j]) {
                        c.set(Calendar.DAY_OF_WEEK, day);
                        if (sb.length() > 0) {
                            sb.append(context.getString(R.string.summary_divider_text));
                        }
                        sb.append(mDayFormat.format(c.getTime()));
                        break;
                    }
                }
            }

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

    /**
     * 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").
     */
    public String getShortDaysSummary(Context context, 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();
            final Calendar cEnd = Calendar.getInstance();
            int[] daysOfWeek = ZenModeScheduleDaysSelection.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.summary_divider_text));
                    }

                    if (startDay == lastSeenDay) {
                        // last group was only one day
                        cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
                        sb.append(mDayFormat.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.summary_range_symbol_combination,
                                mDayFormat.format(cStart.getTime()),
                                mDayFormat.format(cEnd.getTime())));
                    }
                }
            }

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

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

    /**
     * Combination description for a zen rule schedule including both day summary and time bounds.
     */
    public String getDaysAndTimeSummary(Context context, 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.summary_divider_text));
        sb.append(context.getString(R.string.summary_range_symbol_combination,
                timeString(context, schedule.startHour, schedule.startMinute),
                timeString(context, schedule.endHour, schedule.endMinute)));

        return sb.toString();
    }
}
+191 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 com.android.settings.notification.zen;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.LocaleList;
import android.service.notification.ZenModeConfig.ScheduleInfo;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Calendar;
import java.util.Locale;

@RunWith(AndroidJUnit4.class)
public class ZenRuleScheduleHelperTest {
    private ZenRuleScheduleHelper mScheduleHelper;
    private ScheduleInfo mScheduleInfo;

    private Context mContext;

    @Mock
    private Resources mResources;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);

        // explicitly initialize to Locale.US just for ease of explicitly testing the string values
        // of the days of the week if the test locale doesn't happen to be in the US
        mScheduleHelper = new ZenRuleScheduleHelper(Locale.US);
        mScheduleInfo = new ScheduleInfo();

        mContext = spy(ApplicationProvider.getApplicationContext());
        when(mContext.getResources()).thenReturn(mResources);

        // Resources will be called upon to join strings together, either to get a divider
        // or a combination of two strings. Conveniently, these have different signatures.
        // Divider method calls getString(string divider id)
        when(mResources.getString(anyInt())).thenReturn(",");

        // Combination method calls getString(combination id, first item, second item)
        // and returns "first - second"
        when(mResources.getString(anyInt(), anyString(), anyString())).thenAnswer(
                invocation -> {
                    return invocation.getArgument(1).toString() // first item
                            + "-"
                            + invocation.getArgument(2).toString();  // second item
                });

        // for locale used in time format
        Configuration config = new Configuration();
        config.setLocales(new LocaleList(Locale.US));
        when(mResources.getConfiguration()).thenReturn(config);
    }

    @Test
    public void getDaysDescription() {
        // Test various cases of where the days are set.
        // No days
        mScheduleInfo.days = new int[] {};
        assertThat(mScheduleHelper.getDaysDescription(mContext, mScheduleInfo)).isNull();

        // one day
        mScheduleInfo.days = new int[] {Calendar.FRIDAY};
        assertThat(mScheduleHelper.getDaysDescription(mContext, mScheduleInfo)).isEqualTo("Fri");

        // Monday through Friday
        mScheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY,
                Calendar.THURSDAY, Calendar.FRIDAY};
        assertThat(mScheduleHelper.getDaysDescription(mContext, mScheduleInfo))
                .isEqualTo("Mon,Tue,Wed,Thu,Fri");

        // Some scattered days of the week
        mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.WEDNESDAY, Calendar.THURSDAY,
                Calendar.SATURDAY};
        assertThat(mScheduleHelper.getDaysDescription(mContext, mScheduleInfo))
                .isEqualTo("Sun,Wed,Thu,Sat");
    }

    @Test
    public void getShortDaysSummary_noOrSingleDays() {
        // Test various cases for grouping and not-grouping of days.
        // No days
        mScheduleInfo.days = new int[]{};
        assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)).isNull();

        // A single day at the beginning of the week
        mScheduleInfo.days = new int[]{Calendar.SUNDAY};
        assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)).isEqualTo("Sun");

        // A single day in the middle of the week
        mScheduleInfo.days = new int[]{Calendar.THURSDAY};
        assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)).isEqualTo("Thu");

        // A single day at the end of the week
        mScheduleInfo.days = new int[]{Calendar.SATURDAY};
        assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)).isEqualTo("Sat");
    }

    @Test
    public void getShortDaysSummary_oneGroup() {
        // The whole week
        mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
                Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY};
        assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo))
                .isEqualTo("Sun-Sat");

        // Various cases of one big group
        // Sunday through Thursday
        mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
                Calendar.WEDNESDAY, Calendar.THURSDAY};
        assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo))
                .isEqualTo("Sun-Thu");

        // Wednesday through Saturday
        mScheduleInfo.days = new int[] {Calendar.WEDNESDAY, Calendar.THURSDAY,
                Calendar.FRIDAY, Calendar.SATURDAY};
        assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo))
                .isEqualTo("Wed-Sat");

        // Monday through Friday
        mScheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY,
                Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY};
        assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo))
                .isEqualTo("Mon-Fri");
    }

    @Test
    public void getShortDaysSummary_mixed() {
        // cases combining groups and single days scattered around
        mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.TUESDAY,
                Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.SATURDAY};
        assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo))
                .isEqualTo("Sun,Tue-Thu,Sat");

        mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
                Calendar.WEDNESDAY, Calendar.FRIDAY, Calendar.SATURDAY};
        assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo))
                .isEqualTo("Sun-Wed,Fri-Sat");

        mScheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.WEDNESDAY,
                Calendar.FRIDAY, Calendar.SATURDAY};
        assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo))
                .isEqualTo("Mon,Wed,Fri-Sat");
    }

    @Test
    public void getDaysAndTimeSummary() {
        // Combination days & time settings
        // No days, no output, even if the times are set.
        mScheduleInfo.startHour = 10;
        mScheduleInfo.endHour = 16;
        mScheduleInfo.days = new int[]{};
        assertThat(mScheduleHelper.getDaysAndTimeSummary(mContext, mScheduleInfo)).isNull();

        // If there are days then they are combined with the time combination
        mScheduleInfo.days = new int[]{Calendar.SUNDAY, Calendar.MONDAY, Calendar.WEDNESDAY};
        assertThat(mScheduleHelper.getDaysAndTimeSummary(mContext, mScheduleInfo))
                .isEqualTo("Sun-Mon,Wed,10:00 AM-4:00 PM");
    }
}