Merge "Add schedule setting page for time-based modes." into main

This commit is contained in:
Yuri Lin
2024-06-10 20:50:57 +00:00
committed by Android (Google) Code Review
13 changed files with 1129 additions and 3 deletions

View File

@@ -0,0 +1,56 @@
/*
* 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 com.android.settings.notification.modes;
import android.content.Context;
import android.service.notification.ZenModeConfig;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
import androidx.preference.TwoStatePreference;
/**
* Preference controller controlling whether a time schedule-based mode ends at the next alarm.
*/
class ZenModeExitAtAlarmPreferenceController extends
AbstractZenModePreferenceController implements Preference.OnPreferenceChangeListener {
private ZenModeConfig.ScheduleInfo mSchedule;
ZenModeExitAtAlarmPreferenceController(Context context,
String key, ZenModesBackend backend) {
super(context, key, backend);
}
@Override
public void updateState(Preference preference, @NonNull ZenMode zenMode) {
mSchedule = ZenModeConfig.tryParseScheduleConditionId(zenMode.getRule().getConditionId());
((TwoStatePreference) preference).setChecked(mSchedule.exitAtAlarm);
}
@Override
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
final boolean exitAtAlarm = (Boolean) newValue;
if (mSchedule.exitAtAlarm != exitAtAlarm) {
mSchedule.exitAtAlarm = exitAtAlarm;
return saveMode(mode -> {
mode.getRule().setConditionId(ZenModeConfig.toScheduleConditionId(mSchedule));
return mode;
});
}
return false;
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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 com.android.settings.notification.modes;
import android.app.settings.SettingsEnums;
import android.content.Context;
import com.android.settings.R;
import com.android.settingslib.core.AbstractPreferenceController;
import java.util.ArrayList;
import java.util.List;
/**
* Settings page to set a schedule for a mode that turns on automatically based on specific days
* of the week and times of day.
*/
public class ZenModeSetScheduleFragment extends ZenModeFragmentBase {
@Override
protected int getPreferenceScreenResId() {
return R.xml.modes_set_schedule;
}
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
List<AbstractPreferenceController> controllers = new ArrayList<>();
controllers.add(
new ZenModeSetSchedulePreferenceController(mContext, this, "schedule", mBackend));
controllers.add(
new ZenModeExitAtAlarmPreferenceController(mContext, "exit_at_alarm", mBackend));
return controllers;
}
@Override
public int getMetricsCategory() {
// TODO: b/332937635 - make this the correct metrics category
return SettingsEnums.NOTIFICATION_ZEN_MODE_SCHEDULE_RULE;
}
}

View File

@@ -0,0 +1,274 @@
/*
* 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 com.android.settings.notification.modes;
import android.app.Flags;
import android.content.Context;
import android.service.notification.SystemZenRules;
import android.service.notification.ZenModeConfig;
import android.text.format.DateFormat;
import android.util.ArraySet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.ToggleButton;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settingslib.widget.LayoutPreference;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Arrays;
import java.util.Calendar;
import java.util.function.Function;
/**
* Preference controller for setting the start and end time and days of the week associated with
* an automatic zen mode.
*/
class ZenModeSetSchedulePreferenceController extends AbstractZenModePreferenceController {
// per-instance to ensure we're always using the current locale
// E = day of the week; "EEEEE" is the shortest version; "EEEE" is the full name
private final SimpleDateFormat mShortDayFormat = new SimpleDateFormat("EEEEE");
private final SimpleDateFormat mLongDayFormat = new SimpleDateFormat("EEEE");
private static final String TAG = "ZenModeSetSchedulePreferenceController";
private Fragment mParent;
private ZenModeConfig.ScheduleInfo mSchedule;
ZenModeSetSchedulePreferenceController(Context context, Fragment parent, String key,
ZenModesBackend backend) {
super(context, key, backend);
mParent = parent;
}
@Override
public void updateState(Preference preference, @NonNull ZenMode zenMode) {
mSchedule = ZenModeConfig.tryParseScheduleConditionId(zenMode.getRule().getConditionId());
LayoutPreference layoutPref = (LayoutPreference) preference;
TextView start = layoutPref.findViewById(R.id.start_time);
start.setText(timeString(mSchedule.startHour, mSchedule.startMinute));
start.setOnClickListener(
timePickerLauncher(mSchedule.startHour, mSchedule.startMinute, mStartSetter));
TextView end = layoutPref.findViewById(R.id.end_time);
end.setText(timeString(mSchedule.endHour, mSchedule.endMinute));
end.setOnClickListener(
timePickerLauncher(mSchedule.endHour, mSchedule.endMinute, mEndSetter));
TextView durationView = layoutPref.findViewById(R.id.schedule_duration);
durationView.setText(getScheduleDurationDescription(mSchedule));
ViewGroup daysContainer = layoutPref.findViewById(R.id.days_of_week_container);
setupDayToggles(daysContainer, mSchedule, Calendar.getInstance());
}
private String timeString(int hour, int minute) {
final Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY, hour);
c.set(Calendar.MINUTE, minute);
return DateFormat.getTimeFormat(mContext).format(c.getTime());
}
private boolean isValidTime(int hour, int minute) {
return ZenModeConfig.isValidHour(hour) && ZenModeConfig.isValidMinute(minute);
}
private String getScheduleDurationDescription(ZenModeConfig.ScheduleInfo schedule) {
final int startMin = 60 * schedule.startHour + schedule.startMinute;
final int endMin = 60 * schedule.endHour + schedule.endMinute;
final boolean nextDay = startMin >= endMin;
Duration scheduleDuration;
if (nextDay) {
// add one day's worth of minutes (24h x 60min) to end minute for end time calculation
int endMinNextDay = endMin + (24 * 60);
scheduleDuration = Duration.ofMinutes(endMinNextDay - startMin);
} else {
scheduleDuration = Duration.ofMinutes(endMin - startMin);
}
int hours = scheduleDuration.toHoursPart();
int minutes = scheduleDuration.minusHours(hours).toMinutesPart();
return mContext.getString(R.string.zen_mode_schedule_duration, hours, minutes);
}
@VisibleForTesting
protected Function<ZenMode, ZenMode> updateScheduleMode(ZenModeConfig.ScheduleInfo schedule) {
return (zenMode) -> {
zenMode.getRule().setConditionId(ZenModeConfig.toScheduleConditionId(schedule));
if (Flags.modesApi() && Flags.modesUi()) {
zenMode.getRule().setTriggerDescription(
SystemZenRules.getTriggerDescriptionForScheduleTime(mContext, schedule));
}
return zenMode;
};
}
private ZenModeTimePickerFragment.TimeSetter mStartSetter = (hour, minute) -> {
if (!isValidTime(hour, minute)) {
return;
}
if (hour == mSchedule.startHour && minute == mSchedule.startMinute) {
return;
}
mSchedule.startHour = hour;
mSchedule.startMinute = minute;
saveMode(updateScheduleMode(mSchedule));
};
private ZenModeTimePickerFragment.TimeSetter mEndSetter = (hour, minute) -> {
if (!isValidTime(hour, minute)) {
return;
}
if (hour == mSchedule.endHour && minute == mSchedule.endMinute) {
return;
}
mSchedule.endHour = hour;
mSchedule.endMinute = minute;
saveMode(updateScheduleMode(mSchedule));
};
private View.OnClickListener timePickerLauncher(int hour, int minute,
ZenModeTimePickerFragment.TimeSetter timeSetter) {
return v -> {
final ZenModeTimePickerFragment frag = new ZenModeTimePickerFragment(mContext, hour,
minute, timeSetter);
frag.show(mParent.getParentFragmentManager(), TAG);
};
}
protected 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;
}
@VisibleForTesting
protected void setupDayToggles(ViewGroup dayContainer, ZenModeConfig.ScheduleInfo schedule,
Calendar c) {
int[] daysOfWeek = getDaysOfWeekForLocale(c);
// Index in daysOfWeek is associated with the [idx]'th object in the list of days in the
// layout. Note that because the order of the days of the week may differ per locale, this
// is not necessarily the same as the actual value of the day number at that index.
for (int i = 0; i < daysOfWeek.length; i++) {
ToggleButton dayToggle = dayContainer.findViewById(resIdForDayIndex(i));
if (dayToggle == null) {
continue;
}
final int day = daysOfWeek[i];
c.set(Calendar.DAY_OF_WEEK, day);
// find current setting for this day
boolean dayEnabled = false;
if (schedule.days != null) {
for (int idx = 0; idx < schedule.days.length; idx++) {
if (schedule.days[idx] == day) {
dayEnabled = true;
break;
}
}
}
// On/off is indicated by visuals, and both states share the shortest (one-character)
// day label.
dayToggle.setTextOn(mShortDayFormat.format(c.getTime()));
dayToggle.setTextOff(mShortDayFormat.format(c.getTime()));
dayToggle.setContentDescription(mLongDayFormat.format(c.getTime()));
dayToggle.setChecked(dayEnabled);
dayToggle.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (updateScheduleDays(schedule, day, isChecked)) {
saveMode(updateScheduleMode(schedule));
}
});
// If display and text settings cause the text to be larger than its containing box,
// don't show scrollbars.
dayToggle.setVerticalScrollBarEnabled(false);
dayToggle.setHorizontalScrollBarEnabled(false);
}
}
// Updates the set of enabled days in provided schedule to either turn on or off the given day.
// The format of days in ZenModeConfig.ScheduleInfo is an array of days, where inclusion means
// the schedule is set to run on that day. Returns whether anything was changed.
@VisibleForTesting
protected static boolean updateScheduleDays(ZenModeConfig.ScheduleInfo schedule, int day,
boolean set) {
// Build a set representing the days that are currently set in mSchedule.
ArraySet<Integer> daySet = new ArraySet();
if (schedule.days != null) {
for (int i = 0; i < schedule.days.length; i++) {
daySet.add(schedule.days[i]);
}
}
if (daySet.contains(day) != set) {
if (set) {
daySet.add(day);
} else {
daySet.remove(day);
}
// rebuild days array for mSchedule
final int[] out = new int[daySet.size()];
for (int i = 0; i < daySet.size(); i++) {
out[i] = daySet.valueAt(i);
}
Arrays.sort(out);
schedule.days = out;
return true;
}
// If the setting is the same as it was before, no need to update anything.
return false;
}
protected static int resIdForDayIndex(int idx) {
switch (idx) {
case 0:
return R.id.day0;
case 1:
return R.id.day1;
case 2:
return R.id.day2;
case 3:
return R.id.day3;
case 4:
return R.id.day4;
case 5:
return R.id.day5;
case 6:
return R.id.day6;
default:
return 0; // unknown
}
}
}

View File

@@ -16,6 +16,7 @@
package com.android.settings.notification.modes;
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
@@ -32,13 +33,13 @@ import com.android.settings.core.SubSettingLauncher;
import com.android.settingslib.PrimarySwitchPreference;
/**
* Preference controller for the link
* Preference controller for the link to an individual mode's configuration page.
*/
public class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenceController {
class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenceController {
@VisibleForTesting
protected static final String AUTOMATIC_TRIGGER_PREF_KEY = "zen_automatic_trigger_settings";
public ZenModeSetTriggerLinkPreferenceController(Context context, String key,
ZenModeSetTriggerLinkPreferenceController(Context context, String key,
ZenModesBackend backend) {
super(context, key, backend);
}
@@ -66,6 +67,16 @@ public class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePr
// TODO: b/341961712 - direct preference to app-owned intent if available
switch (zenMode.getRule().getType()) {
case TYPE_SCHEDULE_TIME:
switchPref.setTitle(R.string.zen_mode_set_schedule_link);
switchPref.setSummary(zenMode.getRule().getTriggerDescription());
switchPref.setIntent(new SubSettingLauncher(mContext)
.setDestination(ZenModeSetScheduleFragment.class.getName())
// TODO: b/332937635 - set correct metrics category
.setSourceMetricsCategory(0)
.setArguments(bundle)
.toIntent());
break;
case TYPE_SCHEDULE_CALENDAR:
switchPref.setTitle(R.string.zen_mode_set_calendar_link);
switchPref.setSummary(zenMode.getRule().getTriggerDescription());

View File

@@ -0,0 +1,76 @@
/*
* 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 com.android.settings.notification.modes;
import android.app.Dialog;
import android.app.TimePickerDialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.widget.TimePicker;
import androidx.annotation.NonNull;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
/**
* Dialog that shows when a user selects a (start or end) time to edit for a schedule-based mode.
*/
public class ZenModeTimePickerFragment extends InstrumentedDialogFragment implements
TimePickerDialog.OnTimeSetListener {
private final Context mContext;
private final TimeSetter mTimeSetter;
private final int mHour;
private final int mMinute;
public ZenModeTimePickerFragment(Context context, int hour, int minute,
@NonNull TimeSetter timeSetter) {
super();
mContext = context;
mHour = hour;
mMinute = minute;
mTimeSetter = timeSetter;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new TimePickerDialog(mContext, this, mHour, mMinute,
DateFormat.is24HourFormat(mContext));
}
/**
* Calls the provided TimeSetter's setTime() method when a time is set on the TimePicker.
*/
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
mTimeSetter.setTime(hourOfDay, minute);
}
@Override
public int getMetricsCategory() {
// TODO: b/332937635 - set correct metrics category (or decide to keep this one?)
return SettingsEnums.DIALOG_ZEN_TIMEPICKER;
}
/**
* Interface for a method to pass into the TimePickerFragment that specifies what to do when the
* time is updated.
*/
public interface TimeSetter {
void setTime(int hour, int minute);
}
}