Zen: Stateful condition panel.
When DND is on, expanded panel shows the current time
condition, or time remaining. The last time bucket
selected is remembered as the default option for the
next time.
Move the server-side countdown helper into a proper
condition provider, but register it in-process as a
system provider.
Move common countdown condition parsing into ZenModeConfig
to reuse from system components.
Keep the manual exit condition around in zen mode config
and add plumbing for getting / listening to the
controller.
Keep the last QS detail panel around instead of
recreating it every time.
Fix the time condition's plus and minus button
enabling logic, and enhance the click handler to
deal properly with the next or previous bucket.
Bug:15344758
Change-Id: Ie7018a1c20e20f6d7e5f9e7874188374e6f8e2ab
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index d02fc7b..68a3d30 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -21,6 +21,7 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
+import android.util.Slog;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -37,6 +38,7 @@
* @hide
*/
public class ZenModeConfig implements Parcelable {
+ private static String TAG = "ZenModeConfig";
public static final String SLEEP_MODE_NIGHTS = "nights";
public static final String SLEEP_MODE_WEEKNIGHTS = "weeknights";
@@ -65,6 +67,9 @@
private static final String CONDITION_ATT_COMPONENT = "component";
private static final String CONDITION_ATT_ID = "id";
+ private static final String EXIT_CONDITION_TAG = "exitCondition";
+ private static final String EXIT_CONDITION_ATT_ID = "id";
+
public boolean allowCalls;
public boolean allowMessages;
public int allowFrom = SOURCE_ANYONE;
@@ -76,6 +81,7 @@
public int sleepEndMinute;
public ComponentName[] conditionComponents;
public Uri[] conditionIds;
+ public Uri exitConditionId;
public ZenModeConfig() { }
@@ -100,6 +106,7 @@
source.readTypedArray(conditionIds, Uri.CREATOR);
}
allowFrom = source.readInt();
+ exitConditionId = source.readParcelable(null);
}
@Override
@@ -129,6 +136,7 @@
dest.writeInt(0);
}
dest.writeInt(allowFrom);
+ dest.writeParcelable(exitConditionId, 0);
}
@Override
@@ -144,6 +152,7 @@
.append(conditionComponents == null ? null : TextUtils.join(",", conditionComponents))
.append(",conditionIds=")
.append(conditionIds == null ? null : TextUtils.join(",", conditionIds))
+ .append(",exitConditionId=").append(exitConditionId)
.append(']').toString();
}
@@ -174,14 +183,16 @@
&& other.sleepEndHour == sleepEndHour
&& other.sleepEndMinute == sleepEndMinute
&& Objects.deepEquals(other.conditionComponents, conditionComponents)
- && Objects.deepEquals(other.conditionIds, conditionIds);
+ && Objects.deepEquals(other.conditionIds, conditionIds)
+ && Objects.equals(other.exitConditionId, exitConditionId);
}
@Override
public int hashCode() {
return Objects.hash(allowCalls, allowMessages, allowFrom, sleepMode,
sleepStartHour, sleepStartMinute, sleepEndHour, sleepEndMinute,
- Arrays.hashCode(conditionComponents), Arrays.hashCode(conditionIds));
+ Arrays.hashCode(conditionComponents), Arrays.hashCode(conditionIds),
+ exitConditionId);
}
public boolean isValid() {
@@ -239,6 +250,8 @@
conditionComponents.add(component);
conditionIds.add(conditionId);
}
+ } else if (EXIT_CONDITION_TAG.equals(tag)) {
+ rt.exitConditionId = safeUri(parser, EXIT_CONDITION_ATT_ID);
}
}
}
@@ -275,6 +288,11 @@
out.endTag(null, CONDITION_TAG);
}
}
+ if (exitConditionId != null) {
+ out.startTag(null, EXIT_CONDITION_TAG);
+ out.attribute(null, EXIT_CONDITION_ATT_ID, exitConditionId.toString());
+ out.endTag(null, EXIT_CONDITION_TAG);
+ }
out.endTag(null, ZEN_TAG);
}
@@ -338,4 +356,33 @@
return new ZenModeConfig[size];
}
};
+
+ // Built-in countdown conditions, e.g. condition://android/countdown/1399917958951
+
+ private static final String COUNTDOWN_AUTHORITY = "android";
+ private static final String COUNTDOWN_PATH = "countdown";
+
+ public static Uri toCountdownConditionId(long time) {
+ return new Uri.Builder().scheme(Condition.SCHEME)
+ .authority(COUNTDOWN_AUTHORITY)
+ .appendPath(COUNTDOWN_PATH)
+ .appendPath(Long.toString(time))
+ .build();
+ }
+
+ public static long tryParseCountdownConditionId(Uri conditionId) {
+ if (!Condition.isValidId(conditionId, COUNTDOWN_AUTHORITY)) return 0;
+ if (conditionId.getPathSegments().size() != 2
+ || !COUNTDOWN_PATH.equals(conditionId.getPathSegments().get(0))) return 0;
+ try {
+ return Long.parseLong(conditionId.getPathSegments().get(1));
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Error parsing countdown condition: " + conditionId, e);
+ return 0;
+ }
+ }
+
+ public static boolean isValidCountdownConditionId(Uri conditionId) {
+ return tryParseCountdownConditionId(conditionId) != 0;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 2bf369a..07ffd66 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -156,12 +156,14 @@
AnimatorListener listener = null;
if (show) {
if (mDetailRecord != null) return;
- final View detail = r.tile.createDetailView(mContext, mDetail);
- if (detail == null) return;
+ if (r.detailView == null) {
+ r.detailView = r.tile.createDetailView(mContext, mDetail);
+ }
+ if (r.detailView == null) return;
mDetailRecord = r;
mDetail.removeAllViews();
mDetail.bringToFront();
- mDetail.addView(detail);
+ mDetail.addView(r.detailView);
} else {
if (mDetailRecord == null) return;
listener = mTeardownDetailWhenDone;
@@ -273,6 +275,7 @@
private static final class TileRecord {
QSTile<?> tile;
QSTileView tileView;
+ View detailView;
int row;
int col;
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java
index 2edd8d5..5388994 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java
@@ -135,7 +135,7 @@
}
private Drawable getTileBackground() {
- final int[] attrs = new int[] { android.R.attr.selectableItemBackground};
+ final int[] attrs = new int[] { android.R.attr.selectableItemBackgroundBorderless };
final TypedArray ta = mContext.obtainStyledAttributes(attrs);
final Drawable d = ta.getDrawable(0);
ta.recycle();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java
index 6225c12..4cf72a2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.policy;
+import android.net.Uri;
import android.service.notification.Condition;
public interface ZenModeController {
@@ -24,10 +25,12 @@
void setZen(boolean zen);
boolean isZen();
void requestConditions(boolean request);
- void select(Condition condition);
+ void setExitConditionId(Uri exitConditionId);
+ Uri getExitConditionId();
public static class Callback {
public void onZenChanged(boolean zen) {}
+ public void onExitConditionChanged(Uri exitConditionId) {}
public void onConditionsChanged(Condition[] conditions) {}
}
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
index adf2935..da8fd32 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -25,6 +25,7 @@
import android.provider.Settings.Global;
import android.service.notification.Condition;
import android.service.notification.IConditionListener;
+import android.service.notification.ZenModeConfig;
import android.util.Slog;
import com.android.systemui.qs.GlobalSetting;
@@ -35,10 +36,12 @@
/** Platform implementation of the zen mode controller. **/
public class ZenModeControllerImpl implements ZenModeController {
private static final String TAG = "ZenModeControllerImpl";
+ private static final boolean DEBUG = false;
private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
private final Context mContext;
- private final GlobalSetting mSetting;
+ private final GlobalSetting mModeSetting;
+ private final GlobalSetting mConfigSetting;
private final INotificationManager mNoMan;
private final LinkedHashMap<Uri, Condition> mConditions = new LinkedHashMap<Uri, Condition>();
@@ -46,13 +49,20 @@
public ZenModeControllerImpl(Context context, Handler handler) {
mContext = context;
- mSetting = new GlobalSetting(mContext, handler, Global.ZEN_MODE) {
+ mModeSetting = new GlobalSetting(mContext, handler, Global.ZEN_MODE) {
@Override
protected void handleValueChanged(int value) {
fireZenChanged(value != 0);
}
};
- mSetting.setListening(true);
+ mConfigSetting = new GlobalSetting(mContext, handler, Global.ZEN_MODE_CONFIG_ETAG) {
+ @Override
+ protected void handleValueChanged(int value) {
+ fireExitConditionChanged();
+ }
+ };
+ mModeSetting.setListening(true);
+ mConfigSetting.setListening(true);
mNoMan = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
}
@@ -69,12 +79,12 @@
@Override
public boolean isZen() {
- return mSetting.getValue() != 0;
+ return mModeSetting.getValue() != 0;
}
@Override
public void setZen(boolean zen) {
- mSetting.setValue(zen ? 1 : 0);
+ mModeSetting.setValue(zen ? 1 : 0);
}
@Override
@@ -91,14 +101,27 @@
}
@Override
- public void select(Condition condition) {
+ public void setExitConditionId(Uri exitConditionId) {
try {
- mNoMan.setZenModeCondition(condition == null ? null : condition.id);
+ mNoMan.setZenModeCondition(exitConditionId);
} catch (RemoteException e) {
// noop
}
}
+ @Override
+ public Uri getExitConditionId() {
+ try {
+ final ZenModeConfig config = mNoMan.getZenModeConfig();
+ if (config != null) {
+ return config.exitConditionId;
+ }
+ } catch (RemoteException e) {
+ // noop
+ }
+ return null;
+ }
+
private void fireZenChanged(boolean zen) {
for (Callback cb : mCallbacks) {
cb.onZenChanged(zen);
@@ -111,6 +134,14 @@
}
}
+ private void fireExitConditionChanged() {
+ final Uri exitConditionId = getExitConditionId();
+ if (DEBUG) Slog.d(TAG, "exitConditionId changed: " + exitConditionId);
+ for (Callback cb : mCallbacks) {
+ cb.onExitConditionChanged(exitConditionId);
+ }
+ }
+
private void updateConditions(Condition[] conditions) {
if (conditions == null || conditions.length == 0) return;
for (Condition c : conditions) {
@@ -124,8 +155,8 @@
private final IConditionListener mListener = new IConditionListener.Stub() {
@Override
public void onConditionsReceived(Condition[] conditions) {
- Slog.d(TAG, "onConditionsReceived " + (conditions == null ? 0 : conditions.length)
- + " mRequesting=" + mRequesting);
+ if (DEBUG) Slog.d(TAG, "onConditionsReceived "
+ + (conditions == null ? 0 : conditions.length) + " mRequesting=" + mRequesting);
if (!mRequesting) return;
updateConditions(conditions);
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java
index 898b46e..80a14b3 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java
@@ -602,7 +602,7 @@
if (LOGD) Log.d(mTag, "expand mZenPanel=" + mZenPanel);
if (mZenPanel == null) {
mZenPanel = (ZenModePanel) mZenPanelStub.inflate();
- mZenPanel.init(mZenController);
+ mZenPanel.init(mZenController, mDialog != null ? 'D' : 'E');
mZenPanel.setCallback(new ZenModePanel.Callback() {
@Override
public void onMoreSettings() {
@@ -651,6 +651,13 @@
mExpandDivider.setVisibility(show ? View.VISIBLE : View.GONE);
mExpandButton.setImageResource(zen ? com.android.systemui.R.drawable.ic_vol_zen_on
: com.android.systemui.R.drawable.ic_vol_zen_off);
+ if (show) {
+ if (zen) {
+ expand();
+ } else {
+ collapse();
+ }
+ }
} else {
mExpandButton.setVisibility(View.GONE);
mExpandDivider.setVisibility(View.GONE);
@@ -882,10 +889,6 @@
// when the stream is for remote playback, use -1 to reset the stream type evaluation
mAudioManager.forceVolumeControlStream(stream);
- // Showing dialog - use collapsed state
- if (mZenModeCapable) {
- collapse();
- }
if (mDialog != null) {
mDialog.show();
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java
index c338563..1000352 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java
@@ -18,16 +18,21 @@
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.provider.Settings;
import android.service.notification.Condition;
+import android.service.notification.ZenModeConfig;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ImageView;
@@ -39,26 +44,47 @@
import com.android.systemui.statusbar.policy.ZenModeController;
import java.util.Arrays;
-import java.util.HashSet;
+import java.util.Objects;
public class ZenModePanel extends LinearLayout {
- private static final int[] MINUTES = new int[] { 15, 30, 45, 60, 120, 180, 240, 480 };
+ private static final boolean DEBUG = false;
+
+ private static final int[] MINUTE_BUCKETS = new int[] { 15, 30, 45, 60, 120, 180, 240, 480 };
+ private static final int MIN_BUCKET_MINUTES = MINUTE_BUCKETS[0];
+ private static final int MAX_BUCKET_MINUTES = MINUTE_BUCKETS[MINUTE_BUCKETS.length - 1];
+ private static final int DEFAULT_BUCKET_INDEX = Arrays.binarySearch(MINUTE_BUCKETS, 60);
+
+ private static final int SECONDS_MS = 1000;
+ private static final int MINUTES_MS = 60 * SECONDS_MS;
+
public static final Intent ZEN_SETTINGS = new Intent(Settings.ACTION_ZEN_MODE_SETTINGS);
private final Context mContext;
private final LayoutInflater mInflater;
- private final HashSet<RadioButton> mRadioButtons = new HashSet<RadioButton>();
private final H mHandler = new H();
+ private final Favorites mFavorites;
+
+ private char mLogTag = '?';
+ private String mTag;
private LinearLayout mConditions;
- private int mMinutesIndex = Arrays.binarySearch(MINUTES, 60); // default to one hour
private Callback mCallback;
private ZenModeController mController;
private boolean mRequestingConditions;
+ private Uri mExitConditionId;
+ private int mBucketIndex = -1;
+ private boolean mShowing;
public ZenModePanel(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
+ mFavorites = new Favorites();
mInflater = LayoutInflater.from(new ContextThemeWrapper(context, R.style.QSWhiteTheme));
+ updateTag();
+ if (DEBUG) Log.d(mTag, "new ZenModePanel");
+ }
+
+ private void updateTag() {
+ mTag = "ZenModePanel/" + mLogTag + "/" + Integer.toHexString(System.identityHashCode(this));
}
@Override
@@ -74,44 +100,97 @@
}
@Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (DEBUG) Log.d(mTag, "onAttachedToWindow");
+ final ViewGroup p = (ViewGroup) getParent();
+ updateShowing(p != null && p.getVisibility() == VISIBLE);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (DEBUG) Log.d(mTag, "onDetachedFromWindow");
+ updateShowing(false);
+ }
+
+ @Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
- setRequestingConditions(visibility == VISIBLE);
+ final boolean vis = visibility == VISIBLE;
+ updateShowing(isAttachedToWindow() && vis);
+ }
+
+ private void updateShowing(boolean showing) {
+ if (showing == mShowing) return;
+ mShowing = showing;
+ if (DEBUG) Log.d(mTag, "mShowing=" + mShowing);
+ if (mController != null) {
+ setRequestingConditions(mShowing);
+ }
}
/** Start or stop requesting relevant zen mode exit conditions */
private void setRequestingConditions(boolean requesting) {
if (mRequestingConditions == requesting) return;
+ if (DEBUG) Log.d(mTag, "setRequestingConditions " + requesting);
mRequestingConditions = requesting;
- if (mRequestingConditions) {
- mController.addCallback(mZenCallback);
- } else {
- mController.removeCallback(mZenCallback);
+ if (mController != null) {
+ mController.requestConditions(mRequestingConditions);
}
- mController.requestConditions(mRequestingConditions);
+ if (mRequestingConditions) {
+ Condition timeCondition = parseExistingTimeCondition(mExitConditionId);
+ if (timeCondition != null) {
+ mBucketIndex = -1;
+ } else {
+ mBucketIndex = DEFAULT_BUCKET_INDEX;
+ timeCondition = newTimeCondition(MINUTE_BUCKETS[mBucketIndex]);
+ }
+ if (DEBUG) Log.d(mTag, "Initial bucket index: " + mBucketIndex);
+ bind(timeCondition, mConditions.getChildAt(0));
+ handleUpdateConditions(new Condition[0]);
+ } else {
+ mConditions.removeAllViews();
+ }
}
- public void init(ZenModeController controller) {
+ public void init(ZenModeController controller, char logTag) {
mController = controller;
+ mLogTag = logTag;
+ updateTag();
+ mExitConditionId = mController.getExitConditionId();
+ if (DEBUG) Log.d(mTag, "init mExitConditionId=" + mExitConditionId);
mConditions.removeAllViews();
- bind(updateTimeCondition(), mConditions.getChildAt(0));
- handleUpdateConditions(new Condition[0]);
+ mController.addCallback(mZenCallback);
+ if (mShowing) {
+ setRequestingConditions(true);
+ }
}
public void setCallback(Callback callback) {
mCallback = callback;
}
- private Condition updateTimeCondition() {
- final int minutes = MINUTES[mMinutesIndex];
- final long millis = System.currentTimeMillis() + minutes * 60 * 1000;
- final Uri id = new Uri.Builder().scheme(Condition.SCHEME).authority("android")
- .appendPath("countdown").appendPath(Long.toString(millis)).build();
- final int num = minutes < 60 ? minutes : minutes / 60;
+ private Condition parseExistingTimeCondition(Uri conditionId) {
+ final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
+ if (time == 0) return null;
+ final long span = time - System.currentTimeMillis();
+ if (span <= 0 || span > MAX_BUCKET_MINUTES * MINUTES_MS) return null;
+ return timeCondition(time, Math.round(span / (float)MINUTES_MS));
+ }
+
+ private Condition newTimeCondition(int minutesFromNow) {
+ final long now = System.currentTimeMillis();
+ return timeCondition(now + minutesFromNow * MINUTES_MS, minutesFromNow);
+ }
+
+ private Condition timeCondition(long time, int minutes) {
+ final int num = minutes < 60 ? minutes : Math.round(minutes / 60f);
final int resId = minutes < 60
? R.plurals.zen_mode_duration_minutes
: R.plurals.zen_mode_duration_hours;
final String caption = mContext.getResources().getQuantityString(resId, num, num);
+ final Uri id = ZenModeConfig.toCountdownConditionId(time);
return new Condition(id, caption, "", "", 0, Condition.STATE_TRUE,
Condition.FLAG_RELEVANT_NOW);
}
@@ -125,14 +204,40 @@
bind(conditions[i], mConditions.getChildAt(i + 1));
}
bind(null, mConditions.getChildAt(newCount + 1));
+ checkForDefault();
}
- private void editTimeCondition(int delta) {
- final int i = mMinutesIndex + delta;
- if (i < 0 || i >= MINUTES.length) return;
- mMinutesIndex = i;
- final Condition c = updateTimeCondition();
- bind(c, mConditions.getChildAt(0));
+ private ConditionTag getConditionTagAt(int index) {
+ return (ConditionTag) mConditions.getChildAt(index).getTag();
+ }
+
+ private void checkForDefault() {
+ // are we left without anything selected? if so, set a default
+ for (int i = 0; i < mConditions.getChildCount(); i++) {
+ if (getConditionTagAt(i).rb.isChecked()) {
+ return;
+ }
+ }
+ if (DEBUG) Log.d(mTag, "Selecting a default");
+ final int favoriteIndex = mFavorites.getMinuteIndex();
+ if (favoriteIndex == -1) {
+ getConditionTagAt(mConditions.getChildCount() - 1).rb.setChecked(true);
+ } else {
+ final Condition c = newTimeCondition(MINUTE_BUCKETS[favoriteIndex]);
+ mBucketIndex = favoriteIndex;
+ bind(c, mConditions.getChildAt(0));
+ getConditionTagAt(0).rb.setChecked(true);
+ }
+ }
+
+ private void handleExitConditionChanged(Uri exitCondition) {
+ mExitConditionId = exitCondition;
+ if (DEBUG) Log.d(mTag, "handleExitConditionChanged " + mExitConditionId);
+ final int N = mConditions.getChildCount();
+ for (int i = 0; i < N; i++) {
+ final ConditionTag tag = getConditionTagAt(i);
+ tag.rb.setChecked(Objects.equals(tag.conditionId, exitCondition));
+ }
}
private void bind(final Condition condition, View convertView) {
@@ -140,23 +245,31 @@
final View row;
if (convertView == null) {
row = mInflater.inflate(R.layout.zen_mode_condition, this, false);
+ if (DEBUG) Log.d(mTag, "Adding new condition view for: " + condition);
mConditions.addView(row);
} else {
row = convertView;
}
- final int position = mConditions.indexOfChild(row);
- final RadioButton rb = (RadioButton) row.findViewById(android.R.id.checkbox);
- mRadioButtons.add(rb);
- rb.setEnabled(enabled);
- rb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+ final ConditionTag tag =
+ row.getTag() != null ? (ConditionTag) row.getTag() : new ConditionTag();
+ row.setTag(tag);
+ if (tag.rb == null) {
+ tag.rb = (RadioButton) row.findViewById(android.R.id.checkbox);
+ }
+ tag.conditionId = condition != null ? condition.id : null;
+ tag.rb.setEnabled(enabled);
+ tag.rb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- if (isChecked) {
- for (RadioButton otherButton : mRadioButtons) {
- if (otherButton == rb) continue;
- otherButton.setChecked(false);
+ if (mShowing && isChecked) {
+ if (DEBUG) Log.d(mTag, "onCheckedChanged " + tag.conditionId);
+ final int N = mConditions.getChildCount();
+ for (int i = 0; i < N; i++) {
+ ConditionTag childTag = getConditionTagAt(i);
+ if (childTag == tag) continue;
+ childTag.rb.setChecked(false);
}
- mController.select(condition);
+ select(tag.conditionId);
fireInteraction();
}
}
@@ -173,9 +286,7 @@
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- rb.setChecked(true);
- editTimeCondition(-1);
- fireInteraction();
+ onClickTimeButton(row, tag, false /*down*/);
}
});
@@ -183,29 +294,83 @@
button2.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- rb.setChecked(true);
- editTimeCondition(1);
- fireInteraction();
+ onClickTimeButton(row, tag, true /*up*/);
}
});
title.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- rb.setChecked(true);
+ tag.rb.setChecked(true);
fireInteraction();
}
});
- if (position == 0) {
- button1.setEnabled(mMinutesIndex > 0);
- button2.setEnabled(mMinutesIndex < MINUTES.length - 1);
- button1.setImageAlpha(button1.isEnabled() ? 0xff : 0x7f);
- button2.setImageAlpha(button2.isEnabled() ? 0xff : 0x7f);
+
+ final long time = ZenModeConfig.tryParseCountdownConditionId(tag.conditionId);
+ if (time > 0) {
+ if (mBucketIndex > -1) {
+ button1.setEnabled(mBucketIndex > 0);
+ button2.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1);
+ } else {
+ final long span = time - System.currentTimeMillis();
+ button1.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS);
+ final Condition maxCondition = newTimeCondition(MAX_BUCKET_MINUTES);
+ button2.setEnabled(!Objects.equals(condition.summary, maxCondition.summary));
+ }
+
+ button1.setAlpha(button1.isEnabled() ? 1f : .5f);
+ button2.setAlpha(button2.isEnabled() ? 1f : .5f);
} else {
button1.setVisibility(View.GONE);
button2.setVisibility(View.GONE);
}
- if (position == 0 && mConditions.getChildCount() == 1) {
- rb.setChecked(true);
+ if (mExitConditionId != null && mExitConditionId.equals(tag.conditionId)) {
+ if (DEBUG) Log.d(mTag, "Auto-selecting row with mExitConditionId=" + mExitConditionId);
+ tag.rb.setChecked(true);
+ }
+ }
+
+ private void onClickTimeButton(View row, ConditionTag tag, boolean up) {
+ Condition newCondition = null;
+ final int N = MINUTE_BUCKETS.length;
+ if (mBucketIndex == -1) {
+ // not on a known index, search for the next or prev bucket by time
+ final long time = ZenModeConfig.tryParseCountdownConditionId(tag.conditionId);
+ final long now = System.currentTimeMillis();
+ for (int i = 0; i < N; i++) {
+ int j = up ? i : N - 1 - i;
+ final int bucketMinutes = MINUTE_BUCKETS[j];
+ final long bucketTime = now + bucketMinutes * MINUTES_MS;
+ if (up && bucketTime > time || !up && bucketTime < time) {
+ mBucketIndex = j;
+ newCondition = timeCondition(bucketTime, bucketMinutes);
+ break;
+ }
+ }
+ if (newCondition == null) {
+ mBucketIndex = DEFAULT_BUCKET_INDEX;
+ newCondition = newTimeCondition(MINUTE_BUCKETS[mBucketIndex]);
+ }
+ } else {
+ // on a known index, simply increment or decrement
+ mBucketIndex = Math.max(0, Math.min(N - 1, mBucketIndex + (up ? 1 : -1)));
+ newCondition = newTimeCondition(MINUTE_BUCKETS[mBucketIndex]);
+ }
+ bind(newCondition, row);
+ tag.rb.setChecked(true);
+ select(newCondition.id);
+ fireInteraction();
+ }
+
+ private void select(Uri conditionId) {
+ if (DEBUG) Log.d(mTag, "select " + conditionId);
+ if (mController != null) {
+ mController.setExitConditionId(conditionId);
+ }
+ mExitConditionId = conditionId;
+ if (conditionId == null) {
+ mFavorites.setMinuteIndex(-1);
+ } else if (ZenModeConfig.isValidCountdownConditionId(conditionId) && mBucketIndex != -1) {
+ mFavorites.setMinuteIndex(mBucketIndex);
}
}
@@ -226,10 +391,16 @@
public void onConditionsChanged(Condition[] conditions) {
mHandler.obtainMessage(H.UPDATE_CONDITIONS, conditions).sendToTarget();
}
+
+ @Override
+ public void onExitConditionChanged(Uri exitConditionId) {
+ mHandler.obtainMessage(H.EXIT_CONDITION_CHANGED, exitConditionId).sendToTarget();
+ }
};
private final class H extends Handler {
private static final int UPDATE_CONDITIONS = 1;
+ private static final int EXIT_CONDITION_CHANGED = 2;
private H() {
super(Looper.getMainLooper());
@@ -239,6 +410,8 @@
public void handleMessage(Message msg) {
if (msg.what == UPDATE_CONDITIONS) {
handleUpdateConditions((Condition[])msg.obj);
+ } else if (msg.what == EXIT_CONDITION_CHANGED) {
+ handleExitConditionChanged((Uri)msg.obj);
}
}
}
@@ -247,4 +420,51 @@
void onMoreSettings();
void onInteraction();
}
+
+ // used as the view tag on condition rows
+ private static class ConditionTag {
+ RadioButton rb;
+ Uri conditionId;
+ }
+
+ private final class Favorites implements OnSharedPreferenceChangeListener {
+ private static final String KEY_MINUTE_INDEX = "minuteIndex";
+
+ private int mMinuteIndex;
+
+ private Favorites() {
+ prefs().registerOnSharedPreferenceChangeListener(this);
+ updateMinuteIndex();
+ }
+
+ public int getMinuteIndex() {
+ return mMinuteIndex;
+ }
+
+ public void setMinuteIndex(int minuteIndex) {
+ minuteIndex = clamp(minuteIndex);
+ if (minuteIndex == mMinuteIndex) return;
+ mMinuteIndex = clamp(minuteIndex);
+ if (DEBUG) Log.d(mTag, "Setting favorite minute index: " + mMinuteIndex);
+ prefs().edit().putInt(KEY_MINUTE_INDEX, mMinuteIndex).apply();
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+ updateMinuteIndex();
+ }
+
+ private SharedPreferences prefs() {
+ return mContext.getSharedPreferences(ZenModePanel.class.getSimpleName(), 0);
+ }
+
+ private void updateMinuteIndex() {
+ mMinuteIndex = clamp(prefs().getInt(KEY_MINUTE_INDEX, DEFAULT_BUCKET_INDEX));
+ if (DEBUG) Log.d(mTag, "Favorite minute index: " + mMinuteIndex);
+ }
+
+ private int clamp(int index) {
+ return Math.max(-1, Math.min(MINUTE_BUCKETS.length - 1, index));
+ }
+ }
}
diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java
index dbfb1cf..15f0ebf 100644
--- a/services/core/java/com/android/server/notification/ConditionProviders.java
+++ b/services/core/java/com/android/server/notification/ConditionProviders.java
@@ -16,18 +16,14 @@
package com.android.server.notification;
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-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.IBinder;
import android.os.IInterface;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.service.notification.Condition;
@@ -35,7 +31,6 @@
import android.service.notification.IConditionListener;
import android.service.notification.IConditionProvider;
import android.service.notification.ZenModeConfig;
-import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
@@ -45,7 +40,7 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Date;
+import java.util.Objects;
public class ConditionProviders extends ManagedServices {
private static final Condition[] NO_CONDITIONS = new Condition[0];
@@ -54,7 +49,9 @@
private final ArrayMap<IBinder, IConditionListener> mListeners
= new ArrayMap<IBinder, IConditionListener>();
private final ArrayList<ConditionRecord> mRecords = new ArrayList<ConditionRecord>();
- private final CountdownConditionHelper mCountdownHelper = new CountdownConditionHelper();
+ private final CountdownConditionProvider mCountdown = new CountdownConditionProvider();
+
+ private Uri mExitConditionId;
public ConditionProviders(Context context, Handler handler,
UserProfiles userProfiles, ZenModeHelper zenModeHelper) {
@@ -86,10 +83,13 @@
}
pw.print(" mRecords("); pw.print(mRecords.size()); pw.println("):");
for (int i = 0; i < mRecords.size(); i++) {
- pw.print(" "); pw.println(mRecords.get(i));
+ final ConditionRecord r = mRecords.get(i);
+ pw.print(" "); pw.println(r);
+ final String countdownDesc = CountdownConditionProvider.tryParseDescription(r.id);
+ if (countdownDesc != null) {
+ pw.print(" ("); pw.print(countdownDesc); pw.println(")");
+ }
}
- pw.print(" mCountdownHelper: ");
- pw.println(mCountdownHelper.getCurrentConditionDescription());
}
}
@@ -99,6 +99,14 @@
}
@Override
+ public void onBootPhaseAppsCanStart() {
+ super.onBootPhaseAppsCanStart();
+ mCountdown.attachBase(mContext);
+ registerService(mCountdown.asInterface(), CountdownConditionProvider.COMPONENT,
+ UserHandle.USER_OWNER);
+ }
+
+ @Override
protected void onServiceAdded(ManagedServiceInfo info) {
Slog.d(TAG, "onServiceAdded " + info);
final IConditionProvider provider = provider(info);
@@ -262,6 +270,15 @@
public void setZenModeCondition(Uri conditionId) {
if (DEBUG) Slog.d(TAG, "setZenModeCondition " + conditionId);
synchronized(mMutex) {
+ if (ZenModeConfig.isValidCountdownConditionId(conditionId)) {
+ // constructed by the client, make sure the record exists...
+ final ConditionRecord r = getRecordLocked(conditionId,
+ CountdownConditionProvider.COMPONENT);
+ if (r.info == null) {
+ // ... and is associated with the in-process service
+ r.info = checkServiceTokenLocked(mCountdown.asInterface());
+ }
+ }
final int N = mRecords.size();
for (int i = 0; i < N; i++) {
final ConditionRecord r = mRecords.get(i);
@@ -276,8 +293,11 @@
r.isManual = true;
}
}
+ if (!Objects.equals(mExitConditionId, conditionId)) {
+ mExitConditionId = conditionId;
+ saveZenConfigLocked();
+ }
}
- mCountdownHelper.setZenModeCondition(conditionId);
}
private void subscribeLocked(ConditionRecord r) {
@@ -395,6 +415,7 @@
return;
}
synchronized (mMutex) {
+ mExitConditionId = config.exitConditionId;
if (config.conditionComponents == null || config.conditionIds == null
|| config.conditionComponents.length != config.conditionIds.length) {
if (DEBUG) Slog.d(TAG, "loadZenConfig: no conditions");
@@ -441,82 +462,11 @@
config.conditionIds[i] = r.id;
}
}
+ config.exitConditionId = mExitConditionId;
if (DEBUG) Slog.d(TAG, "Setting zen config to: " + config);
mZenModeHelper.setConfig(config);
}
- private final class CountdownConditionHelper extends BroadcastReceiver {
- private static final String ACTION = "CountdownConditionHelper";
- private static final int REQUEST_CODE = 100;
- private static final String EXTRA_TIME = "time";
-
- private long mCurrent;
-
- public CountdownConditionHelper() {
- mContext.registerReceiver(this, new IntentFilter(ACTION));
- }
-
- public void setZenModeCondition(Uri conditionId) {
- final long time = tryParseCondition(conditionId);
- final AlarmManager alarms = (AlarmManager)
- mContext.getSystemService(Context.ALARM_SERVICE);
- final Intent intent = new Intent(ACTION).putExtra(EXTRA_TIME, time)
- .setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, REQUEST_CODE,
- intent, PendingIntent.FLAG_UPDATE_CURRENT);
- alarms.cancel(pendingIntent);
- mCurrent = time;
- if (time > 0) {
- final long now = System.currentTimeMillis();
- final CharSequence span =
- DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
- Slog.d(TAG, String.format("Scheduling %s for %s, %s in the future (%s), now=%s",
- ACTION, ts(time), time - now, span, ts(now)));
- alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
- }
- }
-
- public String getCurrentConditionDescription() {
- if (mCurrent == 0) return null;
- final long time = mCurrent;
- final long now = System.currentTimeMillis();
- final CharSequence span =
- DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
- return String.format("Scheduled for %s, %s in the future (%s), now=%s",
- ts(time), time - now, span, ts(now));
- }
-
- private String ts(long time) {
- return new Date(time) + " (" + time + ")";
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (ACTION.equals(intent.getAction())) {
- final long time = intent.getLongExtra(EXTRA_TIME, 0);
- Slog.d(TAG, "Countdown condition fired. time=" + time + " mCurrent=" + mCurrent);
- if (time > 0 && time == mCurrent) {
- // countdown condition is still the manual condition, leave zen
- mZenModeHelper.setZenMode(Global.ZEN_MODE_OFF);
- ConditionProviders.this.setZenModeCondition(null);
- }
- }
- }
-
- private long tryParseCondition(Uri conditionId) {
- // condition://android/countdown/1399917958951
- if (!Condition.isValidId(conditionId, "android")) return 0;
- if (conditionId.getPathSegments().size() != 2
- || !"countdown".equals(conditionId.getPathSegments().get(0))) return 0;
- try {
- return Long.parseLong(conditionId.getPathSegments().get(1));
- } catch (RuntimeException e) {
- Slog.w(TAG, "Error parsing countdown condition: " + conditionId, e);
- return 0;
- }
- }
- }
-
private class ZenModeHelperCallback extends ZenModeHelper.Callback {
@Override
void onConfigChanged() {
diff --git a/services/core/java/com/android/server/notification/CountdownConditionProvider.java b/services/core/java/com/android/server/notification/CountdownConditionProvider.java
new file mode 100644
index 0000000..0884f76
--- /dev/null
+++ b/services/core/java/com/android/server/notification/CountdownConditionProvider.java
@@ -0,0 +1,146 @@
+/*
+ * 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.AlarmManager;
+import android.app.PendingIntent;
+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.service.notification.Condition;
+import android.service.notification.ConditionProviderService;
+import android.service.notification.IConditionProvider;
+import android.service.notification.ZenModeConfig;
+import android.text.format.DateUtils;
+import android.util.Slog;
+
+import java.util.Date;
+
+/** Built-in zen condition provider for simple time-based conditions */
+public class CountdownConditionProvider extends ConditionProviderService {
+ private static final String TAG = "CountdownConditionProvider";
+ private static final boolean DEBUG = false;
+
+ public static final ComponentName COMPONENT =
+ new ComponentName("android", CountdownConditionProvider.class.getName());
+
+ private static final String ACTION = CountdownConditionProvider.class.getName();
+ private static final int REQUEST_CODE = 100;
+ private static final String EXTRA_CONDITION_ID = "condition_id";
+
+ private final Context mContext = this;
+ private final Receiver mReceiver = new Receiver();
+
+ private boolean mConnected;
+
+ public CountdownConditionProvider() {
+ if (DEBUG) Slog.d(TAG, "new CountdownConditionProvider()");
+ }
+
+ @Override
+ public void onConnected() {
+ if (DEBUG) Slog.d(TAG, "onConnected");
+ mContext.registerReceiver(mReceiver, new IntentFilter(ACTION));
+ mConnected = true;
+ }
+
+ @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) {
+ // by convention
+ }
+
+ @Override
+ public void onSubscribe(Uri conditionId) {
+ if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
+ final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
+ final AlarmManager alarms = (AlarmManager)
+ mContext.getSystemService(Context.ALARM_SERVICE);
+ final Intent intent = new Intent(ACTION).putExtra(EXTRA_CONDITION_ID, conditionId)
+ .setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, REQUEST_CODE,
+ intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ alarms.cancel(pendingIntent);
+ if (time > 0) {
+ final long now = System.currentTimeMillis();
+ final CharSequence span =
+ DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
+ if (DEBUG) Slog.d(TAG, String.format(
+ "Scheduling %s for %s, %s in the future (%s), now=%s",
+ ACTION, ts(time), time - now, span, ts(now)));
+ alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
+ }
+ }
+
+ @Override
+ public void onUnsubscribe(Uri conditionId) {
+ // noop
+ }
+
+ private final class Receiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (ACTION.equals(intent.getAction())) {
+ final Uri conditionId = intent.getParcelableExtra(EXTRA_CONDITION_ID);
+ final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
+ if (DEBUG) Slog.d(TAG, "Countdown condition fired: " + conditionId);
+ if (time > 0) {
+ notifyCondition(newCondition(time, Condition.STATE_FALSE));
+ }
+ }
+ }
+ }
+
+ private static final Condition newCondition(long time, int state) {
+ return new Condition(ZenModeConfig.toCountdownConditionId(time),
+ "", "", "", 0, state,Condition.FLAG_RELEVANT_NOW);
+ }
+
+ public static String tryParseDescription(Uri conditionUri) {
+ final long time = ZenModeConfig.tryParseCountdownConditionId(conditionUri);
+ if (time == 0) return null;
+ final long now = System.currentTimeMillis();
+ final CharSequence span =
+ DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
+ return String.format("Scheduled for %s, %s in the future (%s), now=%s",
+ ts(time), time - now, span, ts(now));
+ }
+
+ private static String ts(long time) {
+ return new Date(time) + " (" + time + ")";
+ }
+
+ public void attachBase(Context base) {
+ attachBaseContext(base);
+ }
+
+ public IConditionProvider asInterface() {
+ return (IConditionProvider) onBind(null);
+ }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 5f37443..495db20 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1238,7 +1238,7 @@
@Override
public ZenModeConfig getZenModeConfig() {
- checkCallerIsSystem();
+ enforceSystemOrSystemUI("INotificationManager.getZenModeConfig");
return mZenModeHelper.getConfig();
}