diff options
author | John Spurlock <jspurlock@google.com> | 2014-11-03 11:01:51 -0500 |
---|---|---|
committer | John Spurlock <jspurlock@google.com> | 2014-11-05 08:46:01 -0500 |
commit | 37bc92cc2332eb6f864977381135c19d6a081a92 (patch) | |
tree | c563109a324acd911f3ca8b50e09c6090ef36772 | |
parent | 18b055e106d5452ef390716d3ea8d1641a225361 (diff) | |
download | frameworks_base-37bc92cc2332eb6f864977381135c19d6a081a92.zip frameworks_base-37bc92cc2332eb6f864977381135c19d6a081a92.tar.gz frameworks_base-37bc92cc2332eb6f864977381135c19d6a081a92.tar.bz2 |
Zen: Create a new exit condition for "next alarm".
- If the user's next alarm is in the next 12 hrs, provide
this as an exit condition trigger for leaving none/priority.
- Don't display the next alarm condition when downtime is active.
- When the next-alarm exit condition is active, follow changes
to the next alarm, assuming it remains within the 12-hr window.
- Tweak the downtime condition strings to be consistent.
Bug: 16373455
Change-Id: I4020b91d323dead998e62d655132eca07438b148
8 files changed, 366 insertions, 6 deletions
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 9a84a1e..9fb3535 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -498,7 +498,7 @@ public class ZenModeConfig implements Parcelable { } // For built-in conditions - private static final String SYSTEM_AUTHORITY = "android"; + public static final String SYSTEM_AUTHORITY = "android"; // Built-in countdown conditions, e.g. condition://android/countdown/1399917958951 private static final String COUNTDOWN_PATH = "countdown"; diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 1534b49..0e90334 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1907,4 +1907,7 @@ 1. Lenient threshold --> <integer name="config_LTE_RSRP_threshold_type">1</integer> + + <!-- Show the next-alarm as a zen exit condition if it occurs in the next n hours. --> + <integer name="config_next_alarm_condition_lookahead_threshold_hrs">12</integer> </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 61fc161..3051234 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -4879,7 +4879,10 @@ <string name="battery_saver_description">To help improve battery life, battery saver reduces your device’s performance and limits vibration and most background data. Email, messaging, and other apps that rely on syncing may not update unless you open them.\n\nBattery saver turns off automatically when your device is charging.</string> <!-- [CHAR_LIMIT=NONE] Zen mode: Condition summary for built-in downtime condition, if active --> - <string name="downtime_condition_summary">Until your downtime ends at <xliff:g id="formattedTime" example="10.00 PM">%1$s</xliff:g></string> + <string name="downtime_condition_summary">Until your downtime ends at <xliff:g id="formattedTime" example="10:00 PM">%1$s</xliff:g></string> + + <!-- [CHAR_LIMIT=NONE] Zen mode: Condition line one for built-in downtime condition, if active --> + <string name="downtime_condition_line_one">Until your downtime ends</string> <!-- Zen mode condition - summary: time duration in minutes. [CHAR LIMIT=NONE] --> <plurals name="zen_mode_duration_minutes_summary"> @@ -4913,4 +4916,10 @@ <!-- Content description for the Toolbar icon used to collapse an expanded action mode. [CHAR LIMIT=NONE] --> <string name="toolbar_collapse_description">Collapse</string> + + <!-- Zen mode condition - summary: until next alarm. [CHAR LIMIT=NONE] --> + <string name="zen_mode_next_alarm_summary">Until next alarm at <xliff:g id="formattedTime" example="7:30 AM">%1$s</xliff:g></string> + + <!-- Zen mode condition - line one: until next alarm. [CHAR LIMIT=NONE] --> + <string name="zen_mode_next_alarm_line_one">Until next alarm</string> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 3835d8b..f6f8bfa 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1984,12 +1984,16 @@ <java-symbol type="string" name="timepicker_transition_end_radius_multiplier" /> <java-symbol type="string" name="battery_saver_description" /> <java-symbol type="string" name="downtime_condition_summary" /> + <java-symbol type="string" name="downtime_condition_line_one" /> <java-symbol type="string" name="zen_mode_forever" /> <java-symbol type="plurals" name="zen_mode_duration_minutes" /> <java-symbol type="plurals" name="zen_mode_duration_hours" /> <java-symbol type="plurals" name="zen_mode_duration_minutes_summary" /> <java-symbol type="plurals" name="zen_mode_duration_hours_summary" /> <java-symbol type="string" name="zen_mode_until" /> + <java-symbol type="string" name="zen_mode_next_alarm_summary" /> + <java-symbol type="string" name="zen_mode_next_alarm_line_one" /> + <java-symbol type="integer" name="config_next_alarm_condition_lookahead_threshold_hrs" /> <java-symbol type="string" name="item_is_selected" /> <java-symbol type="string" name="day_of_week_label_typeface" /> diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java index c840f17..28ecbf9 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java +++ b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java @@ -69,6 +69,7 @@ public class ZenModePanel extends LinearLayout { private static final int TIME_CONDITION_INDEX = 1; private static final int FIRST_CONDITION_INDEX = 2; private static final float SILENT_HINT_PULSE_SCALE = 1.1f; + private static final long SELECT_DEFAULT_DELAY = 300; public static final Intent ZEN_SETTINGS = new Intent(Settings.ACTION_ZEN_MODE_SETTINGS); @@ -373,8 +374,9 @@ public class ZenModePanel extends LinearLayout { if (isDowntime(mSessionExitCondition) && !foundDowntime) { bind(mSessionExitCondition, null); } - // ensure something is selected - checkForDefault(); + // ensure something is selected, after waiting for providers to respond + mHandler.removeMessages(H.SELECT_DEFAULT); + mHandler.sendEmptyMessageDelayed(H.SELECT_DEFAULT, SELECT_DEFAULT_DELAY); } private static boolean isDowntime(Condition c) { @@ -385,7 +387,8 @@ public class ZenModePanel extends LinearLayout { return (ConditionTag) mZenConditions.getChildAt(index).getTag(); } - private void checkForDefault() { + private void handleSelectDefault() { + if (!mExpanded) return; // are we left without anything selected? if so, set a default for (int i = 0; i < mZenConditions.getChildCount(); i++) { if (getConditionTagAt(i).rb.isChecked()) { @@ -638,6 +641,7 @@ public class ZenModePanel extends LinearLayout { private static final int UPDATE_CONDITIONS = 1; private static final int EXIT_CONDITION_CHANGED = 2; private static final int UPDATE_ZEN = 3; + private static final int SELECT_DEFAULT = 4; private H() { super(Looper.getMainLooper()); @@ -651,6 +655,8 @@ public class ZenModePanel extends LinearLayout { handleExitConditionChanged((Condition) msg.obj); } else if (msg.what == UPDATE_ZEN) { handleUpdateZen(msg.arg1); + } else if (msg.what == SELECT_DEFAULT) { + handleSelectDefault(); } } } diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java index 05ad1fe..fc7b5a9 100644 --- a/services/core/java/com/android/server/notification/ConditionProviders.java +++ b/services/core/java/com/android/server/notification/ConditionProviders.java @@ -52,6 +52,7 @@ public class ConditionProviders extends ManagedServices { private final ArrayList<ConditionRecord> mRecords = new ArrayList<ConditionRecord>(); private final CountdownConditionProvider mCountdown = new CountdownConditionProvider(); private final DowntimeConditionProvider mDowntime = new DowntimeConditionProvider(); + private final NextAlarmConditionProvider mNextAlarm = new NextAlarmConditionProvider(); private Condition mExitCondition; private ComponentName mExitConditionComponent; @@ -99,6 +100,7 @@ public class ConditionProviders extends ManagedServices { } mCountdown.dump(pw, filter); mDowntime.dump(pw, filter); + mNextAlarm.dump(pw, filter); } @Override @@ -116,6 +118,23 @@ public class ConditionProviders extends ManagedServices { registerService(mDowntime.asInterface(), DowntimeConditionProvider.COMPONENT, UserHandle.USER_OWNER); mDowntime.setCallback(new DowntimeCallback()); + mNextAlarm.attachBase(mContext); + registerService(mNextAlarm.asInterface(), NextAlarmConditionProvider.COMPONENT, + UserHandle.USER_OWNER); + mNextAlarm.setCallback(new NextAlarmConditionProvider.Callback() { + @Override + public boolean isInDowntime() { + return mDowntime.isInDowntime(); + } + }); + } + + @Override + public void onUserSwitched() { + super.onUserSwitched(); + if (mNextAlarm != null) { + mNextAlarm.onUserSwitched(); + } } @Override diff --git a/services/core/java/com/android/server/notification/DowntimeConditionProvider.java b/services/core/java/com/android/server/notification/DowntimeConditionProvider.java index efe47c3..2a4b718 100644 --- a/services/core/java/com/android/server/notification/DowntimeConditionProvider.java +++ b/services/core/java/com/android/server/notification/DowntimeConditionProvider.java @@ -158,7 +158,8 @@ public class DowntimeConditionProvider extends ConditionProviderService { final long time = getTime(System.currentTimeMillis(), downtime.endHour, downtime.endMinute); final String formatted = new SimpleDateFormat(pattern, locale).format(new Date(time)); final String summary = mContext.getString(R.string.downtime_condition_summary, formatted); - return new Condition(id, summary, "", "", 0, state, Condition.FLAG_RELEVANT_NOW); + final String line1 = mContext.getString(R.string.downtime_condition_line_one); + return new Condition(id, summary, line1, formatted, 0, state, Condition.FLAG_RELEVANT_NOW); } public boolean isDowntimeCondition(Condition condition) { diff --git a/services/core/java/com/android/server/notification/NextAlarmConditionProvider.java b/services/core/java/com/android/server/notification/NextAlarmConditionProvider.java new file mode 100644 index 0000000..dba203b --- /dev/null +++ b/services/core/java/com/android/server/notification/NextAlarmConditionProvider.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2014 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.server.notification; + +import android.app.ActivityManager; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.AlarmManager.AlarmClockInfo; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.UserHandle; +import android.service.notification.Condition; +import android.service.notification.ConditionProviderService; +import android.service.notification.IConditionProvider; +import android.service.notification.ZenModeConfig; +import android.util.TimeUtils; +import android.text.format.DateFormat; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.R; +import com.android.server.notification.NotificationManagerService.DumpFilter; + +import java.io.PrintWriter; +import java.util.Locale; + +/** Built-in zen condition provider for alarm clock conditions */ +public class NextAlarmConditionProvider extends ConditionProviderService { + private static final String TAG = "NextAlarmConditions"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final String ACTION_TRIGGER = TAG + ".trigger"; + private static final String EXTRA_TRIGGER = "trigger"; + private static final int REQUEST_CODE = 100; + private static final long SECONDS = 1000; + private static final long HOURS = 60 * 60 * SECONDS; + private static final long NEXT_ALARM_UPDATE_DELAY = 1 * SECONDS; // treat clear+set as update + private static final long EARLY = 5 * SECONDS; // fire early, ensure alarm stream is unmuted + private static final String NEXT_ALARM_PATH = "next_alarm"; + public static final ComponentName COMPONENT = + new ComponentName("android", NextAlarmConditionProvider.class.getName()); + + private final Context mContext = this; + private final H mHandler = new H(); + + private boolean mConnected; + private boolean mRegistered; + private AlarmManager mAlarmManager; + private int mCurrentUserId; + private long mLookaheadThreshold; + private long mScheduledAlarmTime; + private Callback mCallback; + private Uri mCurrentSubscription; + private PowerManager.WakeLock mWakeLock; + + public NextAlarmConditionProvider() { + if (DEBUG) Slog.d(TAG, "new NextAlarmConditionProvider()"); + } + + public void dump(PrintWriter pw, DumpFilter filter) { + pw.println(" NextAlarmConditionProvider:"); + pw.print(" mConnected="); pw.println(mConnected); + pw.print(" mRegistered="); pw.println(mRegistered); + pw.print(" mCurrentUserId="); pw.println(mCurrentUserId); + pw.print(" mScheduledAlarmTime="); pw.println(formatAlarmDebug(mScheduledAlarmTime)); + pw.print(" mLookaheadThreshold="); pw.print(mLookaheadThreshold); + pw.print(" ("); TimeUtils.formatDuration(mLookaheadThreshold, pw); pw.println(")"); + pw.print(" mCurrentSubscription="); pw.println(mCurrentSubscription); + pw.print(" mWakeLock="); pw.println(mWakeLock); + } + + public void setCallback(Callback callback) { + mCallback = callback; + } + + @Override + public void onConnected() { + if (DEBUG) Slog.d(TAG, "onConnected"); + mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + final PowerManager p = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + mWakeLock = p.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mLookaheadThreshold = mContext.getResources() + .getInteger(R.integer.config_next_alarm_condition_lookahead_threshold_hrs) * HOURS; + init(); + mConnected = true; + } + + public void onUserSwitched() { + if (DEBUG) Slog.d(TAG, "onUserSwitched"); + if (mConnected) { + init(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (DEBUG) Slog.d(TAG, "onDestroy"); + if (mConnected) { + mContext.unregisterReceiver(mReceiver); + } + mConnected = false; + } + + @Override + public void onRequestConditions(int relevance) { + if (!mConnected || (relevance & Condition.FLAG_RELEVANT_NOW) == 0) return; + + final AlarmClockInfo nextAlarm = mAlarmManager.getNextAlarmClock(mCurrentUserId); + if (nextAlarm == null) return; // no next alarm + if (mCallback != null && mCallback.isInDowntime()) return; // prefer downtime condition + if (!isWithinLookaheadThreshold(nextAlarm)) return; // alarm not within window + + // next alarm exists, and is within the configured lookahead threshold + notifyCondition(newConditionId(), nextAlarm, true, "request"); + } + + private boolean isWithinLookaheadThreshold(AlarmClockInfo alarm) { + if (alarm == null) return false; + final long delta = getEarlyTriggerTime(alarm) - System.currentTimeMillis(); + return delta > 0 && (mLookaheadThreshold <= 0 || delta < mLookaheadThreshold); + } + + @Override + public void onSubscribe(Uri conditionId) { + if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId); + if (!isNextAlarmCondition(conditionId)) { + notifyCondition(conditionId, null, false, "badCondition"); + return; + } + mCurrentSubscription = conditionId; + mHandler.postEvaluate(0); + } + + private static long getEarlyTriggerTime(AlarmClockInfo alarm) { + return alarm != null ? (alarm.getTriggerTime() - EARLY) : 0; + } + + private void handleEvaluate() { + final AlarmClockInfo nextAlarm = mAlarmManager.getNextAlarmClock(mCurrentUserId); + final long triggerTime = getEarlyTriggerTime(nextAlarm); + final boolean withinThreshold = isWithinLookaheadThreshold(nextAlarm); + if (DEBUG) Slog.d(TAG, "handleEvaluate mCurrentSubscription=" + mCurrentSubscription + + " nextAlarm=" + formatAlarmDebug(triggerTime) + + " withinThreshold=" + withinThreshold); + if (mCurrentSubscription == null) return; // no one cares + if (!withinThreshold) { + // triggertime invalid or in the past, condition = false + notifyCondition(mCurrentSubscription, nextAlarm, false, "!withinThreshold"); + mCurrentSubscription = null; + return; + } + // triggertime in the future, condition = true, schedule alarm + notifyCondition(mCurrentSubscription, nextAlarm, true, "withinThreshold"); + rescheduleAlarm(triggerTime); + } + + private static String formatDuration(long millis) { + final StringBuilder sb = new StringBuilder(); + TimeUtils.formatDuration(millis, sb); + return sb.toString(); + } + + private void rescheduleAlarm(long time) { + if (DEBUG) Slog.d(TAG, "rescheduleAlarm " + time); + final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, REQUEST_CODE, + new Intent(ACTION_TRIGGER) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) + .putExtra(EXTRA_TRIGGER, time), + PendingIntent.FLAG_UPDATE_CURRENT); + alarms.cancel(pendingIntent); + mScheduledAlarmTime = time; + if (time > 0) { + if (DEBUG) Slog.d(TAG, String.format("Scheduling alarm for %s (in %s)", + formatAlarmDebug(time), formatDuration(time - System.currentTimeMillis()))); + alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent); + } + } + + private void notifyCondition(Uri id, AlarmClockInfo alarm, boolean state, String reason) { + final String formattedAlarm = alarm == null ? "" : formatAlarm(alarm.getTriggerTime()); + if (DEBUG) Slog.d(TAG, "notifyCondition " + state + " alarm=" + formattedAlarm + " reason=" + + reason); + notifyCondition(new Condition(id, + mContext.getString(R.string.zen_mode_next_alarm_summary, formattedAlarm), + mContext.getString(R.string.zen_mode_next_alarm_line_one), + formattedAlarm, 0, + state ? Condition.STATE_TRUE : Condition.STATE_FALSE, + Condition.FLAG_RELEVANT_NOW)); + } + + @Override + public void onUnsubscribe(Uri conditionId) { + if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId); + if (conditionId != null && conditionId.equals(mCurrentSubscription)) { + mCurrentSubscription = null; + rescheduleAlarm(0); + } + } + + public void attachBase(Context base) { + attachBaseContext(base); + } + + public IConditionProvider asInterface() { + return (IConditionProvider) onBind(null); + } + + private Uri newConditionId() { + return new Uri.Builder().scheme(Condition.SCHEME) + .authority(ZenModeConfig.SYSTEM_AUTHORITY) + .appendPath(NEXT_ALARM_PATH) + .appendPath(Integer.toString(mCurrentUserId)) + .build(); + } + + private boolean isNextAlarmCondition(Uri conditionId) { + return conditionId != null && conditionId.getScheme().equals(Condition.SCHEME) + && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY) + && conditionId.getPathSegments().size() == 2 + && conditionId.getPathSegments().get(0).equals(NEXT_ALARM_PATH) + && conditionId.getPathSegments().get(1).equals(Integer.toString(mCurrentUserId)); + } + + private void init() { + if (mRegistered) { + mContext.unregisterReceiver(mReceiver); + } + mCurrentUserId = ActivityManager.getCurrentUser(); + final IntentFilter filter = new IntentFilter(); + filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); + filter.addAction(ACTION_TRIGGER); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + mContext.registerReceiverAsUser(mReceiver, new UserHandle(mCurrentUserId), filter, null, + null); + mRegistered = true; + mHandler.postEvaluate(0); + } + + private String formatAlarm(long time) { + return formatAlarm(time, "Hm", "hma"); + } + + private String formatAlarm(long time, String skeleton24, String skeleton12) { + final String skeleton = DateFormat.is24HourFormat(mContext) ? skeleton24 : skeleton12; + final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton); + return DateFormat.format(pattern, time).toString(); + } + + private String formatAlarmDebug(AlarmClockInfo alarm) { + return formatAlarmDebug(alarm != null ? alarm.getTriggerTime() : 0); + } + + private String formatAlarmDebug(long time) { + if (time <= 0) return Long.toString(time); + return String.format("%s (%s)", time, formatAlarm(time, "Hms", "hmsa")); + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (DEBUG) Slog.d(TAG, "onReceive " + action); + long delay = 0; + if (action.equals(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)) { + delay = NEXT_ALARM_UPDATE_DELAY; + if (DEBUG) Slog.d(TAG, String.format(" next alarm for user %s: %s", + mCurrentUserId, + formatAlarmDebug(mAlarmManager.getNextAlarmClock(mCurrentUserId)))); + } + mHandler.postEvaluate(delay); + mWakeLock.acquire(delay + 5000); // stay awake during evaluate + } + }; + + public interface Callback { + boolean isInDowntime(); + } + + private class H extends Handler { + private static final int MSG_EVALUATE = 1; + + public void postEvaluate(long delay) { + removeMessages(MSG_EVALUATE); + sendEmptyMessageDelayed(MSG_EVALUATE, delay); + } + + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_EVALUATE) { + handleEvaluate(); + } + } + } +} |