ZenModeConfig.java revision 7f73308f50791740beb380fecc17903301054ad3
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.ActivityManager;
20import android.app.NotificationManager.Policy;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.res.Resources;
24import android.net.Uri;
25import android.os.Parcel;
26import android.os.Parcelable;
27import android.os.UserHandle;
28import android.provider.Settings.Global;
29import android.text.TextUtils;
30import android.text.format.DateFormat;
31import android.util.ArrayMap;
32import android.util.ArraySet;
33import android.util.Slog;
34
35import com.android.internal.R;
36
37import org.xmlpull.v1.XmlPullParser;
38import org.xmlpull.v1.XmlPullParserException;
39import org.xmlpull.v1.XmlSerializer;
40
41import java.io.IOException;
42import java.util.ArrayList;
43import java.util.Arrays;
44import java.util.Calendar;
45import java.util.GregorianCalendar;
46import java.util.Locale;
47import java.util.Objects;
48import java.util.UUID;
49
50/**
51 * Persisted configuration for zen mode.
52 *
53 * @hide
54 */
55public class ZenModeConfig implements Parcelable {
56    private static String TAG = "ZenModeConfig";
57
58    public static final int SOURCE_ANYONE = 0;
59    public static final int SOURCE_CONTACT = 1;
60    public static final int SOURCE_STAR = 2;
61    public static final int MAX_SOURCE = SOURCE_STAR;
62    private static final int DEFAULT_SOURCE = SOURCE_CONTACT;
63
64    public static final int[] ALL_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
65            Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY };
66    public static final int[] WEEKNIGHT_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
67            Calendar.WEDNESDAY, Calendar.THURSDAY };
68    public static final int[] WEEKEND_DAYS = { Calendar.FRIDAY, Calendar.SATURDAY };
69
70    public static final int[] MINUTE_BUCKETS = generateMinuteBuckets();
71    private static final int SECONDS_MS = 1000;
72    private static final int MINUTES_MS = 60 * SECONDS_MS;
73    private static final int DAY_MINUTES = 24 * 60;
74    private static final int ZERO_VALUE_MS = 10 * SECONDS_MS;
75
76    private static final boolean DEFAULT_ALLOW_CALLS = true;
77    private static final boolean DEFAULT_ALLOW_MESSAGES = false;
78    private static final boolean DEFAULT_ALLOW_REMINDERS = true;
79    private static final boolean DEFAULT_ALLOW_EVENTS = true;
80    private static final boolean DEFAULT_ALLOW_REPEAT_CALLERS = false;
81    private static final boolean DEFAULT_ALLOW_SCREEN_OFF = true;
82    private static final boolean DEFAULT_ALLOW_SCREEN_ON = true;
83
84    private static final int XML_VERSION = 2;
85    private static final String ZEN_TAG = "zen";
86    private static final String ZEN_ATT_VERSION = "version";
87    private static final String ZEN_ATT_USER = "user";
88    private static final String ALLOW_TAG = "allow";
89    private static final String ALLOW_ATT_CALLS = "calls";
90    private static final String ALLOW_ATT_REPEAT_CALLERS = "repeatCallers";
91    private static final String ALLOW_ATT_MESSAGES = "messages";
92    private static final String ALLOW_ATT_FROM = "from";
93    private static final String ALLOW_ATT_CALLS_FROM = "callsFrom";
94    private static final String ALLOW_ATT_MESSAGES_FROM = "messagesFrom";
95    private static final String ALLOW_ATT_REMINDERS = "reminders";
96    private static final String ALLOW_ATT_EVENTS = "events";
97    private static final String ALLOW_ATT_SCREEN_OFF = "visualScreenOff";
98    private static final String ALLOW_ATT_SCREEN_ON = "visualScreenOn";
99
100    private static final String CONDITION_TAG = "condition";
101    private static final String CONDITION_ATT_COMPONENT = "component";
102    private static final String CONDITION_ATT_ID = "id";
103    private static final String CONDITION_ATT_SUMMARY = "summary";
104    private static final String CONDITION_ATT_LINE1 = "line1";
105    private static final String CONDITION_ATT_LINE2 = "line2";
106    private static final String CONDITION_ATT_ICON = "icon";
107    private static final String CONDITION_ATT_STATE = "state";
108    private static final String CONDITION_ATT_FLAGS = "flags";
109
110    private static final String MANUAL_TAG = "manual";
111    private static final String AUTOMATIC_TAG = "automatic";
112
113    private static final String RULE_ATT_ID = "ruleId";
114    private static final String RULE_ATT_ENABLED = "enabled";
115    private static final String RULE_ATT_SNOOZING = "snoozing";
116    private static final String RULE_ATT_NAME = "name";
117    private static final String RULE_ATT_COMPONENT = "component";
118    private static final String RULE_ATT_ZEN = "zen";
119    private static final String RULE_ATT_CONDITION_ID = "conditionId";
120    private static final String RULE_ATT_CREATION_TIME = "creationTime";
121
122    public boolean allowCalls = DEFAULT_ALLOW_CALLS;
123    public boolean allowRepeatCallers = DEFAULT_ALLOW_REPEAT_CALLERS;
124    public boolean allowMessages = DEFAULT_ALLOW_MESSAGES;
125    public boolean allowReminders = DEFAULT_ALLOW_REMINDERS;
126    public boolean allowEvents = DEFAULT_ALLOW_EVENTS;
127    public int allowCallsFrom = DEFAULT_SOURCE;
128    public int allowMessagesFrom = DEFAULT_SOURCE;
129    public int user = UserHandle.USER_SYSTEM;
130    public boolean allowWhenScreenOff = DEFAULT_ALLOW_SCREEN_OFF;
131    public boolean allowWhenScreenOn = DEFAULT_ALLOW_SCREEN_ON;
132
133    public ZenRule manualRule;
134    public ArrayMap<String, ZenRule> automaticRules = new ArrayMap<>();
135
136    public ZenModeConfig() { }
137
138    public ZenModeConfig(Parcel source) {
139        allowCalls = source.readInt() == 1;
140        allowRepeatCallers = source.readInt() == 1;
141        allowMessages = source.readInt() == 1;
142        allowReminders = source.readInt() == 1;
143        allowEvents = source.readInt() == 1;
144        allowCallsFrom = source.readInt();
145        allowMessagesFrom = source.readInt();
146        user = source.readInt();
147        manualRule = source.readParcelable(null);
148        final int len = source.readInt();
149        if (len > 0) {
150            final String[] ids = new String[len];
151            final ZenRule[] rules = new ZenRule[len];
152            source.readStringArray(ids);
153            source.readTypedArray(rules, ZenRule.CREATOR);
154            for (int i = 0; i < len; i++) {
155                automaticRules.put(ids[i], rules[i]);
156            }
157        }
158        allowWhenScreenOff = source.readInt() == 1;
159        allowWhenScreenOn = source.readInt() == 1;
160    }
161
162    @Override
163    public void writeToParcel(Parcel dest, int flags) {
164        dest.writeInt(allowCalls ? 1 : 0);
165        dest.writeInt(allowRepeatCallers ? 1 : 0);
166        dest.writeInt(allowMessages ? 1 : 0);
167        dest.writeInt(allowReminders ? 1 : 0);
168        dest.writeInt(allowEvents ? 1 : 0);
169        dest.writeInt(allowCallsFrom);
170        dest.writeInt(allowMessagesFrom);
171        dest.writeInt(user);
172        dest.writeParcelable(manualRule, 0);
173        if (!automaticRules.isEmpty()) {
174            final int len = automaticRules.size();
175            final String[] ids = new String[len];
176            final ZenRule[] rules = new ZenRule[len];
177            for (int i = 0; i < len; i++) {
178                ids[i] = automaticRules.keyAt(i);
179                rules[i] = automaticRules.valueAt(i);
180            }
181            dest.writeInt(len);
182            dest.writeStringArray(ids);
183            dest.writeTypedArray(rules, 0);
184        } else {
185            dest.writeInt(0);
186        }
187        dest.writeInt(allowWhenScreenOff ? 1 : 0);
188        dest.writeInt(allowWhenScreenOn ? 1 : 0);
189    }
190
191    @Override
192    public String toString() {
193        return new StringBuilder(ZenModeConfig.class.getSimpleName()).append('[')
194                .append("user=").append(user)
195                .append(",allowCalls=").append(allowCalls)
196                .append(",allowRepeatCallers=").append(allowRepeatCallers)
197                .append(",allowMessages=").append(allowMessages)
198                .append(",allowCallsFrom=").append(sourceToString(allowCallsFrom))
199                .append(",allowMessagesFrom=").append(sourceToString(allowMessagesFrom))
200                .append(",allowReminders=").append(allowReminders)
201                .append(",allowEvents=").append(allowEvents)
202                .append(",allowWhenScreenOff=").append(allowWhenScreenOff)
203                .append(",allowWhenScreenOn=").append(allowWhenScreenOn)
204                .append(",automaticRules=").append(automaticRules)
205                .append(",manualRule=").append(manualRule)
206                .append(']').toString();
207    }
208
209    private Diff diff(ZenModeConfig to) {
210        final Diff d = new Diff();
211        if (to == null) {
212            return d.addLine("config", "delete");
213        }
214        if (user != to.user) {
215            d.addLine("user", user, to.user);
216        }
217        if (allowCalls != to.allowCalls) {
218            d.addLine("allowCalls", allowCalls, to.allowCalls);
219        }
220        if (allowRepeatCallers != to.allowRepeatCallers) {
221            d.addLine("allowRepeatCallers", allowRepeatCallers, to.allowRepeatCallers);
222        }
223        if (allowMessages != to.allowMessages) {
224            d.addLine("allowMessages", allowMessages, to.allowMessages);
225        }
226        if (allowCallsFrom != to.allowCallsFrom) {
227            d.addLine("allowCallsFrom", allowCallsFrom, to.allowCallsFrom);
228        }
229        if (allowMessagesFrom != to.allowMessagesFrom) {
230            d.addLine("allowMessagesFrom", allowMessagesFrom, to.allowMessagesFrom);
231        }
232        if (allowReminders != to.allowReminders) {
233            d.addLine("allowReminders", allowReminders, to.allowReminders);
234        }
235        if (allowEvents != to.allowEvents) {
236            d.addLine("allowEvents", allowEvents, to.allowEvents);
237        }
238        if (allowWhenScreenOff != to.allowWhenScreenOff) {
239            d.addLine("allowWhenScreenOff", allowWhenScreenOff, to.allowWhenScreenOff);
240        }
241        if (allowWhenScreenOn != to.allowWhenScreenOn) {
242            d.addLine("allowWhenScreenOn", allowWhenScreenOn, to.allowWhenScreenOn);
243        }
244        final ArraySet<String> allRules = new ArraySet<>();
245        addKeys(allRules, automaticRules);
246        addKeys(allRules, to.automaticRules);
247        final int N = allRules.size();
248        for (int i = 0; i < N; i++) {
249            final String rule = allRules.valueAt(i);
250            final ZenRule fromRule = automaticRules != null ? automaticRules.get(rule) : null;
251            final ZenRule toRule = to.automaticRules != null ? to.automaticRules.get(rule) : null;
252            ZenRule.appendDiff(d, "automaticRule[" + rule + "]", fromRule, toRule);
253        }
254        ZenRule.appendDiff(d, "manualRule", manualRule, to.manualRule);
255        return d;
256    }
257
258    public static Diff diff(ZenModeConfig from, ZenModeConfig to) {
259        if (from == null) {
260            final Diff d = new Diff();
261            if (to != null) {
262                d.addLine("config", "insert");
263            }
264            return d;
265        }
266        return from.diff(to);
267    }
268
269    private static <T> void addKeys(ArraySet<T> set, ArrayMap<T, ?> map) {
270        if (map != null) {
271            for (int i = 0; i < map.size(); i++) {
272                set.add(map.keyAt(i));
273            }
274        }
275    }
276
277    public boolean isValid() {
278        if (!isValidManualRule(manualRule)) return false;
279        final int N = automaticRules.size();
280        for (int i = 0; i < N; i++) {
281            if (!isValidAutomaticRule(automaticRules.valueAt(i))) return false;
282        }
283        return true;
284    }
285
286    private static boolean isValidManualRule(ZenRule rule) {
287        return rule == null || Global.isValidZenMode(rule.zenMode) && sameCondition(rule);
288    }
289
290    private static boolean isValidAutomaticRule(ZenRule rule) {
291        return rule != null && !TextUtils.isEmpty(rule.name) && Global.isValidZenMode(rule.zenMode)
292                && rule.conditionId != null && sameCondition(rule);
293    }
294
295    private static boolean sameCondition(ZenRule rule) {
296        if (rule == null) return false;
297        if (rule.conditionId == null) {
298            return rule.condition == null;
299        } else {
300            return rule.condition == null || rule.conditionId.equals(rule.condition.id);
301        }
302    }
303
304    private static int[] generateMinuteBuckets() {
305        final int maxHrs = 12;
306        final int[] buckets = new int[maxHrs + 3];
307        buckets[0] = 15;
308        buckets[1] = 30;
309        buckets[2] = 45;
310        for (int i = 1; i <= maxHrs; i++) {
311            buckets[2 + i] = 60 * i;
312        }
313        return buckets;
314    }
315
316    public static String sourceToString(int source) {
317        switch (source) {
318            case SOURCE_ANYONE:
319                return "anyone";
320            case SOURCE_CONTACT:
321                return "contacts";
322            case SOURCE_STAR:
323                return "stars";
324            default:
325                return "UNKNOWN";
326        }
327    }
328
329    @Override
330    public boolean equals(Object o) {
331        if (!(o instanceof ZenModeConfig)) return false;
332        if (o == this) return true;
333        final ZenModeConfig other = (ZenModeConfig) o;
334        return other.allowCalls == allowCalls
335                && other.allowRepeatCallers == allowRepeatCallers
336                && other.allowMessages == allowMessages
337                && other.allowCallsFrom == allowCallsFrom
338                && other.allowMessagesFrom == allowMessagesFrom
339                && other.allowReminders == allowReminders
340                && other.allowEvents == allowEvents
341                && other.allowWhenScreenOff == allowWhenScreenOff
342                && other.allowWhenScreenOn == allowWhenScreenOn
343                && other.user == user
344                && Objects.equals(other.automaticRules, automaticRules)
345                && Objects.equals(other.manualRule, manualRule);
346    }
347
348    @Override
349    public int hashCode() {
350        return Objects.hash(allowCalls, allowRepeatCallers, allowMessages, allowCallsFrom,
351                allowMessagesFrom, allowReminders, allowEvents, allowWhenScreenOff,
352                allowWhenScreenOn,
353                user, automaticRules, manualRule);
354    }
355
356    private static String toDayList(int[] days) {
357        if (days == null || days.length == 0) return "";
358        final StringBuilder sb = new StringBuilder();
359        for (int i = 0; i < days.length; i++) {
360            if (i > 0) sb.append('.');
361            sb.append(days[i]);
362        }
363        return sb.toString();
364    }
365
366    private static int[] tryParseDayList(String dayList, String sep) {
367        if (dayList == null) return null;
368        final String[] tokens = dayList.split(sep);
369        if (tokens.length == 0) return null;
370        final int[] rt = new int[tokens.length];
371        for (int i = 0; i < tokens.length; i++) {
372            final int day = tryParseInt(tokens[i], -1);
373            if (day == -1) return null;
374            rt[i] = day;
375        }
376        return rt;
377    }
378
379    private static int tryParseInt(String value, int defValue) {
380        if (TextUtils.isEmpty(value)) return defValue;
381        try {
382            return Integer.parseInt(value);
383        } catch (NumberFormatException e) {
384            return defValue;
385        }
386    }
387
388    private static long tryParseLong(String value, long defValue) {
389        if (TextUtils.isEmpty(value)) return defValue;
390        try {
391            return Long.valueOf(value);
392        } catch (NumberFormatException e) {
393            return defValue;
394        }
395    }
396
397    public static ZenModeConfig readXml(XmlPullParser parser, Migration migration)
398            throws XmlPullParserException, IOException {
399        int type = parser.getEventType();
400        if (type != XmlPullParser.START_TAG) return null;
401        String tag = parser.getName();
402        if (!ZEN_TAG.equals(tag)) return null;
403        final ZenModeConfig rt = new ZenModeConfig();
404        final int version = safeInt(parser, ZEN_ATT_VERSION, XML_VERSION);
405        if (version == 1) {
406            final XmlV1 v1 = XmlV1.readXml(parser);
407            return migration.migrate(v1);
408        }
409        rt.user = safeInt(parser, ZEN_ATT_USER, rt.user);
410        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
411            tag = parser.getName();
412            if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) {
413                return rt;
414            }
415            if (type == XmlPullParser.START_TAG) {
416                if (ALLOW_TAG.equals(tag)) {
417                    rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, false);
418                    rt.allowRepeatCallers = safeBoolean(parser, ALLOW_ATT_REPEAT_CALLERS,
419                            DEFAULT_ALLOW_REPEAT_CALLERS);
420                    rt.allowMessages = safeBoolean(parser, ALLOW_ATT_MESSAGES, false);
421                    rt.allowReminders = safeBoolean(parser, ALLOW_ATT_REMINDERS,
422                            DEFAULT_ALLOW_REMINDERS);
423                    rt.allowEvents = safeBoolean(parser, ALLOW_ATT_EVENTS, DEFAULT_ALLOW_EVENTS);
424                    final int from = safeInt(parser, ALLOW_ATT_FROM, -1);
425                    final int callsFrom = safeInt(parser, ALLOW_ATT_CALLS_FROM, -1);
426                    final int messagesFrom = safeInt(parser, ALLOW_ATT_MESSAGES_FROM, -1);
427                    if (isValidSource(callsFrom) && isValidSource(messagesFrom)) {
428                        rt.allowCallsFrom = callsFrom;
429                        rt.allowMessagesFrom = messagesFrom;
430                    } else if (isValidSource(from)) {
431                        Slog.i(TAG, "Migrating existing shared 'from': " + sourceToString(from));
432                        rt.allowCallsFrom = from;
433                        rt.allowMessagesFrom = from;
434                    } else {
435                        rt.allowCallsFrom = DEFAULT_SOURCE;
436                        rt.allowMessagesFrom = DEFAULT_SOURCE;
437                    }
438                    rt.allowWhenScreenOff =
439                            safeBoolean(parser, ALLOW_ATT_SCREEN_OFF, DEFAULT_ALLOW_SCREEN_OFF);
440                    rt.allowWhenScreenOn =
441                            safeBoolean(parser, ALLOW_ATT_SCREEN_ON, DEFAULT_ALLOW_SCREEN_ON);
442                } else if (MANUAL_TAG.equals(tag)) {
443                    rt.manualRule = readRuleXml(parser);
444                } else if (AUTOMATIC_TAG.equals(tag)) {
445                    final String id = parser.getAttributeValue(null, RULE_ATT_ID);
446                    final ZenRule automaticRule = readRuleXml(parser);
447                    if (id != null && automaticRule != null) {
448                        automaticRule.id = id;
449                        rt.automaticRules.put(id, automaticRule);
450                    }
451                }
452            }
453        }
454        throw new IllegalStateException("Failed to reach END_DOCUMENT");
455    }
456
457    public void writeXml(XmlSerializer out) throws IOException {
458        out.startTag(null, ZEN_TAG);
459        out.attribute(null, ZEN_ATT_VERSION, Integer.toString(XML_VERSION));
460        out.attribute(null, ZEN_ATT_USER, Integer.toString(user));
461
462        out.startTag(null, ALLOW_TAG);
463        out.attribute(null, ALLOW_ATT_CALLS, Boolean.toString(allowCalls));
464        out.attribute(null, ALLOW_ATT_REPEAT_CALLERS, Boolean.toString(allowRepeatCallers));
465        out.attribute(null, ALLOW_ATT_MESSAGES, Boolean.toString(allowMessages));
466        out.attribute(null, ALLOW_ATT_REMINDERS, Boolean.toString(allowReminders));
467        out.attribute(null, ALLOW_ATT_EVENTS, Boolean.toString(allowEvents));
468        out.attribute(null, ALLOW_ATT_CALLS_FROM, Integer.toString(allowCallsFrom));
469        out.attribute(null, ALLOW_ATT_MESSAGES_FROM, Integer.toString(allowMessagesFrom));
470        out.attribute(null, ALLOW_ATT_SCREEN_OFF, Boolean.toString(allowWhenScreenOff));
471        out.attribute(null, ALLOW_ATT_SCREEN_ON, Boolean.toString(allowWhenScreenOn));
472        out.endTag(null, ALLOW_TAG);
473
474        if (manualRule != null) {
475            out.startTag(null, MANUAL_TAG);
476            writeRuleXml(manualRule, out);
477            out.endTag(null, MANUAL_TAG);
478        }
479        final int N = automaticRules.size();
480        for (int i = 0; i < N; i++) {
481            final String id = automaticRules.keyAt(i);
482            final ZenRule automaticRule = automaticRules.valueAt(i);
483            out.startTag(null, AUTOMATIC_TAG);
484            out.attribute(null, RULE_ATT_ID, id);
485            writeRuleXml(automaticRule, out);
486            out.endTag(null, AUTOMATIC_TAG);
487        }
488        out.endTag(null, ZEN_TAG);
489    }
490
491    public static ZenRule readRuleXml(XmlPullParser parser) {
492        final ZenRule rt = new ZenRule();
493        rt.enabled = safeBoolean(parser, RULE_ATT_ENABLED, true);
494        rt.snoozing = safeBoolean(parser, RULE_ATT_SNOOZING, false);
495        rt.name = parser.getAttributeValue(null, RULE_ATT_NAME);
496        final String zen = parser.getAttributeValue(null, RULE_ATT_ZEN);
497        rt.zenMode = tryParseZenMode(zen, -1);
498        if (rt.zenMode == -1) {
499            Slog.w(TAG, "Bad zen mode in rule xml:" + zen);
500            return null;
501        }
502        rt.conditionId = safeUri(parser, RULE_ATT_CONDITION_ID);
503        rt.component = safeComponentName(parser, RULE_ATT_COMPONENT);
504        rt.creationTime = safeLong(parser, RULE_ATT_CREATION_TIME, 0);
505        rt.condition = readConditionXml(parser);
506        return rt;
507    }
508
509    public static void writeRuleXml(ZenRule rule, XmlSerializer out) throws IOException {
510        out.attribute(null, RULE_ATT_ENABLED, Boolean.toString(rule.enabled));
511        out.attribute(null, RULE_ATT_SNOOZING, Boolean.toString(rule.snoozing));
512        if (rule.name != null) {
513            out.attribute(null, RULE_ATT_NAME, rule.name);
514        }
515        out.attribute(null, RULE_ATT_ZEN, Integer.toString(rule.zenMode));
516        if (rule.component != null) {
517            out.attribute(null, RULE_ATT_COMPONENT, rule.component.flattenToString());
518        }
519        if (rule.conditionId != null) {
520            out.attribute(null, RULE_ATT_CONDITION_ID, rule.conditionId.toString());
521        }
522        out.attribute(null, RULE_ATT_CREATION_TIME, Long.toString(rule.creationTime));
523        if (rule.condition != null) {
524            writeConditionXml(rule.condition, out);
525        }
526    }
527
528    public static Condition readConditionXml(XmlPullParser parser) {
529        final Uri id = safeUri(parser, CONDITION_ATT_ID);
530        if (id == null) return null;
531        final String summary = parser.getAttributeValue(null, CONDITION_ATT_SUMMARY);
532        final String line1 = parser.getAttributeValue(null, CONDITION_ATT_LINE1);
533        final String line2 = parser.getAttributeValue(null, CONDITION_ATT_LINE2);
534        final int icon = safeInt(parser, CONDITION_ATT_ICON, -1);
535        final int state = safeInt(parser, CONDITION_ATT_STATE, -1);
536        final int flags = safeInt(parser, CONDITION_ATT_FLAGS, -1);
537        try {
538            return new Condition(id, summary, line1, line2, icon, state, flags);
539        } catch (IllegalArgumentException e) {
540            Slog.w(TAG, "Unable to read condition xml", e);
541            return null;
542        }
543    }
544
545    public static void writeConditionXml(Condition c, XmlSerializer out) throws IOException {
546        out.attribute(null, CONDITION_ATT_ID, c.id.toString());
547        out.attribute(null, CONDITION_ATT_SUMMARY, c.summary);
548        out.attribute(null, CONDITION_ATT_LINE1, c.line1);
549        out.attribute(null, CONDITION_ATT_LINE2, c.line2);
550        out.attribute(null, CONDITION_ATT_ICON, Integer.toString(c.icon));
551        out.attribute(null, CONDITION_ATT_STATE, Integer.toString(c.state));
552        out.attribute(null, CONDITION_ATT_FLAGS, Integer.toString(c.flags));
553    }
554
555    public static boolean isValidHour(int val) {
556        return val >= 0 && val < 24;
557    }
558
559    public static boolean isValidMinute(int val) {
560        return val >= 0 && val < 60;
561    }
562
563    private static boolean isValidSource(int source) {
564        return source >= SOURCE_ANYONE && source <= MAX_SOURCE;
565    }
566
567    private static boolean safeBoolean(XmlPullParser parser, String att, boolean defValue) {
568        final String val = parser.getAttributeValue(null, att);
569        return safeBoolean(val, defValue);
570    }
571
572    private static boolean safeBoolean(String val, boolean defValue) {
573        if (TextUtils.isEmpty(val)) return defValue;
574        return Boolean.valueOf(val);
575    }
576
577    private static int safeInt(XmlPullParser parser, String att, int defValue) {
578        final String val = parser.getAttributeValue(null, att);
579        return tryParseInt(val, defValue);
580    }
581
582    private static ComponentName safeComponentName(XmlPullParser parser, String att) {
583        final String val = parser.getAttributeValue(null, att);
584        if (TextUtils.isEmpty(val)) return null;
585        return ComponentName.unflattenFromString(val);
586    }
587
588    private static Uri safeUri(XmlPullParser parser, String att) {
589        final String val = parser.getAttributeValue(null, att);
590        if (TextUtils.isEmpty(val)) return null;
591        return Uri.parse(val);
592    }
593
594    private static long safeLong(XmlPullParser parser, String att, long defValue) {
595        final String val = parser.getAttributeValue(null, att);
596        return tryParseLong(val, defValue);
597    }
598
599    @Override
600    public int describeContents() {
601        return 0;
602    }
603
604    public ZenModeConfig copy() {
605        final Parcel parcel = Parcel.obtain();
606        try {
607            writeToParcel(parcel, 0);
608            parcel.setDataPosition(0);
609            return new ZenModeConfig(parcel);
610        } finally {
611            parcel.recycle();
612        }
613    }
614
615    public static final Parcelable.Creator<ZenModeConfig> CREATOR
616            = new Parcelable.Creator<ZenModeConfig>() {
617        @Override
618        public ZenModeConfig createFromParcel(Parcel source) {
619            return new ZenModeConfig(source);
620        }
621
622        @Override
623        public ZenModeConfig[] newArray(int size) {
624            return new ZenModeConfig[size];
625        }
626    };
627
628    public Policy toNotificationPolicy() {
629        int priorityCategories = 0;
630        int priorityCallSenders = Policy.PRIORITY_SENDERS_CONTACTS;
631        int priorityMessageSenders = Policy.PRIORITY_SENDERS_CONTACTS;
632        if (allowCalls) {
633            priorityCategories |= Policy.PRIORITY_CATEGORY_CALLS;
634        }
635        if (allowMessages) {
636            priorityCategories |= Policy.PRIORITY_CATEGORY_MESSAGES;
637        }
638        if (allowEvents) {
639            priorityCategories |= Policy.PRIORITY_CATEGORY_EVENTS;
640        }
641        if (allowReminders) {
642            priorityCategories |= Policy.PRIORITY_CATEGORY_REMINDERS;
643        }
644        if (allowRepeatCallers) {
645            priorityCategories |= Policy.PRIORITY_CATEGORY_REPEAT_CALLERS;
646        }
647        int suppressedVisualEffects = 0;
648        if (!allowWhenScreenOff) {
649            suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
650        }
651        if (!allowWhenScreenOn) {
652            suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_SCREEN_ON;
653        }
654        priorityCallSenders = sourceToPrioritySenders(allowCallsFrom, priorityCallSenders);
655        priorityMessageSenders = sourceToPrioritySenders(allowMessagesFrom, priorityMessageSenders);
656        return new Policy(priorityCategories, priorityCallSenders, priorityMessageSenders,
657                suppressedVisualEffects);
658    }
659
660    private static int sourceToPrioritySenders(int source, int def) {
661        switch (source) {
662            case SOURCE_ANYONE: return Policy.PRIORITY_SENDERS_ANY;
663            case SOURCE_CONTACT: return Policy.PRIORITY_SENDERS_CONTACTS;
664            case SOURCE_STAR: return Policy.PRIORITY_SENDERS_STARRED;
665            default: return def;
666        }
667    }
668
669    private static int prioritySendersToSource(int prioritySenders, int def) {
670        switch (prioritySenders) {
671            case Policy.PRIORITY_SENDERS_CONTACTS: return SOURCE_CONTACT;
672            case Policy.PRIORITY_SENDERS_STARRED: return SOURCE_STAR;
673            case Policy.PRIORITY_SENDERS_ANY: return SOURCE_ANYONE;
674            default: return def;
675        }
676    }
677
678    public void applyNotificationPolicy(Policy policy) {
679        if (policy == null) return;
680        allowCalls = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_CALLS) != 0;
681        allowMessages = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_MESSAGES) != 0;
682        allowEvents = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_EVENTS) != 0;
683        allowReminders = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_REMINDERS) != 0;
684        allowRepeatCallers = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_REPEAT_CALLERS)
685                != 0;
686        allowCallsFrom = prioritySendersToSource(policy.priorityCallSenders, allowCallsFrom);
687        allowMessagesFrom = prioritySendersToSource(policy.priorityMessageSenders,
688                allowMessagesFrom);
689        if (policy.suppressedVisualEffects != Policy.SUPPRESSED_EFFECTS_UNSET) {
690            allowWhenScreenOff =
691                    (policy.suppressedVisualEffects & Policy.SUPPRESSED_EFFECT_SCREEN_OFF) == 0;
692            allowWhenScreenOn =
693                    (policy.suppressedVisualEffects & Policy.SUPPRESSED_EFFECT_SCREEN_ON) == 0;
694        }
695    }
696
697    public static Condition toTimeCondition(Context context, int minutesFromNow, int userHandle) {
698        return toTimeCondition(context, minutesFromNow, userHandle, false /*shortVersion*/);
699    }
700
701    public static Condition toTimeCondition(Context context, int minutesFromNow, int userHandle,
702            boolean shortVersion) {
703        final long now = System.currentTimeMillis();
704        final long millis = minutesFromNow == 0 ? ZERO_VALUE_MS : minutesFromNow * MINUTES_MS;
705        return toTimeCondition(context, now + millis, minutesFromNow, userHandle, shortVersion);
706    }
707
708    public static Condition toTimeCondition(Context context, long time, int minutes,
709            int userHandle, boolean shortVersion) {
710        final int num;
711        String summary, line1, line2;
712        final CharSequence formattedTime = getFormattedTime(context, time, userHandle);
713        final Resources res = context.getResources();
714        if (minutes < 60) {
715            // display as minutes
716            num = minutes;
717            int summaryResId = shortVersion ? R.plurals.zen_mode_duration_minutes_summary_short
718                    : R.plurals.zen_mode_duration_minutes_summary;
719            summary = res.getQuantityString(summaryResId, num, num, formattedTime);
720            int line1ResId = shortVersion ? R.plurals.zen_mode_duration_minutes_short
721                    : R.plurals.zen_mode_duration_minutes;
722            line1 = res.getQuantityString(line1ResId, num, num, formattedTime);
723            line2 = res.getString(R.string.zen_mode_until, formattedTime);
724        } else if (minutes < DAY_MINUTES) {
725            // display as hours
726            num =  Math.round(minutes / 60f);
727            int summaryResId = shortVersion ? R.plurals.zen_mode_duration_hours_summary_short
728                    : R.plurals.zen_mode_duration_hours_summary;
729            summary = res.getQuantityString(summaryResId, num, num, formattedTime);
730            int line1ResId = shortVersion ? R.plurals.zen_mode_duration_hours_short
731                    : R.plurals.zen_mode_duration_hours;
732            line1 = res.getQuantityString(line1ResId, num, num, formattedTime);
733            line2 = res.getString(R.string.zen_mode_until, formattedTime);
734        } else {
735            // display as day/time
736            summary = line1 = line2 = res.getString(R.string.zen_mode_until, formattedTime);
737        }
738        final Uri id = toCountdownConditionId(time);
739        return new Condition(id, summary, line1, line2, 0, Condition.STATE_TRUE,
740                Condition.FLAG_RELEVANT_NOW);
741    }
742
743    public static Condition toNextAlarmCondition(Context context, long now, long alarm,
744            int userHandle) {
745        final CharSequence formattedTime = getFormattedTime(context, alarm, userHandle);
746        final Resources res = context.getResources();
747        final String line1 = res.getString(R.string.zen_mode_alarm, formattedTime);
748        final Uri id = toCountdownConditionId(alarm);
749        return new Condition(id, "", line1, "", 0, Condition.STATE_TRUE,
750                Condition.FLAG_RELEVANT_NOW);
751    }
752
753    private static CharSequence getFormattedTime(Context context, long time, int userHandle) {
754        String skeleton = "EEE " + (DateFormat.is24HourFormat(context, userHandle) ? "Hm" : "hma");
755        GregorianCalendar now = new GregorianCalendar();
756        GregorianCalendar endTime = new GregorianCalendar();
757        endTime.setTimeInMillis(time);
758        if (now.get(Calendar.YEAR) == endTime.get(Calendar.YEAR)
759                && now.get(Calendar.MONTH) == endTime.get(Calendar.MONTH)
760                && now.get(Calendar.DATE) == endTime.get(Calendar.DATE)) {
761            skeleton = DateFormat.is24HourFormat(context, userHandle) ? "Hm" : "hma";
762        }
763        final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
764        return DateFormat.format(pattern, time);
765    }
766
767    // ==== Built-in system conditions ====
768
769    public static final String SYSTEM_AUTHORITY = "android";
770
771    // ==== Built-in system condition: countdown ====
772
773    public static final String COUNTDOWN_PATH = "countdown";
774
775    public static Uri toCountdownConditionId(long time) {
776        return new Uri.Builder().scheme(Condition.SCHEME)
777                .authority(SYSTEM_AUTHORITY)
778                .appendPath(COUNTDOWN_PATH)
779                .appendPath(Long.toString(time))
780                .build();
781    }
782
783    public static long tryParseCountdownConditionId(Uri conditionId) {
784        if (!Condition.isValidId(conditionId, SYSTEM_AUTHORITY)) return 0;
785        if (conditionId.getPathSegments().size() != 2
786                || !COUNTDOWN_PATH.equals(conditionId.getPathSegments().get(0))) return 0;
787        try {
788            return Long.parseLong(conditionId.getPathSegments().get(1));
789        } catch (RuntimeException e) {
790            Slog.w(TAG, "Error parsing countdown condition: " + conditionId, e);
791            return 0;
792        }
793    }
794
795    public static boolean isValidCountdownConditionId(Uri conditionId) {
796        return tryParseCountdownConditionId(conditionId) != 0;
797    }
798
799    // ==== Built-in system condition: schedule ====
800
801    public static final String SCHEDULE_PATH = "schedule";
802
803    public static Uri toScheduleConditionId(ScheduleInfo schedule) {
804        return new Uri.Builder().scheme(Condition.SCHEME)
805                .authority(SYSTEM_AUTHORITY)
806                .appendPath(SCHEDULE_PATH)
807                .appendQueryParameter("days", toDayList(schedule.days))
808                .appendQueryParameter("start", schedule.startHour + "." + schedule.startMinute)
809                .appendQueryParameter("end", schedule.endHour + "." + schedule.endMinute)
810                .appendQueryParameter("exitAtAlarm", String.valueOf(schedule.exitAtAlarm))
811                .build();
812    }
813
814    public static boolean isValidScheduleConditionId(Uri conditionId) {
815        return tryParseScheduleConditionId(conditionId) != null;
816    }
817
818    public static ScheduleInfo tryParseScheduleConditionId(Uri conditionId) {
819        final boolean isSchedule =  conditionId != null
820                && conditionId.getScheme().equals(Condition.SCHEME)
821                && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY)
822                && conditionId.getPathSegments().size() == 1
823                && conditionId.getPathSegments().get(0).equals(ZenModeConfig.SCHEDULE_PATH);
824        if (!isSchedule) return null;
825        final int[] start = tryParseHourAndMinute(conditionId.getQueryParameter("start"));
826        final int[] end = tryParseHourAndMinute(conditionId.getQueryParameter("end"));
827        if (start == null || end == null) return null;
828        final ScheduleInfo rt = new ScheduleInfo();
829        rt.days = tryParseDayList(conditionId.getQueryParameter("days"), "\\.");
830        rt.startHour = start[0];
831        rt.startMinute = start[1];
832        rt.endHour = end[0];
833        rt.endMinute = end[1];
834        rt.exitAtAlarm = safeBoolean(conditionId.getQueryParameter("exitAtAlarm"), false);
835        return rt;
836    }
837
838    public static ComponentName getScheduleConditionProvider() {
839        return new ComponentName(SYSTEM_AUTHORITY, "ScheduleConditionProvider");
840    }
841
842    public static class ScheduleInfo {
843        public int[] days;
844        public int startHour;
845        public int startMinute;
846        public int endHour;
847        public int endMinute;
848        public boolean exitAtAlarm;
849        public long nextAlarm;
850
851        @Override
852        public int hashCode() {
853            return 0;
854        }
855
856        @Override
857        public boolean equals(Object o) {
858            if (!(o instanceof ScheduleInfo)) return false;
859            final ScheduleInfo other = (ScheduleInfo) o;
860            return toDayList(days).equals(toDayList(other.days))
861                    && startHour == other.startHour
862                    && startMinute == other.startMinute
863                    && endHour == other.endHour
864                    && endMinute == other.endMinute
865                    && exitAtAlarm == other.exitAtAlarm;
866        }
867
868        public ScheduleInfo copy() {
869            final ScheduleInfo rt = new ScheduleInfo();
870            if (days != null) {
871                rt.days = new int[days.length];
872                System.arraycopy(days, 0, rt.days, 0, days.length);
873            }
874            rt.startHour = startHour;
875            rt.startMinute = startMinute;
876            rt.endHour = endHour;
877            rt.endMinute = endMinute;
878            rt.exitAtAlarm = exitAtAlarm;
879            rt.nextAlarm = nextAlarm;
880            return rt;
881        }
882
883        @Override
884        public String toString() {
885            return "ScheduleInfo{" +
886                    "days=" + Arrays.toString(days) +
887                    ", startHour=" + startHour +
888                    ", startMinute=" + startMinute +
889                    ", endHour=" + endHour +
890                    ", endMinute=" + endMinute +
891                    ", exitAtAlarm=" + exitAtAlarm +
892                    ", nextAlarm=" + nextAlarm +
893                    '}';
894        }
895    }
896
897    // ==== Built-in system condition: event ====
898
899    public static final String EVENT_PATH = "event";
900
901    public static Uri toEventConditionId(EventInfo event) {
902        return new Uri.Builder().scheme(Condition.SCHEME)
903                .authority(SYSTEM_AUTHORITY)
904                .appendPath(EVENT_PATH)
905                .appendQueryParameter("userId", Long.toString(event.userId))
906                .appendQueryParameter("calendar", event.calendar != null ? event.calendar : "")
907                .appendQueryParameter("reply", Integer.toString(event.reply))
908                .build();
909    }
910
911    public static boolean isValidEventConditionId(Uri conditionId) {
912        return tryParseEventConditionId(conditionId) != null;
913    }
914
915    public static EventInfo tryParseEventConditionId(Uri conditionId) {
916        final boolean isEvent = conditionId != null
917                && conditionId.getScheme().equals(Condition.SCHEME)
918                && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY)
919                && conditionId.getPathSegments().size() == 1
920                && conditionId.getPathSegments().get(0).equals(EVENT_PATH);
921        if (!isEvent) return null;
922        final EventInfo rt = new EventInfo();
923        rt.userId = tryParseInt(conditionId.getQueryParameter("userId"), UserHandle.USER_NULL);
924        rt.calendar = conditionId.getQueryParameter("calendar");
925        if (TextUtils.isEmpty(rt.calendar) || tryParseLong(rt.calendar, -1L) != -1L) {
926            rt.calendar = null;
927        }
928        rt.reply = tryParseInt(conditionId.getQueryParameter("reply"), 0);
929        return rt;
930    }
931
932    public static ComponentName getEventConditionProvider() {
933        return new ComponentName(SYSTEM_AUTHORITY, "EventConditionProvider");
934    }
935
936    public static class EventInfo {
937        public static final int REPLY_ANY_EXCEPT_NO = 0;
938        public static final int REPLY_YES_OR_MAYBE = 1;
939        public static final int REPLY_YES = 2;
940
941        public int userId = UserHandle.USER_NULL;  // USER_NULL = unspecified - use current user
942        public String calendar;  // CalendarContract.Calendars.OWNER_ACCOUNT, or null for any
943        public int reply;
944
945        @Override
946        public int hashCode() {
947            return 0;
948        }
949
950        @Override
951        public boolean equals(Object o) {
952            if (!(o instanceof EventInfo)) return false;
953            final EventInfo other = (EventInfo) o;
954            return userId == other.userId
955                    && Objects.equals(calendar, other.calendar)
956                    && reply == other.reply;
957        }
958
959        public EventInfo copy() {
960            final EventInfo rt = new EventInfo();
961            rt.userId = userId;
962            rt.calendar = calendar;
963            rt.reply = reply;
964            return rt;
965        }
966
967        public static int resolveUserId(int userId) {
968            return userId == UserHandle.USER_NULL ? ActivityManager.getCurrentUser() : userId;
969        }
970    }
971
972    // ==== End built-in system conditions ====
973
974    private static int[] tryParseHourAndMinute(String value) {
975        if (TextUtils.isEmpty(value)) return null;
976        final int i = value.indexOf('.');
977        if (i < 1 || i >= value.length() - 1) return null;
978        final int hour = tryParseInt(value.substring(0, i), -1);
979        final int minute = tryParseInt(value.substring(i + 1), -1);
980        return isValidHour(hour) && isValidMinute(minute) ? new int[] { hour, minute } : null;
981    }
982
983    private static int tryParseZenMode(String value, int defValue) {
984        final int rt = tryParseInt(value, defValue);
985        return Global.isValidZenMode(rt) ? rt : defValue;
986    }
987
988    public static String newRuleId() {
989        return UUID.randomUUID().toString().replace("-", "");
990    }
991
992    public static String getConditionSummary(Context context, ZenModeConfig config,
993            int userHandle, boolean shortVersion) {
994        return getConditionLine(context, config, userHandle, false /*useLine1*/, shortVersion);
995    }
996
997    private static String getConditionLine(Context context, ZenModeConfig config,
998            int userHandle, boolean useLine1, boolean shortVersion) {
999        if (config == null) return "";
1000        if (config.manualRule != null) {
1001            final Uri id = config.manualRule.conditionId;
1002            if (id == null) {
1003                return context.getString(com.android.internal.R.string.zen_mode_forever);
1004            }
1005            final long time = tryParseCountdownConditionId(id);
1006            Condition c = config.manualRule.condition;
1007            if (time > 0) {
1008                final long now = System.currentTimeMillis();
1009                final long span = time - now;
1010                c = toTimeCondition(context, time, Math.round(span / (float) MINUTES_MS),
1011                        userHandle, shortVersion);
1012            }
1013            final String rt = c == null ? "" : useLine1 ? c.line1 : c.summary;
1014            return TextUtils.isEmpty(rt) ? "" : rt;
1015        }
1016        String summary = "";
1017        for (ZenRule automaticRule : config.automaticRules.values()) {
1018            if (automaticRule.isAutomaticActive()) {
1019                if (summary.isEmpty()) {
1020                    summary = automaticRule.name;
1021                } else {
1022                    summary = context.getResources()
1023                            .getString(R.string.zen_mode_rule_name_combination, summary,
1024                                    automaticRule.name);
1025                }
1026            }
1027        }
1028        return summary;
1029    }
1030
1031    public static class ZenRule implements Parcelable {
1032        public boolean enabled;
1033        public boolean snoozing;         // user manually disabled this instance
1034        public String name;              // required for automatic
1035        public int zenMode;
1036        public Uri conditionId;          // required for automatic
1037        public Condition condition;      // optional
1038        public ComponentName component;  // optional
1039        public String id;                // required for automatic (unique)
1040        public long creationTime;        // required for automatic
1041
1042        public ZenRule() { }
1043
1044        public ZenRule(Parcel source) {
1045            enabled = source.readInt() == 1;
1046            snoozing = source.readInt() == 1;
1047            if (source.readInt() == 1) {
1048                name = source.readString();
1049            }
1050            zenMode = source.readInt();
1051            conditionId = source.readParcelable(null);
1052            condition = source.readParcelable(null);
1053            component = source.readParcelable(null);
1054            if (source.readInt() == 1) {
1055                id = source.readString();
1056            }
1057            creationTime = source.readLong();
1058        }
1059
1060        @Override
1061        public int describeContents() {
1062            return 0;
1063        }
1064
1065        @Override
1066        public void writeToParcel(Parcel dest, int flags) {
1067            dest.writeInt(enabled ? 1 : 0);
1068            dest.writeInt(snoozing ? 1 : 0);
1069            if (name != null) {
1070                dest.writeInt(1);
1071                dest.writeString(name);
1072            } else {
1073                dest.writeInt(0);
1074            }
1075            dest.writeInt(zenMode);
1076            dest.writeParcelable(conditionId, 0);
1077            dest.writeParcelable(condition, 0);
1078            dest.writeParcelable(component, 0);
1079            if (id != null) {
1080                dest.writeInt(1);
1081                dest.writeString(id);
1082            } else {
1083                dest.writeInt(0);
1084            }
1085            dest.writeLong(creationTime);
1086        }
1087
1088        @Override
1089        public String toString() {
1090            return new StringBuilder(ZenRule.class.getSimpleName()).append('[')
1091                    .append("enabled=").append(enabled)
1092                    .append(",snoozing=").append(snoozing)
1093                    .append(",name=").append(name)
1094                    .append(",zenMode=").append(Global.zenModeToString(zenMode))
1095                    .append(",conditionId=").append(conditionId)
1096                    .append(",condition=").append(condition)
1097                    .append(",component=").append(component)
1098                    .append(",id=").append(id)
1099                    .append(",creationTime=").append(creationTime)
1100                    .append(']').toString();
1101        }
1102
1103        private static void appendDiff(Diff d, String item, ZenRule from, ZenRule to) {
1104            if (d == null) return;
1105            if (from == null) {
1106                if (to != null) {
1107                    d.addLine(item, "insert");
1108                }
1109                return;
1110            }
1111            from.appendDiff(d, item, to);
1112        }
1113
1114        private void appendDiff(Diff d, String item, ZenRule to) {
1115            if (to == null) {
1116                d.addLine(item, "delete");
1117                return;
1118            }
1119            if (enabled != to.enabled) {
1120                d.addLine(item, "enabled", enabled, to.enabled);
1121            }
1122            if (snoozing != to.snoozing) {
1123                d.addLine(item, "snoozing", snoozing, to.snoozing);
1124            }
1125            if (!Objects.equals(name, to.name)) {
1126                d.addLine(item, "name", name, to.name);
1127            }
1128            if (zenMode != to.zenMode) {
1129                d.addLine(item, "zenMode", zenMode, to.zenMode);
1130            }
1131            if (!Objects.equals(conditionId, to.conditionId)) {
1132                d.addLine(item, "conditionId", conditionId, to.conditionId);
1133            }
1134            if (!Objects.equals(condition, to.condition)) {
1135                d.addLine(item, "condition", condition, to.condition);
1136            }
1137            if (!Objects.equals(component, to.component)) {
1138                d.addLine(item, "component", component, to.component);
1139            }
1140            if (!Objects.equals(id, to.id)) {
1141                d.addLine(item, "id", id, to.id);
1142            }
1143            if (creationTime != to.creationTime) {
1144                d.addLine(item, "creationTime", creationTime, to.creationTime);
1145            }
1146        }
1147
1148        @Override
1149        public boolean equals(Object o) {
1150            if (!(o instanceof ZenRule)) return false;
1151            if (o == this) return true;
1152            final ZenRule other = (ZenRule) o;
1153            return other.enabled == enabled
1154                    && other.snoozing == snoozing
1155                    && Objects.equals(other.name, name)
1156                    && other.zenMode == zenMode
1157                    && Objects.equals(other.conditionId, conditionId)
1158                    && Objects.equals(other.condition, condition)
1159                    && Objects.equals(other.component, component)
1160                    && Objects.equals(other.id, id)
1161                    && other.creationTime == creationTime;
1162        }
1163
1164        @Override
1165        public int hashCode() {
1166            return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
1167                    component, id, creationTime);
1168        }
1169
1170        public boolean isAutomaticActive() {
1171            return enabled && !snoozing && component != null && isTrueOrUnknown();
1172        }
1173
1174        public boolean isTrueOrUnknown() {
1175            return condition != null && (condition.state == Condition.STATE_TRUE
1176                    || condition.state == Condition.STATE_UNKNOWN);
1177        }
1178
1179        public static final Parcelable.Creator<ZenRule> CREATOR
1180                = new Parcelable.Creator<ZenRule>() {
1181            @Override
1182            public ZenRule createFromParcel(Parcel source) {
1183                return new ZenRule(source);
1184            }
1185            @Override
1186            public ZenRule[] newArray(int size) {
1187                return new ZenRule[size];
1188            }
1189        };
1190    }
1191
1192    // Legacy config
1193    public static final class XmlV1 {
1194        public static final String SLEEP_MODE_NIGHTS = "nights";
1195        public static final String SLEEP_MODE_WEEKNIGHTS = "weeknights";
1196        public static final String SLEEP_MODE_DAYS_PREFIX = "days:";
1197
1198        private static final String EXIT_CONDITION_TAG = "exitCondition";
1199        private static final String EXIT_CONDITION_ATT_COMPONENT = "component";
1200        private static final String SLEEP_TAG = "sleep";
1201        private static final String SLEEP_ATT_MODE = "mode";
1202        private static final String SLEEP_ATT_NONE = "none";
1203
1204        private static final String SLEEP_ATT_START_HR = "startHour";
1205        private static final String SLEEP_ATT_START_MIN = "startMin";
1206        private static final String SLEEP_ATT_END_HR = "endHour";
1207        private static final String SLEEP_ATT_END_MIN = "endMin";
1208
1209        public boolean allowCalls;
1210        public boolean allowMessages;
1211        public boolean allowReminders = DEFAULT_ALLOW_REMINDERS;
1212        public boolean allowEvents = DEFAULT_ALLOW_EVENTS;
1213        public int allowFrom = SOURCE_ANYONE;
1214
1215        public String sleepMode;     // nights, weeknights, days:1,2,3  Calendar.days
1216        public int sleepStartHour;   // 0-23
1217        public int sleepStartMinute; // 0-59
1218        public int sleepEndHour;
1219        public int sleepEndMinute;
1220        public boolean sleepNone;    // false = priority, true = none
1221        public ComponentName[] conditionComponents;
1222        public Uri[] conditionIds;
1223        public Condition exitCondition;  // manual exit condition
1224        public ComponentName exitConditionComponent;  // manual exit condition component
1225
1226        private static boolean isValidSleepMode(String sleepMode) {
1227            return sleepMode == null || sleepMode.equals(SLEEP_MODE_NIGHTS)
1228                    || sleepMode.equals(SLEEP_MODE_WEEKNIGHTS) || tryParseDays(sleepMode) != null;
1229        }
1230
1231        public static int[] tryParseDays(String sleepMode) {
1232            if (sleepMode == null) return null;
1233            sleepMode = sleepMode.trim();
1234            if (SLEEP_MODE_NIGHTS.equals(sleepMode)) return ALL_DAYS;
1235            if (SLEEP_MODE_WEEKNIGHTS.equals(sleepMode)) return WEEKNIGHT_DAYS;
1236            if (!sleepMode.startsWith(SLEEP_MODE_DAYS_PREFIX)) return null;
1237            if (sleepMode.equals(SLEEP_MODE_DAYS_PREFIX)) return null;
1238            return tryParseDayList(sleepMode.substring(SLEEP_MODE_DAYS_PREFIX.length()), ",");
1239        }
1240
1241        public static XmlV1 readXml(XmlPullParser parser)
1242                throws XmlPullParserException, IOException {
1243            int type;
1244            String tag;
1245            XmlV1 rt = new XmlV1();
1246            final ArrayList<ComponentName> conditionComponents = new ArrayList<ComponentName>();
1247            final ArrayList<Uri> conditionIds = new ArrayList<Uri>();
1248            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
1249                tag = parser.getName();
1250                if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) {
1251                    if (!conditionComponents.isEmpty()) {
1252                        rt.conditionComponents = conditionComponents
1253                                .toArray(new ComponentName[conditionComponents.size()]);
1254                        rt.conditionIds = conditionIds.toArray(new Uri[conditionIds.size()]);
1255                    }
1256                    return rt;
1257                }
1258                if (type == XmlPullParser.START_TAG) {
1259                    if (ALLOW_TAG.equals(tag)) {
1260                        rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, false);
1261                        rt.allowMessages = safeBoolean(parser, ALLOW_ATT_MESSAGES, false);
1262                        rt.allowReminders = safeBoolean(parser, ALLOW_ATT_REMINDERS,
1263                                DEFAULT_ALLOW_REMINDERS);
1264                        rt.allowEvents = safeBoolean(parser, ALLOW_ATT_EVENTS,
1265                                DEFAULT_ALLOW_EVENTS);
1266                        rt.allowFrom = safeInt(parser, ALLOW_ATT_FROM, SOURCE_ANYONE);
1267                        if (rt.allowFrom < SOURCE_ANYONE || rt.allowFrom > MAX_SOURCE) {
1268                            throw new IndexOutOfBoundsException("bad source in config:"
1269                                    + rt.allowFrom);
1270                        }
1271                    } else if (SLEEP_TAG.equals(tag)) {
1272                        final String mode = parser.getAttributeValue(null, SLEEP_ATT_MODE);
1273                        rt.sleepMode = isValidSleepMode(mode)? mode : null;
1274                        rt.sleepNone = safeBoolean(parser, SLEEP_ATT_NONE, false);
1275                        final int startHour = safeInt(parser, SLEEP_ATT_START_HR, 0);
1276                        final int startMinute = safeInt(parser, SLEEP_ATT_START_MIN, 0);
1277                        final int endHour = safeInt(parser, SLEEP_ATT_END_HR, 0);
1278                        final int endMinute = safeInt(parser, SLEEP_ATT_END_MIN, 0);
1279                        rt.sleepStartHour = isValidHour(startHour) ? startHour : 0;
1280                        rt.sleepStartMinute = isValidMinute(startMinute) ? startMinute : 0;
1281                        rt.sleepEndHour = isValidHour(endHour) ? endHour : 0;
1282                        rt.sleepEndMinute = isValidMinute(endMinute) ? endMinute : 0;
1283                    } else if (CONDITION_TAG.equals(tag)) {
1284                        final ComponentName component =
1285                                safeComponentName(parser, CONDITION_ATT_COMPONENT);
1286                        final Uri conditionId = safeUri(parser, CONDITION_ATT_ID);
1287                        if (component != null && conditionId != null) {
1288                            conditionComponents.add(component);
1289                            conditionIds.add(conditionId);
1290                        }
1291                    } else if (EXIT_CONDITION_TAG.equals(tag)) {
1292                        rt.exitCondition = readConditionXml(parser);
1293                        if (rt.exitCondition != null) {
1294                            rt.exitConditionComponent =
1295                                    safeComponentName(parser, EXIT_CONDITION_ATT_COMPONENT);
1296                        }
1297                    }
1298                }
1299            }
1300            throw new IllegalStateException("Failed to reach END_DOCUMENT");
1301        }
1302    }
1303
1304    public interface Migration {
1305        ZenModeConfig migrate(XmlV1 v1);
1306    }
1307
1308    public static class Diff {
1309        private final ArrayList<String> lines = new ArrayList<>();
1310
1311        @Override
1312        public String toString() {
1313            final StringBuilder sb = new StringBuilder("Diff[");
1314            final int N = lines.size();
1315            for (int i = 0; i < N; i++) {
1316                if (i > 0) {
1317                    sb.append(',');
1318                }
1319                sb.append(lines.get(i));
1320            }
1321            return sb.append(']').toString();
1322        }
1323
1324        private Diff addLine(String item, String action) {
1325            lines.add(item + ":" + action);
1326            return this;
1327        }
1328
1329        public Diff addLine(String item, String subitem, Object from, Object to) {
1330            return addLine(item + "." + subitem, from, to);
1331        }
1332
1333        public Diff addLine(String item, Object from, Object to) {
1334            return addLine(item, from + "->" + to);
1335        }
1336    }
1337
1338}
1339