ZenModeConfig.java revision d39af2d3f8c5d87e102aeb79d4148218ff616245
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("reply", Integer.toString(event.reply))
696                .build();
697    }
698
699    public static boolean isValidEventConditionId(Uri conditionId) {
700        return tryParseEventConditionId(conditionId) != null;
701    }
702
703    public static EventInfo tryParseEventConditionId(Uri conditionId) {
704        final boolean isEvent = conditionId != null
705                && conditionId.getScheme().equals(Condition.SCHEME)
706                && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY)
707                && conditionId.getPathSegments().size() == 1
708                && conditionId.getPathSegments().get(0).equals(EVENT_PATH);
709        if (!isEvent) return null;
710        final EventInfo rt = new EventInfo();
711        rt.calendar = tryParseLong(conditionId.getQueryParameter("calendar"), 0L);
712        rt.reply = tryParseInt(conditionId.getQueryParameter("reply"), 0);
713        return rt;
714    }
715
716    public static class EventInfo {
717        public static final int REPLY_ANY_EXCEPT_NO = 0;
718        public static final int REPLY_YES_OR_MAYBE = 1;
719        public static final int REPLY_YES = 2;
720
721        public long calendar;  // CalendarContract.Calendars._ID, or 0 for any
722        public int reply;
723
724        @Override
725        public int hashCode() {
726            return 0;
727        }
728
729        @Override
730        public boolean equals(Object o) {
731            if (!(o instanceof EventInfo)) return false;
732            final EventInfo other = (EventInfo) o;
733            return calendar == other.calendar
734                    && reply == other.reply;
735        }
736
737        public EventInfo copy() {
738            final EventInfo rt = new EventInfo();
739            rt.calendar = calendar;
740            rt.reply = reply;
741            return rt;
742        }
743    }
744
745    // ==== End built-in system conditions ====
746
747    private static int[] tryParseHourAndMinute(String value) {
748        if (TextUtils.isEmpty(value)) return null;
749        final int i = value.indexOf('.');
750        if (i < 1 || i >= value.length() - 1) return null;
751        final int hour = tryParseInt(value.substring(0, i), -1);
752        final int minute = tryParseInt(value.substring(i + 1), -1);
753        return isValidHour(hour) && isValidMinute(minute) ? new int[] { hour, minute } : null;
754    }
755
756    private static int tryParseZenMode(String value, int defValue) {
757        final int rt = tryParseInt(value, defValue);
758        return Global.isValidZenMode(rt) ? rt : defValue;
759    }
760
761    public String newRuleId() {
762        return UUID.randomUUID().toString().replace("-", "");
763    }
764
765    public static String getConditionLine1(Context context, ZenModeConfig config,
766            int userHandle) {
767        return getConditionLine(context, config, userHandle, true /*useLine1*/);
768    }
769
770    public static String getConditionSummary(Context context, ZenModeConfig config,
771            int userHandle) {
772        return getConditionLine(context, config, userHandle, false /*useLine1*/);
773    }
774
775    private static String getConditionLine(Context context, ZenModeConfig config,
776            int userHandle, boolean useLine1) {
777        if (config == null) return "";
778        if (config.manualRule != null) {
779            final Uri id = config.manualRule.conditionId;
780            if (id == null) {
781                return context.getString(com.android.internal.R.string.zen_mode_forever);
782            }
783            final long time = tryParseCountdownConditionId(id);
784            Condition c = config.manualRule.condition;
785            if (time > 0) {
786                final long now = System.currentTimeMillis();
787                final long span = time - now;
788                c = toTimeCondition(context,
789                        time, Math.round(span / (float) MINUTES_MS), now, userHandle);
790            }
791            final String rt = c == null ? "" : useLine1 ? c.line1 : c.summary;
792            return TextUtils.isEmpty(rt) ? "" : rt;
793        }
794        String summary = "";
795        for (ZenRule automaticRule : config.automaticRules.values()) {
796            if (automaticRule.isAutomaticActive()) {
797                if (summary.isEmpty()) {
798                    summary = automaticRule.name;
799                } else {
800                    summary = context.getResources()
801                            .getString(R.string.zen_mode_rule_name_combination, summary,
802                                    automaticRule.name);
803                }
804            }
805        }
806        return summary;
807    }
808
809    public static class ZenRule implements Parcelable {
810        public boolean enabled;
811        public boolean snoozing;         // user manually disabled this instance
812        public String name;              // required for automatic (unique)
813        public int zenMode;
814        public Uri conditionId;          // required for automatic
815        public Condition condition;      // optional
816        public ComponentName component;  // optional
817
818        public ZenRule() { }
819
820        public ZenRule(Parcel source) {
821            enabled = source.readInt() == 1;
822            snoozing = source.readInt() == 1;
823            if (source.readInt() == 1) {
824                name = source.readString();
825            }
826            zenMode = source.readInt();
827            conditionId = source.readParcelable(null);
828            condition = source.readParcelable(null);
829            component = source.readParcelable(null);
830        }
831
832        @Override
833        public int describeContents() {
834            return 0;
835        }
836
837        @Override
838        public void writeToParcel(Parcel dest, int flags) {
839            dest.writeInt(enabled ? 1 : 0);
840            dest.writeInt(snoozing ? 1 : 0);
841            if (name != null) {
842                dest.writeInt(1);
843                dest.writeString(name);
844            } else {
845                dest.writeInt(0);
846            }
847            dest.writeInt(zenMode);
848            dest.writeParcelable(conditionId, 0);
849            dest.writeParcelable(condition, 0);
850            dest.writeParcelable(component, 0);
851        }
852
853        @Override
854        public String toString() {
855            return new StringBuilder(ZenRule.class.getSimpleName()).append('[')
856                    .append("enabled=").append(enabled)
857                    .append(",snoozing=").append(snoozing)
858                    .append(",name=").append(name)
859                    .append(",zenMode=").append(Global.zenModeToString(zenMode))
860                    .append(",conditionId=").append(conditionId)
861                    .append(",condition=").append(condition)
862                    .append(",component=").append(component)
863                    .append(']').toString();
864        }
865
866        @Override
867        public boolean equals(Object o) {
868            if (!(o instanceof ZenRule)) return false;
869            if (o == this) return true;
870            final ZenRule other = (ZenRule) o;
871            return other.enabled == enabled
872                    && other.snoozing == snoozing
873                    && Objects.equals(other.name, name)
874                    && other.zenMode == zenMode
875                    && Objects.equals(other.conditionId, conditionId)
876                    && Objects.equals(other.condition, condition)
877                    && Objects.equals(other.component, component);
878        }
879
880        @Override
881        public int hashCode() {
882            return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
883                    component);
884        }
885
886        public boolean isAutomaticActive() {
887            return enabled && !snoozing && component != null && isTrueOrUnknown();
888        }
889
890        public boolean isTrueOrUnknown() {
891            return condition != null && (condition.state == Condition.STATE_TRUE
892                    || condition.state == Condition.STATE_UNKNOWN);
893        }
894
895        public static final Parcelable.Creator<ZenRule> CREATOR
896                = new Parcelable.Creator<ZenRule>() {
897            @Override
898            public ZenRule createFromParcel(Parcel source) {
899                return new ZenRule(source);
900            }
901            @Override
902            public ZenRule[] newArray(int size) {
903                return new ZenRule[size];
904            }
905        };
906    }
907
908    // Legacy config
909    public static final class XmlV1 {
910        public static final String SLEEP_MODE_NIGHTS = "nights";
911        public static final String SLEEP_MODE_WEEKNIGHTS = "weeknights";
912        public static final String SLEEP_MODE_DAYS_PREFIX = "days:";
913
914        private static final String EXIT_CONDITION_TAG = "exitCondition";
915        private static final String EXIT_CONDITION_ATT_COMPONENT = "component";
916        private static final String SLEEP_TAG = "sleep";
917        private static final String SLEEP_ATT_MODE = "mode";
918        private static final String SLEEP_ATT_NONE = "none";
919
920        private static final String SLEEP_ATT_START_HR = "startHour";
921        private static final String SLEEP_ATT_START_MIN = "startMin";
922        private static final String SLEEP_ATT_END_HR = "endHour";
923        private static final String SLEEP_ATT_END_MIN = "endMin";
924
925        public boolean allowCalls;
926        public boolean allowMessages;
927        public boolean allowReminders = DEFAULT_ALLOW_REMINDERS;
928        public boolean allowEvents = DEFAULT_ALLOW_EVENTS;
929        public int allowFrom = SOURCE_ANYONE;
930
931        public String sleepMode;     // nights, weeknights, days:1,2,3  Calendar.days
932        public int sleepStartHour;   // 0-23
933        public int sleepStartMinute; // 0-59
934        public int sleepEndHour;
935        public int sleepEndMinute;
936        public boolean sleepNone;    // false = priority, true = none
937        public ComponentName[] conditionComponents;
938        public Uri[] conditionIds;
939        public Condition exitCondition;  // manual exit condition
940        public ComponentName exitConditionComponent;  // manual exit condition component
941
942        private static boolean isValidSleepMode(String sleepMode) {
943            return sleepMode == null || sleepMode.equals(SLEEP_MODE_NIGHTS)
944                    || sleepMode.equals(SLEEP_MODE_WEEKNIGHTS) || tryParseDays(sleepMode) != null;
945        }
946
947        public static int[] tryParseDays(String sleepMode) {
948            if (sleepMode == null) return null;
949            sleepMode = sleepMode.trim();
950            if (SLEEP_MODE_NIGHTS.equals(sleepMode)) return ALL_DAYS;
951            if (SLEEP_MODE_WEEKNIGHTS.equals(sleepMode)) return WEEKNIGHT_DAYS;
952            if (!sleepMode.startsWith(SLEEP_MODE_DAYS_PREFIX)) return null;
953            if (sleepMode.equals(SLEEP_MODE_DAYS_PREFIX)) return null;
954            return tryParseDayList(sleepMode.substring(SLEEP_MODE_DAYS_PREFIX.length()), ",");
955        }
956
957        public static XmlV1 readXml(XmlPullParser parser)
958                throws XmlPullParserException, IOException {
959            int type;
960            String tag;
961            XmlV1 rt = new XmlV1();
962            final ArrayList<ComponentName> conditionComponents = new ArrayList<ComponentName>();
963            final ArrayList<Uri> conditionIds = new ArrayList<Uri>();
964            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
965                tag = parser.getName();
966                if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) {
967                    if (!conditionComponents.isEmpty()) {
968                        rt.conditionComponents = conditionComponents
969                                .toArray(new ComponentName[conditionComponents.size()]);
970                        rt.conditionIds = conditionIds.toArray(new Uri[conditionIds.size()]);
971                    }
972                    return rt;
973                }
974                if (type == XmlPullParser.START_TAG) {
975                    if (ALLOW_TAG.equals(tag)) {
976                        rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, false);
977                        rt.allowMessages = safeBoolean(parser, ALLOW_ATT_MESSAGES, false);
978                        rt.allowReminders = safeBoolean(parser, ALLOW_ATT_REMINDERS,
979                                DEFAULT_ALLOW_REMINDERS);
980                        rt.allowEvents = safeBoolean(parser, ALLOW_ATT_EVENTS,
981                                DEFAULT_ALLOW_EVENTS);
982                        rt.allowFrom = safeInt(parser, ALLOW_ATT_FROM, SOURCE_ANYONE);
983                        if (rt.allowFrom < SOURCE_ANYONE || rt.allowFrom > MAX_SOURCE) {
984                            throw new IndexOutOfBoundsException("bad source in config:"
985                                    + rt.allowFrom);
986                        }
987                    } else if (SLEEP_TAG.equals(tag)) {
988                        final String mode = parser.getAttributeValue(null, SLEEP_ATT_MODE);
989                        rt.sleepMode = isValidSleepMode(mode)? mode : null;
990                        rt.sleepNone = safeBoolean(parser, SLEEP_ATT_NONE, false);
991                        final int startHour = safeInt(parser, SLEEP_ATT_START_HR, 0);
992                        final int startMinute = safeInt(parser, SLEEP_ATT_START_MIN, 0);
993                        final int endHour = safeInt(parser, SLEEP_ATT_END_HR, 0);
994                        final int endMinute = safeInt(parser, SLEEP_ATT_END_MIN, 0);
995                        rt.sleepStartHour = isValidHour(startHour) ? startHour : 0;
996                        rt.sleepStartMinute = isValidMinute(startMinute) ? startMinute : 0;
997                        rt.sleepEndHour = isValidHour(endHour) ? endHour : 0;
998                        rt.sleepEndMinute = isValidMinute(endMinute) ? endMinute : 0;
999                    } else if (CONDITION_TAG.equals(tag)) {
1000                        final ComponentName component =
1001                                safeComponentName(parser, CONDITION_ATT_COMPONENT);
1002                        final Uri conditionId = safeUri(parser, CONDITION_ATT_ID);
1003                        if (component != null && conditionId != null) {
1004                            conditionComponents.add(component);
1005                            conditionIds.add(conditionId);
1006                        }
1007                    } else if (EXIT_CONDITION_TAG.equals(tag)) {
1008                        rt.exitCondition = readConditionXml(parser);
1009                        if (rt.exitCondition != null) {
1010                            rt.exitConditionComponent =
1011                                    safeComponentName(parser, EXIT_CONDITION_ATT_COMPONENT);
1012                        }
1013                    }
1014                }
1015            }
1016            throw new IllegalStateException("Failed to reach END_DOCUMENT");
1017        }
1018    }
1019
1020    public interface Migration {
1021        ZenModeConfig migrate(XmlV1 v1);
1022    }
1023
1024}
1025