Merge "Zen: Create a new exit condition for "next alarm"." into lmp-mr1-dev automerge: 0bdefb9
automerge: 513f260

* commit '513f26020095c409aa3e828c98485ed20aa9eaa9':
  Zen: Create a new exit condition for "next alarm".
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 @@
     }
 
     // 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 d8b5b0d..56408ca 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 861edac..58c837e 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -4871,7 +4871,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">
@@ -4905,4 +4908,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 @@
     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 @@
         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 @@
         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 @@
         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 @@
                 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 @@
     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 @@
         }
         mCountdown.dump(pw, filter);
         mDowntime.dump(pw, filter);
+        mNextAlarm.dump(pw, filter);
     }
 
     @Override
@@ -116,6 +118,23 @@
         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 @@
         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();
+            }
+        }
+    }
+}