ZenModeConfig.java revision d60258f2d33214077a22c1a682944fa9e47c0461
1/**
2 * Copyright (c) 2014, The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.service.notification;
18
19import android.app.NotificationManager.Policy;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.res.Resources;
23import android.net.Uri;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.provider.Settings.Global;
27import android.text.TextUtils;
28import android.text.format.DateFormat;
29import android.util.ArrayMap;
30import android.util.ArraySet;
31import android.util.Slog;
32
33import com.android.internal.R;
34
35import org.xmlpull.v1.XmlPullParser;
36import org.xmlpull.v1.XmlPullParserException;
37import org.xmlpull.v1.XmlSerializer;
38
39import java.io.IOException;
40import java.util.ArrayList;
41import java.util.Calendar;
42import java.util.Locale;
43import java.util.Objects;
44import java.util.UUID;
45
46/**
47 * Persisted configuration for zen mode.
48 *
49 * @hide
50 */
51public class ZenModeConfig implements Parcelable {
52    private static String TAG = "ZenModeConfig";
53
54    public static final int SOURCE_ANYONE = 0;
55    public static final int SOURCE_CONTACT = 1;
56    public static final int SOURCE_STAR = 2;
57    public static final int MAX_SOURCE = SOURCE_STAR;
58
59    public static final int[] ALL_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
60            Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY };
61    public static final int[] WEEKNIGHT_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
62            Calendar.WEDNESDAY, Calendar.THURSDAY };
63    public static final int[] WEEKEND_DAYS = { Calendar.FRIDAY, Calendar.SATURDAY };
64
65    public static final int[] MINUTE_BUCKETS = generateMinuteBuckets();
66    private static final int SECONDS_MS = 1000;
67    private static final int MINUTES_MS = 60 * SECONDS_MS;
68    private static final int ZERO_VALUE_MS = 10 * SECONDS_MS;
69
70    private static final boolean DEFAULT_ALLOW_REMINDERS = true;
71    private static final boolean DEFAULT_ALLOW_EVENTS = true;
72    private static final boolean DEFAULT_ALLOW_REPEAT_CALLERS = false;
73
74    private static final int XML_VERSION = 2;
75    private static final String ZEN_TAG = "zen";
76    private static final String ZEN_ATT_VERSION = "version";
77    private static final String ALLOW_TAG = "allow";
78    private static final String ALLOW_ATT_CALLS = "calls";
79    private static final String ALLOW_ATT_REPEAT_CALLERS = "repeatCallers";
80    private static final String ALLOW_ATT_MESSAGES = "messages";
81    private static final String ALLOW_ATT_FROM = "from";
82    private static final String ALLOW_ATT_REMINDERS = "reminders";
83    private static final String ALLOW_ATT_EVENTS = "events";
84
85    private static final String CONDITION_TAG = "condition";
86    private static final String CONDITION_ATT_COMPONENT = "component";
87    private static final String CONDITION_ATT_ID = "id";
88    private static final String CONDITION_ATT_SUMMARY = "summary";
89    private static final String CONDITION_ATT_LINE1 = "line1";
90    private static final String CONDITION_ATT_LINE2 = "line2";
91    private static final String CONDITION_ATT_ICON = "icon";
92    private static final String CONDITION_ATT_STATE = "state";
93    private static final String CONDITION_ATT_FLAGS = "flags";
94
95    private static final String MANUAL_TAG = "manual";
96    private static final String AUTOMATIC_TAG = "automatic";
97
98    private static final String RULE_ATT_ID = "ruleId";
99    private static final String RULE_ATT_ENABLED = "enabled";
100    private static final String RULE_ATT_SNOOZING = "snoozing";
101    private static final String RULE_ATT_NAME = "name";
102    private static final String RULE_ATT_COMPONENT = "component";
103    private static final String RULE_ATT_ZEN = "zen";
104    private static final String RULE_ATT_CONDITION_ID = "conditionId";
105
106    public boolean allowCalls;
107    public boolean allowRepeatCallers = DEFAULT_ALLOW_REPEAT_CALLERS;
108    public boolean allowMessages;
109    public boolean allowReminders = DEFAULT_ALLOW_REMINDERS;
110    public boolean allowEvents = DEFAULT_ALLOW_EVENTS;
111    public int allowFrom = SOURCE_ANYONE;
112
113    public ZenRule manualRule;
114    public ArrayMap<String, ZenRule> automaticRules = new ArrayMap<>();
115
116    public ZenModeConfig() { }
117
118    public ZenModeConfig(Parcel source) {
119        allowCalls = source.readInt() == 1;
120        allowRepeatCallers = source.readInt() == 1;
121        allowMessages = source.readInt() == 1;
122        allowReminders = source.readInt() == 1;
123        allowEvents = source.readInt() == 1;
124        allowFrom = source.readInt();
125        manualRule = source.readParcelable(null);
126        final int len = source.readInt();
127        if (len > 0) {
128            final String[] ids = new String[len];
129            final ZenRule[] rules = new ZenRule[len];
130            source.readStringArray(ids);
131            source.readTypedArray(rules, ZenRule.CREATOR);
132            for (int i = 0; i < len; i++) {
133                automaticRules.put(ids[i], rules[i]);
134            }
135        }
136    }
137
138    @Override
139    public void writeToParcel(Parcel dest, int flags) {
140        dest.writeInt(allowCalls ? 1 : 0);
141        dest.writeInt(allowRepeatCallers ? 1 : 0);
142        dest.writeInt(allowMessages ? 1 : 0);
143        dest.writeInt(allowReminders ? 1 : 0);
144        dest.writeInt(allowEvents ? 1 : 0);
145        dest.writeInt(allowFrom);
146        dest.writeParcelable(manualRule, 0);
147        if (!automaticRules.isEmpty()) {
148            final int len = automaticRules.size();
149            final String[] ids = new String[len];
150            final ZenRule[] rules = new ZenRule[len];
151            for (int i = 0; i < len; i++) {
152                ids[i] = automaticRules.keyAt(i);
153                rules[i] = automaticRules.valueAt(i);
154            }
155            dest.writeInt(len);
156            dest.writeStringArray(ids);
157            dest.writeTypedArray(rules, 0);
158        } else {
159            dest.writeInt(0);
160        }
161    }
162
163    @Override
164    public String toString() {
165        return new StringBuilder(ZenModeConfig.class.getSimpleName()).append('[')
166            .append("allowCalls=").append(allowCalls)
167            .append(",allowRepeatCallers=").append(allowRepeatCallers)
168            .append(",allowMessages=").append(allowMessages)
169            .append(",allowFrom=").append(sourceToString(allowFrom))
170            .append(",allowReminders=").append(allowReminders)
171            .append(",allowEvents=").append(allowEvents)
172            .append(",automaticRules=").append(automaticRules)
173            .append(",manualRule=").append(manualRule)
174            .append(']').toString();
175    }
176
177    public boolean isValid() {
178        if (!isValidManualRule(manualRule)) return false;
179        final int N = automaticRules.size();
180        for (int i = 0; i < N; i++) {
181            if (!isValidAutomaticRule(automaticRules.valueAt(i))) return false;
182        }
183        return true;
184    }
185
186    private static boolean isValidManualRule(ZenRule rule) {
187        return rule == null || Global.isValidZenMode(rule.zenMode) && sameCondition(rule);
188    }
189
190    private static boolean isValidAutomaticRule(ZenRule rule) {
191        return rule != null && !TextUtils.isEmpty(rule.name) && Global.isValidZenMode(rule.zenMode)
192                && rule.conditionId != null && sameCondition(rule);
193    }
194
195    private static boolean sameCondition(ZenRule rule) {
196        if (rule == null) return false;
197        if (rule.conditionId == null) {
198            return rule.condition == null;
199        } else {
200            return rule.condition == null || rule.conditionId.equals(rule.condition.id);
201        }
202    }
203
204    private static int[] generateMinuteBuckets() {
205        final int maxHrs = 12;
206        final int[] buckets = new int[maxHrs + 3];
207        buckets[0] = 15;
208        buckets[1] = 30;
209        buckets[2] = 45;
210        for (int i = 1; i <= maxHrs; i++) {
211            buckets[2 + i] = 60 * i;
212        }
213        return buckets;
214    }
215
216    public static String sourceToString(int source) {
217        switch (source) {
218            case SOURCE_ANYONE:
219                return "anyone";
220            case SOURCE_CONTACT:
221                return "contacts";
222            case SOURCE_STAR:
223                return "stars";
224            default:
225                return "UNKNOWN";
226        }
227    }
228
229    @Override
230    public boolean equals(Object o) {
231        if (!(o instanceof ZenModeConfig)) return false;
232        if (o == this) return true;
233        final ZenModeConfig other = (ZenModeConfig) o;
234        return other.allowCalls == allowCalls
235                && other.allowRepeatCallers == allowRepeatCallers
236                && other.allowMessages == allowMessages
237                && other.allowFrom == allowFrom
238                && other.allowReminders == allowReminders
239                && other.allowEvents == allowEvents
240                && Objects.equals(other.automaticRules, automaticRules)
241                && Objects.equals(other.manualRule, manualRule);
242    }
243
244    @Override
245    public int hashCode() {
246        return Objects.hash(allowCalls, allowRepeatCallers, allowMessages, allowFrom,
247                allowReminders, allowEvents, automaticRules, manualRule);
248    }
249
250    private static String toDayList(int[] days) {
251        if (days == null || days.length == 0) return "";
252        final StringBuilder sb = new StringBuilder();
253        for (int i = 0; i < days.length; i++) {
254            if (i > 0) sb.append('.');
255            sb.append(days[i]);
256        }
257        return sb.toString();
258    }
259
260    private static int[] tryParseDayList(String dayList, String sep) {
261        if (dayList == null) return null;
262        final String[] tokens = dayList.split(sep);
263        if (tokens.length == 0) return null;
264        final int[] rt = new int[tokens.length];
265        for (int i = 0; i < tokens.length; i++) {
266            final int day = tryParseInt(tokens[i], -1);
267            if (day == -1) return null;
268            rt[i] = day;
269        }
270        return rt;
271    }
272
273    private static int tryParseInt(String value, int defValue) {
274        if (TextUtils.isEmpty(value)) return defValue;
275        try {
276            return Integer.valueOf(value);
277        } catch (NumberFormatException e) {
278            return defValue;
279        }
280    }
281
282    private static long tryParseLong(String value, long defValue) {
283        if (TextUtils.isEmpty(value)) return defValue;
284        try {
285            return Long.valueOf(value);
286        } catch (NumberFormatException e) {
287            return defValue;
288        }
289    }
290
291    public static ZenModeConfig readXml(XmlPullParser parser, Migration migration)
292            throws XmlPullParserException, IOException {
293        int type = parser.getEventType();
294        if (type != XmlPullParser.START_TAG) return null;
295        String tag = parser.getName();
296        if (!ZEN_TAG.equals(tag)) return null;
297        final ZenModeConfig rt = new ZenModeConfig();
298        final int version = safeInt(parser, ZEN_ATT_VERSION, XML_VERSION);
299        if (version == 1) {
300            final XmlV1 v1 = XmlV1.readXml(parser);
301            return migration.migrate(v1);
302        }
303        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
304            tag = parser.getName();
305            if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) {
306                return rt;
307            }
308            if (type == XmlPullParser.START_TAG) {
309                if (ALLOW_TAG.equals(tag)) {
310                    rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, false);
311                    rt.allowRepeatCallers = safeBoolean(parser, ALLOW_ATT_REPEAT_CALLERS,
312                            DEFAULT_ALLOW_REPEAT_CALLERS);
313                    rt.allowMessages = safeBoolean(parser, ALLOW_ATT_MESSAGES, false);
314                    rt.allowReminders = safeBoolean(parser, ALLOW_ATT_REMINDERS,
315                            DEFAULT_ALLOW_REMINDERS);
316                    rt.allowEvents = safeBoolean(parser, ALLOW_ATT_EVENTS, DEFAULT_ALLOW_EVENTS);
317                    rt.allowFrom = safeInt(parser, ALLOW_ATT_FROM, SOURCE_ANYONE);
318                    if (rt.allowFrom < SOURCE_ANYONE || rt.allowFrom > MAX_SOURCE) {
319                        throw new IndexOutOfBoundsException("bad source in config:" + rt.allowFrom);
320                    }
321                } else if (MANUAL_TAG.equals(tag)) {
322                    rt.manualRule = readRuleXml(parser, false /*conditionRequired*/);
323                } else if (AUTOMATIC_TAG.equals(tag)) {
324                    final String id = parser.getAttributeValue(null, RULE_ATT_ID);
325                    final ZenRule automaticRule = readRuleXml(parser, true /*conditionRequired*/);
326                    if (id != null && automaticRule != null) {
327                        rt.automaticRules.put(id, automaticRule);
328                    }
329                }
330            }
331        }
332        throw new IllegalStateException("Failed to reach END_DOCUMENT");
333    }
334
335    public void writeXml(XmlSerializer out) throws IOException {
336        out.startTag(null, ZEN_TAG);
337        out.attribute(null, ZEN_ATT_VERSION, Integer.toString(XML_VERSION));
338
339        out.startTag(null, ALLOW_TAG);
340        out.attribute(null, ALLOW_ATT_CALLS, Boolean.toString(allowCalls));
341        out.attribute(null, ALLOW_ATT_REPEAT_CALLERS, Boolean.toString(allowRepeatCallers));
342        out.attribute(null, ALLOW_ATT_MESSAGES, Boolean.toString(allowMessages));
343        out.attribute(null, ALLOW_ATT_REMINDERS, Boolean.toString(allowReminders));
344        out.attribute(null, ALLOW_ATT_EVENTS, Boolean.toString(allowEvents));
345        out.attribute(null, ALLOW_ATT_FROM, Integer.toString(allowFrom));
346        out.endTag(null, ALLOW_TAG);
347
348        if (manualRule != null) {
349            out.startTag(null, MANUAL_TAG);
350            writeRuleXml(manualRule, out);
351            out.endTag(null, MANUAL_TAG);
352        }
353        final int N = automaticRules.size();
354        for (int i = 0; i < N; i++) {
355            final String id = automaticRules.keyAt(i);
356            final ZenRule automaticRule = automaticRules.valueAt(i);
357            out.startTag(null, AUTOMATIC_TAG);
358            out.attribute(null, RULE_ATT_ID, id);
359            writeRuleXml(automaticRule, out);
360            out.endTag(null, AUTOMATIC_TAG);
361        }
362        out.endTag(null, ZEN_TAG);
363    }
364
365    public static ZenRule readRuleXml(XmlPullParser parser, boolean conditionRequired) {
366        final ZenRule rt = new ZenRule();
367        rt.enabled = safeBoolean(parser, RULE_ATT_ENABLED, true);
368        rt.snoozing = safeBoolean(parser, RULE_ATT_SNOOZING, false);
369        rt.name = parser.getAttributeValue(null, RULE_ATT_NAME);
370        final String zen = parser.getAttributeValue(null, RULE_ATT_ZEN);
371        rt.zenMode = tryParseZenMode(zen, -1);
372        if (rt.zenMode == -1) {
373            Slog.w(TAG, "Bad zen mode in rule xml:" + zen);
374            return null;
375        }
376        rt.conditionId = safeUri(parser, RULE_ATT_CONDITION_ID);
377        rt.component = safeComponentName(parser, RULE_ATT_COMPONENT);
378        rt.condition = readConditionXml(parser);
379        return rt;
380    }
381
382    public static void writeRuleXml(ZenRule rule, XmlSerializer out) throws IOException {
383        out.attribute(null, RULE_ATT_ENABLED, Boolean.toString(rule.enabled));
384        out.attribute(null, RULE_ATT_SNOOZING, Boolean.toString(rule.snoozing));
385        if (rule.name != null) {
386            out.attribute(null, RULE_ATT_NAME, rule.name);
387        }
388        out.attribute(null, RULE_ATT_ZEN, Integer.toString(rule.zenMode));
389        if (rule.component != null) {
390            out.attribute(null, RULE_ATT_COMPONENT, rule.component.flattenToString());
391        }
392        if (rule.conditionId != null) {
393            out.attribute(null, RULE_ATT_CONDITION_ID, rule.conditionId.toString());
394        }
395        if (rule.condition != null) {
396            writeConditionXml(rule.condition, out);
397        }
398    }
399
400    public static Condition readConditionXml(XmlPullParser parser) {
401        final Uri id = safeUri(parser, CONDITION_ATT_ID);
402        if (id == null) return null;
403        final String summary = parser.getAttributeValue(null, CONDITION_ATT_SUMMARY);
404        final String line1 = parser.getAttributeValue(null, CONDITION_ATT_LINE1);
405        final String line2 = parser.getAttributeValue(null, CONDITION_ATT_LINE2);
406        final int icon = safeInt(parser, CONDITION_ATT_ICON, -1);
407        final int state = safeInt(parser, CONDITION_ATT_STATE, -1);
408        final int flags = safeInt(parser, CONDITION_ATT_FLAGS, -1);
409        try {
410            return new Condition(id, summary, line1, line2, icon, state, flags);
411        } catch (IllegalArgumentException e) {
412            Slog.w(TAG, "Unable to read condition xml", e);
413            return null;
414        }
415    }
416
417    public static void writeConditionXml(Condition c, XmlSerializer out) throws IOException {
418        out.attribute(null, CONDITION_ATT_ID, c.id.toString());
419        out.attribute(null, CONDITION_ATT_SUMMARY, c.summary);
420        out.attribute(null, CONDITION_ATT_LINE1, c.line1);
421        out.attribute(null, CONDITION_ATT_LINE2, c.line2);
422        out.attribute(null, CONDITION_ATT_ICON, Integer.toString(c.icon));
423        out.attribute(null, CONDITION_ATT_STATE, Integer.toString(c.state));
424        out.attribute(null, CONDITION_ATT_FLAGS, Integer.toString(c.flags));
425    }
426
427    public static boolean isValidHour(int val) {
428        return val >= 0 && val < 24;
429    }
430
431    public static boolean isValidMinute(int val) {
432        return val >= 0 && val < 60;
433    }
434
435    private static boolean safeBoolean(XmlPullParser parser, String att, boolean defValue) {
436        final String val = parser.getAttributeValue(null, att);
437        if (TextUtils.isEmpty(val)) return defValue;
438        return Boolean.valueOf(val);
439    }
440
441    private static int safeInt(XmlPullParser parser, String att, int defValue) {
442        final String val = parser.getAttributeValue(null, att);
443        return tryParseInt(val, defValue);
444    }
445
446    private static ComponentName safeComponentName(XmlPullParser parser, String att) {
447        final String val = parser.getAttributeValue(null, att);
448        if (TextUtils.isEmpty(val)) return null;
449        return ComponentName.unflattenFromString(val);
450    }
451
452    private static Uri safeUri(XmlPullParser parser, String att) {
453        final String val = parser.getAttributeValue(null, att);
454        if (TextUtils.isEmpty(val)) return null;
455        return Uri.parse(val);
456    }
457
458    public ArraySet<String> getAutomaticRuleNames() {
459        final ArraySet<String> rt = new ArraySet<String>();
460        for (int i = 0; i < automaticRules.size(); i++) {
461            rt.add(automaticRules.valueAt(i).name);
462        }
463        return rt;
464    }
465
466    @Override
467    public int describeContents() {
468        return 0;
469    }
470
471    public ZenModeConfig copy() {
472        final Parcel parcel = Parcel.obtain();
473        try {
474            writeToParcel(parcel, 0);
475            parcel.setDataPosition(0);
476            return new ZenModeConfig(parcel);
477        } finally {
478            parcel.recycle();
479        }
480    }
481
482    public static final Parcelable.Creator<ZenModeConfig> CREATOR
483            = new Parcelable.Creator<ZenModeConfig>() {
484        @Override
485        public ZenModeConfig createFromParcel(Parcel source) {
486            return new ZenModeConfig(source);
487        }
488
489        @Override
490        public ZenModeConfig[] newArray(int size) {
491            return new ZenModeConfig[size];
492        }
493    };
494
495    public Policy toNotificationPolicy() {
496        int priorityCategories = 0;
497        int prioritySenders = Policy.PRIORITY_SENDERS_ANY;
498        if (allowCalls) {
499            priorityCategories |= Policy.PRIORITY_CATEGORY_CALLS;
500        }
501        if (allowMessages) {
502            priorityCategories |= Policy.PRIORITY_CATEGORY_MESSAGES;
503        }
504        if (allowEvents) {
505            priorityCategories |= Policy.PRIORITY_CATEGORY_EVENTS;
506        }
507        if (allowReminders) {
508            priorityCategories |= Policy.PRIORITY_CATEGORY_REMINDERS;
509        }
510        if (allowRepeatCallers) {
511            priorityCategories |= Policy.PRIORITY_CATEGORY_REPEAT_CALLERS;
512        }
513        switch (allowFrom) {
514            case SOURCE_ANYONE:
515                prioritySenders = Policy.PRIORITY_SENDERS_ANY;
516                break;
517            case SOURCE_CONTACT:
518                prioritySenders = Policy.PRIORITY_SENDERS_CONTACTS;
519                break;
520            case SOURCE_STAR:
521                prioritySenders = Policy.PRIORITY_SENDERS_STARRED;
522                break;
523        }
524        return new Policy(priorityCategories, prioritySenders);
525    }
526
527    public void applyNotificationPolicy(Policy policy) {
528        if (policy == null) return;
529        allowCalls = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_CALLS) != 0;
530        allowMessages = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_MESSAGES) != 0;
531        allowEvents = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_EVENTS) != 0;
532        allowReminders = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_REMINDERS) != 0;
533        allowRepeatCallers = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_REPEAT_CALLERS)
534                != 0;
535        switch (policy.prioritySenders) {
536            case Policy.PRIORITY_SENDERS_CONTACTS:
537                allowFrom = SOURCE_CONTACT;
538                break;
539            case Policy.PRIORITY_SENDERS_STARRED:
540                allowFrom = SOURCE_STAR;
541                break;
542            default:
543                allowFrom = SOURCE_ANYONE;
544                break;
545        }
546    }
547
548    public static Condition toTimeCondition(Context context, int minutesFromNow, int userHandle) {
549        final long now = System.currentTimeMillis();
550        final long millis = minutesFromNow == 0 ? ZERO_VALUE_MS : minutesFromNow * MINUTES_MS;
551        return toTimeCondition(context, now + millis, minutesFromNow, now, userHandle);
552    }
553
554    public static Condition toTimeCondition(Context context, long time, int minutes, long now,
555            int userHandle) {
556        final int num, summaryResId, line1ResId;
557        if (minutes < 60) {
558            // display as minutes
559            num = minutes;
560            summaryResId = R.plurals.zen_mode_duration_minutes_summary;
561            line1ResId = R.plurals.zen_mode_duration_minutes;
562        } else {
563            // display as hours
564            num =  Math.round(minutes / 60f);
565            summaryResId = com.android.internal.R.plurals.zen_mode_duration_hours_summary;
566            line1ResId = com.android.internal.R.plurals.zen_mode_duration_hours;
567        }
568        final String skeleton = DateFormat.is24HourFormat(context, userHandle) ? "Hm" : "hma";
569        final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
570        final CharSequence formattedTime = DateFormat.format(pattern, time);
571        final Resources res = context.getResources();
572        final String summary = res.getQuantityString(summaryResId, num, num, formattedTime);
573        final String line1 = res.getQuantityString(line1ResId, num, num, formattedTime);
574        final String line2 = res.getString(R.string.zen_mode_until, formattedTime);
575        final Uri id = toCountdownConditionId(time);
576        return new Condition(id, summary, line1, line2, 0, Condition.STATE_TRUE,
577                Condition.FLAG_RELEVANT_NOW);
578    }
579
580    // ==== Built-in system conditions ====
581
582    public static final String SYSTEM_AUTHORITY = "android";
583
584    // ==== Built-in system condition: countdown ====
585
586    public static final String COUNTDOWN_PATH = "countdown";
587
588    public static Uri toCountdownConditionId(long time) {
589        return new Uri.Builder().scheme(Condition.SCHEME)
590                .authority(SYSTEM_AUTHORITY)
591                .appendPath(COUNTDOWN_PATH)
592                .appendPath(Long.toString(time))
593                .build();
594    }
595
596    public static long tryParseCountdownConditionId(Uri conditionId) {
597        if (!Condition.isValidId(conditionId, SYSTEM_AUTHORITY)) return 0;
598        if (conditionId.getPathSegments().size() != 2
599                || !COUNTDOWN_PATH.equals(conditionId.getPathSegments().get(0))) return 0;
600        try {
601            return Long.parseLong(conditionId.getPathSegments().get(1));
602        } catch (RuntimeException e) {
603            Slog.w(TAG, "Error parsing countdown condition: " + conditionId, e);
604            return 0;
605        }
606    }
607
608    public static boolean isValidCountdownConditionId(Uri conditionId) {
609        return tryParseCountdownConditionId(conditionId) != 0;
610    }
611
612    // ==== Built-in system condition: schedule ====
613
614    public static final String SCHEDULE_PATH = "schedule";
615
616    public static Uri toScheduleConditionId(ScheduleInfo schedule) {
617        return new Uri.Builder().scheme(Condition.SCHEME)
618                .authority(SYSTEM_AUTHORITY)
619                .appendPath(SCHEDULE_PATH)
620                .appendQueryParameter("days", toDayList(schedule.days))
621                .appendQueryParameter("start", schedule.startHour + "." + schedule.startMinute)
622                .appendQueryParameter("end", schedule.endHour + "." + schedule.endMinute)
623                .build();
624    }
625
626    public static boolean isValidScheduleConditionId(Uri conditionId) {
627        return tryParseScheduleConditionId(conditionId) != null;
628    }
629
630    public static ScheduleInfo tryParseScheduleConditionId(Uri conditionId) {
631        final boolean isSchedule =  conditionId != null
632                && conditionId.getScheme().equals(Condition.SCHEME)
633                && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY)
634                && conditionId.getPathSegments().size() == 1
635                && conditionId.getPathSegments().get(0).equals(ZenModeConfig.SCHEDULE_PATH);
636        if (!isSchedule) return null;
637        final int[] start = tryParseHourAndMinute(conditionId.getQueryParameter("start"));
638        final int[] end = tryParseHourAndMinute(conditionId.getQueryParameter("end"));
639        if (start == null || end == null) return null;
640        final ScheduleInfo rt = new ScheduleInfo();
641        rt.days = tryParseDayList(conditionId.getQueryParameter("days"), "\\.");
642        rt.startHour = start[0];
643        rt.startMinute = start[1];
644        rt.endHour = end[0];
645        rt.endMinute = end[1];
646        return rt;
647    }
648
649    public static class ScheduleInfo {
650        public int[] days;
651        public int startHour;
652        public int startMinute;
653        public int endHour;
654        public int endMinute;
655
656        @Override
657        public int hashCode() {
658            return 0;
659        }
660
661        @Override
662        public boolean equals(Object o) {
663            if (!(o instanceof ScheduleInfo)) return false;
664            final ScheduleInfo other = (ScheduleInfo) o;
665            return toDayList(days).equals(toDayList(other.days))
666                    && startHour == other.startHour
667                    && startMinute == other.startMinute
668                    && endHour == other.endHour
669                    && endMinute == other.endMinute;
670        }
671
672        public ScheduleInfo copy() {
673            final ScheduleInfo rt = new ScheduleInfo();
674            if (days != null) {
675                rt.days = new int[days.length];
676                System.arraycopy(days, 0, rt.days, 0, days.length);
677            }
678            rt.startHour = startHour;
679            rt.startMinute = startMinute;
680            rt.endHour = endHour;
681            rt.endMinute = endMinute;
682            return rt;
683        }
684    }
685
686    // ==== Built-in system condition: event ====
687
688    public static final String EVENT_PATH = "event";
689
690    public static Uri toEventConditionId(EventInfo event) {
691        return new Uri.Builder().scheme(Condition.SCHEME)
692                .authority(SYSTEM_AUTHORITY)
693                .appendPath(EVENT_PATH)
694                .appendQueryParameter("calendar", Long.toString(event.calendar))
695                .appendQueryParameter("attendance", Integer.toString(event.attendance))
696                .appendQueryParameter("reply", Integer.toString(event.reply))
697                .build();
698    }
699
700    public static boolean isValidEventConditionId(Uri conditionId) {
701        return tryParseEventConditionId(conditionId) != null;
702    }
703
704    public static EventInfo tryParseEventConditionId(Uri conditionId) {
705        final boolean isEvent = conditionId != null
706                && conditionId.getScheme().equals(Condition.SCHEME)
707                && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY)
708                && conditionId.getPathSegments().size() == 1
709                && conditionId.getPathSegments().get(0).equals(EVENT_PATH);
710        if (!isEvent) return null;
711        final EventInfo rt = new EventInfo();
712        rt.calendar = tryParseLong(conditionId.getQueryParameter("calendar"), 0L);
713        rt.attendance = tryParseInt(conditionId.getQueryParameter("attendance"), 0);
714        rt.reply = tryParseInt(conditionId.getQueryParameter("reply"), 0);
715        return rt;
716    }
717
718    public static class EventInfo {
719        public static final int ATTENDANCE_REQUIRED_OR_OPTIONAL = 0;
720        public static final int ATTENDANCE_REQUIRED = 1;
721        public static final int ATTENDANCE_OPTIONAL = 2;
722
723        public static final int REPLY_ANY = 0;
724        public static final int REPLY_ANY_EXCEPT_NO = 1;
725        public static final int REPLY_YES = 2;
726
727        public long calendar;  // CalendarContract.Calendars._ID, or 0 for any
728        public int attendance;
729        public int reply;
730
731        @Override
732        public int hashCode() {
733            return 0;
734        }
735
736        @Override
737        public boolean equals(Object o) {
738            if (!(o instanceof EventInfo)) return false;
739            final EventInfo other = (EventInfo) o;
740            return calendar == other.calendar
741                    && attendance == other.attendance
742                    && reply == other.reply;
743        }
744
745        public EventInfo copy() {
746            final EventInfo rt = new EventInfo();
747            rt.calendar = calendar;
748            rt.attendance = attendance;
749            rt.reply = reply;
750            return rt;
751        }
752    }
753
754    // ==== End built-in system conditions ====
755
756    private static int[] tryParseHourAndMinute(String value) {
757        if (TextUtils.isEmpty(value)) return null;
758        final int i = value.indexOf('.');
759        if (i < 1 || i >= value.length() - 1) return null;
760        final int hour = tryParseInt(value.substring(0, i), -1);
761        final int minute = tryParseInt(value.substring(i + 1), -1);
762        return isValidHour(hour) && isValidMinute(minute) ? new int[] { hour, minute } : null;
763    }
764
765    private static int tryParseZenMode(String value, int defValue) {
766        final int rt = tryParseInt(value, defValue);
767        return Global.isValidZenMode(rt) ? rt : defValue;
768    }
769
770    public String newRuleId() {
771        return UUID.randomUUID().toString().replace("-", "");
772    }
773
774    public static String getConditionLine1(Context context, ZenModeConfig config,
775            int userHandle) {
776        return getConditionLine(context, config, userHandle, true /*useLine1*/);
777    }
778
779    public static String getConditionSummary(Context context, ZenModeConfig config,
780            int userHandle) {
781        return getConditionLine(context, config, userHandle, false /*useLine1*/);
782    }
783
784    private static String getConditionLine(Context context, ZenModeConfig config,
785            int userHandle, boolean useLine1) {
786        if (config == null) return "";
787        if (config.manualRule != null) {
788            final Uri id = config.manualRule.conditionId;
789            if (id == null) {
790                return context.getString(com.android.internal.R.string.zen_mode_forever);
791            }
792            final long time = tryParseCountdownConditionId(id);
793            Condition c = config.manualRule.condition;
794            if (time > 0) {
795                final long now = System.currentTimeMillis();
796                final long span = time - now;
797                c = toTimeCondition(context,
798                        time, Math.round(span / (float) MINUTES_MS), now, userHandle);
799            }
800            final String rt = c == null ? "" : useLine1 ? c.line1 : c.summary;
801            return TextUtils.isEmpty(rt) ? "" : rt;
802        }
803        String summary = "";
804        for (ZenRule automaticRule : config.automaticRules.values()) {
805            if (automaticRule.isAutomaticActive()) {
806                if (summary.isEmpty()) {
807                    summary = automaticRule.name;
808                } else {
809                    summary = context.getResources()
810                            .getString(R.string.zen_mode_rule_name_combination, summary,
811                                    automaticRule.name);
812                }
813            }
814        }
815        return summary;
816    }
817
818    public static class ZenRule implements Parcelable {
819        public boolean enabled;
820        public boolean snoozing;         // user manually disabled this instance
821        public String name;              // required for automatic (unique)
822        public int zenMode;
823        public Uri conditionId;          // required for automatic
824        public Condition condition;      // optional
825        public ComponentName component;  // optional
826
827        public ZenRule() { }
828
829        public ZenRule(Parcel source) {
830            enabled = source.readInt() == 1;
831            snoozing = source.readInt() == 1;
832            if (source.readInt() == 1) {
833                name = source.readString();
834            }
835            zenMode = source.readInt();
836            conditionId = source.readParcelable(null);
837            condition = source.readParcelable(null);
838            component = source.readParcelable(null);
839        }
840
841        @Override
842        public int describeContents() {
843            return 0;
844        }
845
846        @Override
847        public void writeToParcel(Parcel dest, int flags) {
848            dest.writeInt(enabled ? 1 : 0);
849            dest.writeInt(snoozing ? 1 : 0);
850            if (name != null) {
851                dest.writeInt(1);
852                dest.writeString(name);
853            } else {
854                dest.writeInt(0);
855            }
856            dest.writeInt(zenMode);
857            dest.writeParcelable(conditionId, 0);
858            dest.writeParcelable(condition, 0);
859            dest.writeParcelable(component, 0);
860        }
861
862        @Override
863        public String toString() {
864            return new StringBuilder(ZenRule.class.getSimpleName()).append('[')
865                    .append("enabled=").append(enabled)
866                    .append(",snoozing=").append(snoozing)
867                    .append(",name=").append(name)
868                    .append(",zenMode=").append(Global.zenModeToString(zenMode))
869                    .append(",conditionId=").append(conditionId)
870                    .append(",condition=").append(condition)
871                    .append(",component=").append(component)
872                    .append(']').toString();
873        }
874
875        @Override
876        public boolean equals(Object o) {
877            if (!(o instanceof ZenRule)) return false;
878            if (o == this) return true;
879            final ZenRule other = (ZenRule) o;
880            return other.enabled == enabled
881                    && other.snoozing == snoozing
882                    && Objects.equals(other.name, name)
883                    && other.zenMode == zenMode
884                    && Objects.equals(other.conditionId, conditionId)
885                    && Objects.equals(other.condition, condition)
886                    && Objects.equals(other.component, component);
887        }
888
889        @Override
890        public int hashCode() {
891            return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
892                    component);
893        }
894
895        public boolean isAutomaticActive() {
896            return enabled && !snoozing && component != null && isTrueOrUnknown();
897        }
898
899        public boolean isTrueOrUnknown() {
900            return condition != null && (condition.state == Condition.STATE_TRUE
901                    || condition.state == Condition.STATE_UNKNOWN);
902        }
903
904        public static final Parcelable.Creator<ZenRule> CREATOR
905                = new Parcelable.Creator<ZenRule>() {
906            @Override
907            public ZenRule createFromParcel(Parcel source) {
908                return new ZenRule(source);
909            }
910            @Override
911            public ZenRule[] newArray(int size) {
912                return new ZenRule[size];
913            }
914        };
915    }
916
917    // Legacy config
918    public static final class XmlV1 {
919        public static final String SLEEP_MODE_NIGHTS = "nights";
920        public static final String SLEEP_MODE_WEEKNIGHTS = "weeknights";
921        public static final String SLEEP_MODE_DAYS_PREFIX = "days:";
922
923        private static final String EXIT_CONDITION_TAG = "exitCondition";
924        private static final String EXIT_CONDITION_ATT_COMPONENT = "component";
925        private static final String SLEEP_TAG = "sleep";
926        private static final String SLEEP_ATT_MODE = "mode";
927        private static final String SLEEP_ATT_NONE = "none";
928
929        private static final String SLEEP_ATT_START_HR = "startHour";
930        private static final String SLEEP_ATT_START_MIN = "startMin";
931        private static final String SLEEP_ATT_END_HR = "endHour";
932        private static final String SLEEP_ATT_END_MIN = "endMin";
933
934        public boolean allowCalls;
935        public boolean allowMessages;
936        public boolean allowReminders = DEFAULT_ALLOW_REMINDERS;
937        public boolean allowEvents = DEFAULT_ALLOW_EVENTS;
938        public int allowFrom = SOURCE_ANYONE;
939
940        public String sleepMode;     // nights, weeknights, days:1,2,3  Calendar.days
941        public int sleepStartHour;   // 0-23
942        public int sleepStartMinute; // 0-59
943        public int sleepEndHour;
944        public int sleepEndMinute;
945        public boolean sleepNone;    // false = priority, true = none
946        public ComponentName[] conditionComponents;
947        public Uri[] conditionIds;
948        public Condition exitCondition;  // manual exit condition
949        public ComponentName exitConditionComponent;  // manual exit condition component
950
951        private static boolean isValidSleepMode(String sleepMode) {
952            return sleepMode == null || sleepMode.equals(SLEEP_MODE_NIGHTS)
953                    || sleepMode.equals(SLEEP_MODE_WEEKNIGHTS) || tryParseDays(sleepMode) != null;
954        }
955
956        public static int[] tryParseDays(String sleepMode) {
957            if (sleepMode == null) return null;
958            sleepMode = sleepMode.trim();
959            if (SLEEP_MODE_NIGHTS.equals(sleepMode)) return ALL_DAYS;
960            if (SLEEP_MODE_WEEKNIGHTS.equals(sleepMode)) return WEEKNIGHT_DAYS;
961            if (!sleepMode.startsWith(SLEEP_MODE_DAYS_PREFIX)) return null;
962            if (sleepMode.equals(SLEEP_MODE_DAYS_PREFIX)) return null;
963            return tryParseDayList(sleepMode.substring(SLEEP_MODE_DAYS_PREFIX.length()), ",");
964        }
965
966        public static XmlV1 readXml(XmlPullParser parser)
967                throws XmlPullParserException, IOException {
968            int type;
969            String tag;
970            XmlV1 rt = new XmlV1();
971            final ArrayList<ComponentName> conditionComponents = new ArrayList<ComponentName>();
972            final ArrayList<Uri> conditionIds = new ArrayList<Uri>();
973            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
974                tag = parser.getName();
975                if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) {
976                    if (!conditionComponents.isEmpty()) {
977                        rt.conditionComponents = conditionComponents
978                                .toArray(new ComponentName[conditionComponents.size()]);
979                        rt.conditionIds = conditionIds.toArray(new Uri[conditionIds.size()]);
980                    }
981                    return rt;
982                }
983                if (type == XmlPullParser.START_TAG) {
984                    if (ALLOW_TAG.equals(tag)) {
985                        rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, false);
986                        rt.allowMessages = safeBoolean(parser, ALLOW_ATT_MESSAGES, false);
987                        rt.allowReminders = safeBoolean(parser, ALLOW_ATT_REMINDERS,
988                                DEFAULT_ALLOW_REMINDERS);
989                        rt.allowEvents = safeBoolean(parser, ALLOW_ATT_EVENTS,
990                                DEFAULT_ALLOW_EVENTS);
991                        rt.allowFrom = safeInt(parser, ALLOW_ATT_FROM, SOURCE_ANYONE);
992                        if (rt.allowFrom < SOURCE_ANYONE || rt.allowFrom > MAX_SOURCE) {
993                            throw new IndexOutOfBoundsException("bad source in config:"
994                                    + rt.allowFrom);
995                        }
996                    } else if (SLEEP_TAG.equals(tag)) {
997                        final String mode = parser.getAttributeValue(null, SLEEP_ATT_MODE);
998                        rt.sleepMode = isValidSleepMode(mode)? mode : null;
999                        rt.sleepNone = safeBoolean(parser, SLEEP_ATT_NONE, false);
1000                        final int startHour = safeInt(parser, SLEEP_ATT_START_HR, 0);
1001                        final int startMinute = safeInt(parser, SLEEP_ATT_START_MIN, 0);
1002                        final int endHour = safeInt(parser, SLEEP_ATT_END_HR, 0);
1003                        final int endMinute = safeInt(parser, SLEEP_ATT_END_MIN, 0);
1004                        rt.sleepStartHour = isValidHour(startHour) ? startHour : 0;
1005                        rt.sleepStartMinute = isValidMinute(startMinute) ? startMinute : 0;
1006                        rt.sleepEndHour = isValidHour(endHour) ? endHour : 0;
1007                        rt.sleepEndMinute = isValidMinute(endMinute) ? endMinute : 0;
1008                    } else if (CONDITION_TAG.equals(tag)) {
1009                        final ComponentName component =
1010                                safeComponentName(parser, CONDITION_ATT_COMPONENT);
1011                        final Uri conditionId = safeUri(parser, CONDITION_ATT_ID);
1012                        if (component != null && conditionId != null) {
1013                            conditionComponents.add(component);
1014                            conditionIds.add(conditionId);
1015                        }
1016                    } else if (EXIT_CONDITION_TAG.equals(tag)) {
1017                        rt.exitCondition = readConditionXml(parser);
1018                        if (rt.exitCondition != null) {
1019                            rt.exitConditionComponent =
1020                                    safeComponentName(parser, EXIT_CONDITION_ATT_COMPONENT);
1021                        }
1022                    }
1023                }
1024            }
1025            throw new IllegalStateException("Failed to reach END_DOCUMENT");
1026        }
1027    }
1028
1029    public interface Migration {
1030        ZenModeConfig migrate(XmlV1 v1);
1031    }
1032
1033}
1034