ZenModeConfig.java revision 4db0d98b42a723f2e16c6595e85e866fd26c6d98
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.content.ComponentName;
20import android.net.Uri;
21import android.os.Parcel;
22import android.os.Parcelable;
23import android.text.TextUtils;
24import android.util.Slog;
25
26import org.xmlpull.v1.XmlPullParser;
27import org.xmlpull.v1.XmlPullParserException;
28import org.xmlpull.v1.XmlSerializer;
29
30import java.io.IOException;
31import java.util.ArrayList;
32import java.util.Arrays;
33import java.util.Calendar;
34import java.util.Objects;
35
36/**
37 * Persisted configuration for zen mode.
38 *
39 * @hide
40 */
41public class ZenModeConfig implements Parcelable {
42    private static String TAG = "ZenModeConfig";
43
44    public static final String SLEEP_MODE_NIGHTS = "nights";
45    public static final String SLEEP_MODE_WEEKNIGHTS = "weeknights";
46    public static final String SLEEP_MODE_DAYS_PREFIX = "days:";
47
48    public static final int SOURCE_ANYONE = 0;
49    public static final int SOURCE_CONTACT = 1;
50    public static final int SOURCE_STAR = 2;
51    public static final int MAX_SOURCE = SOURCE_STAR;
52
53    public static final int[] ALL_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
54            Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY };
55    public static final int[] WEEKNIGHT_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
56            Calendar.WEDNESDAY, Calendar.THURSDAY };
57
58    private static final int XML_VERSION = 1;
59    private static final String ZEN_TAG = "zen";
60    private static final String ZEN_ATT_VERSION = "version";
61    private static final String ALLOW_TAG = "allow";
62    private static final String ALLOW_ATT_CALLS = "calls";
63    private static final String ALLOW_ATT_MESSAGES = "messages";
64    private static final String ALLOW_ATT_FROM = "from";
65    private static final String SLEEP_TAG = "sleep";
66    private static final String SLEEP_ATT_MODE = "mode";
67
68    private static final String SLEEP_ATT_START_HR = "startHour";
69    private static final String SLEEP_ATT_START_MIN = "startMin";
70    private static final String SLEEP_ATT_END_HR = "endHour";
71    private static final String SLEEP_ATT_END_MIN = "endMin";
72
73    private static final String CONDITION_TAG = "condition";
74    private static final String CONDITION_ATT_COMPONENT = "component";
75    private static final String CONDITION_ATT_ID = "id";
76    private static final String CONDITION_ATT_SUMMARY = "summary";
77    private static final String CONDITION_ATT_LINE1 = "line1";
78    private static final String CONDITION_ATT_LINE2 = "line2";
79    private static final String CONDITION_ATT_ICON = "icon";
80    private static final String CONDITION_ATT_STATE = "state";
81    private static final String CONDITION_ATT_FLAGS = "flags";
82
83    private static final String EXIT_CONDITION_TAG = "exitCondition";
84    private static final String EXIT_CONDITION_ATT_COMPONENT = "component";
85
86    public boolean allowCalls;
87    public boolean allowMessages;
88    public int allowFrom = SOURCE_ANYONE;
89
90    public String sleepMode;
91    public int sleepStartHour;   // 0-23
92    public int sleepStartMinute; // 0-59
93    public int sleepEndHour;
94    public int sleepEndMinute;
95    public ComponentName[] conditionComponents;
96    public Uri[] conditionIds;
97    public Condition exitCondition;
98    public ComponentName exitConditionComponent;
99
100    public ZenModeConfig() { }
101
102    public ZenModeConfig(Parcel source) {
103        allowCalls = source.readInt() == 1;
104        allowMessages = source.readInt() == 1;
105        if (source.readInt() == 1) {
106            sleepMode = source.readString();
107        }
108        sleepStartHour = source.readInt();
109        sleepStartMinute = source.readInt();
110        sleepEndHour = source.readInt();
111        sleepEndMinute = source.readInt();
112        int len = source.readInt();
113        if (len > 0) {
114            conditionComponents = new ComponentName[len];
115            source.readTypedArray(conditionComponents, ComponentName.CREATOR);
116        }
117        len = source.readInt();
118        if (len > 0) {
119            conditionIds = new Uri[len];
120            source.readTypedArray(conditionIds, Uri.CREATOR);
121        }
122        allowFrom = source.readInt();
123        exitCondition = source.readParcelable(null);
124        exitConditionComponent = source.readParcelable(null);
125    }
126
127    @Override
128    public void writeToParcel(Parcel dest, int flags) {
129        dest.writeInt(allowCalls ? 1 : 0);
130        dest.writeInt(allowMessages ? 1 : 0);
131        if (sleepMode != null) {
132            dest.writeInt(1);
133            dest.writeString(sleepMode);
134        } else {
135            dest.writeInt(0);
136        }
137        dest.writeInt(sleepStartHour);
138        dest.writeInt(sleepStartMinute);
139        dest.writeInt(sleepEndHour);
140        dest.writeInt(sleepEndMinute);
141        if (conditionComponents != null && conditionComponents.length > 0) {
142            dest.writeInt(conditionComponents.length);
143            dest.writeTypedArray(conditionComponents, 0);
144        } else {
145            dest.writeInt(0);
146        }
147        if (conditionIds != null && conditionIds.length > 0) {
148            dest.writeInt(conditionIds.length);
149            dest.writeTypedArray(conditionIds, 0);
150        } else {
151            dest.writeInt(0);
152        }
153        dest.writeInt(allowFrom);
154        dest.writeParcelable(exitCondition, 0);
155        dest.writeParcelable(exitConditionComponent, 0);
156    }
157
158    @Override
159    public String toString() {
160        return new StringBuilder(ZenModeConfig.class.getSimpleName()).append('[')
161            .append("allowCalls=").append(allowCalls)
162            .append(",allowMessages=").append(allowMessages)
163            .append(",allowFrom=").append(sourceToString(allowFrom))
164            .append(",sleepMode=").append(sleepMode)
165            .append(",sleepStart=").append(sleepStartHour).append('.').append(sleepStartMinute)
166            .append(",sleepEnd=").append(sleepEndHour).append('.').append(sleepEndMinute)
167            .append(",conditionComponents=")
168            .append(conditionComponents == null ? null : TextUtils.join(",", conditionComponents))
169            .append(",conditionIds=")
170            .append(conditionIds == null ? null : TextUtils.join(",", conditionIds))
171            .append(",exitCondition=").append(exitCondition)
172            .append(",exitConditionComponent=").append(exitConditionComponent)
173            .append(']').toString();
174    }
175
176    public static String sourceToString(int source) {
177        switch (source) {
178            case SOURCE_ANYONE:
179                return "anyone";
180            case SOURCE_CONTACT:
181                return "contacts";
182            case SOURCE_STAR:
183                return "stars";
184            default:
185                return "UNKNOWN";
186        }
187    }
188
189    @Override
190    public boolean equals(Object o) {
191        if (!(o instanceof ZenModeConfig)) return false;
192        if (o == this) return true;
193        final ZenModeConfig other = (ZenModeConfig) o;
194        return other.allowCalls == allowCalls
195                && other.allowMessages == allowMessages
196                && other.allowFrom == allowFrom
197                && Objects.equals(other.sleepMode, sleepMode)
198                && other.sleepStartHour == sleepStartHour
199                && other.sleepStartMinute == sleepStartMinute
200                && other.sleepEndHour == sleepEndHour
201                && other.sleepEndMinute == sleepEndMinute
202                && Objects.deepEquals(other.conditionComponents, conditionComponents)
203                && Objects.deepEquals(other.conditionIds, conditionIds)
204                && Objects.equals(other.exitCondition, exitCondition)
205                && Objects.equals(other.exitConditionComponent, exitConditionComponent);
206    }
207
208    @Override
209    public int hashCode() {
210        return Objects.hash(allowCalls, allowMessages, allowFrom, sleepMode,
211                sleepStartHour, sleepStartMinute, sleepEndHour, sleepEndMinute,
212                Arrays.hashCode(conditionComponents), Arrays.hashCode(conditionIds),
213                exitCondition, exitConditionComponent);
214    }
215
216    public boolean isValid() {
217        return isValidHour(sleepStartHour) && isValidMinute(sleepStartMinute)
218                && isValidHour(sleepEndHour) && isValidMinute(sleepEndMinute)
219                && isValidSleepMode(sleepMode);
220    }
221
222    public static boolean isValidSleepMode(String sleepMode) {
223        return sleepMode == null || sleepMode.equals(SLEEP_MODE_NIGHTS)
224                || sleepMode.equals(SLEEP_MODE_WEEKNIGHTS) || tryParseDays(sleepMode) != null;
225    }
226
227    public static int[] tryParseDays(String sleepMode) {
228        if (sleepMode == null) return null;
229        sleepMode = sleepMode.trim();
230        if (SLEEP_MODE_NIGHTS.equals(sleepMode)) return ALL_DAYS;
231        if (SLEEP_MODE_WEEKNIGHTS.equals(sleepMode)) return WEEKNIGHT_DAYS;
232        if (!sleepMode.startsWith(SLEEP_MODE_DAYS_PREFIX)) return null;
233        if (sleepMode.equals(SLEEP_MODE_DAYS_PREFIX)) return null;
234        final String[] tokens = sleepMode.substring(SLEEP_MODE_DAYS_PREFIX.length()).split(",");
235        if (tokens.length == 0) return null;
236        final int[] rt = new int[tokens.length];
237        for (int i = 0; i < tokens.length; i++) {
238            final int day = tryParseInt(tokens[i], -1);
239            if (day == -1) return null;
240            rt[i] = day;
241        }
242        return rt;
243    }
244
245    private static int tryParseInt(String value, int defValue) {
246        if (TextUtils.isEmpty(value)) return defValue;
247        try {
248            return Integer.valueOf(value);
249        } catch (NumberFormatException e) {
250            return defValue;
251        }
252    }
253
254    public static ZenModeConfig readXml(XmlPullParser parser)
255            throws XmlPullParserException, IOException {
256        int type = parser.getEventType();
257        if (type != XmlPullParser.START_TAG) return null;
258        String tag = parser.getName();
259        if (!ZEN_TAG.equals(tag)) return null;
260        final ZenModeConfig rt = new ZenModeConfig();
261        final int version = safeInt(parser, ZEN_ATT_VERSION, XML_VERSION);
262        final ArrayList<ComponentName> conditionComponents = new ArrayList<ComponentName>();
263        final ArrayList<Uri> conditionIds = new ArrayList<Uri>();
264        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
265            tag = parser.getName();
266            if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) {
267                if (!conditionComponents.isEmpty()) {
268                    rt.conditionComponents = conditionComponents
269                            .toArray(new ComponentName[conditionComponents.size()]);
270                    rt.conditionIds = conditionIds.toArray(new Uri[conditionIds.size()]);
271                }
272                return rt;
273            }
274            if (type == XmlPullParser.START_TAG) {
275                if (ALLOW_TAG.equals(tag)) {
276                    rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, false);
277                    rt.allowMessages = safeBoolean(parser, ALLOW_ATT_MESSAGES, false);
278                    rt.allowFrom = safeInt(parser, ALLOW_ATT_FROM, SOURCE_ANYONE);
279                    if (rt.allowFrom < SOURCE_ANYONE || rt.allowFrom > MAX_SOURCE) {
280                        throw new IndexOutOfBoundsException("bad source in config:" + rt.allowFrom);
281                    }
282                } else if (SLEEP_TAG.equals(tag)) {
283                    final String mode = parser.getAttributeValue(null, SLEEP_ATT_MODE);
284                    rt.sleepMode = isValidSleepMode(mode)? mode : null;
285                    final int startHour = safeInt(parser, SLEEP_ATT_START_HR, 0);
286                    final int startMinute = safeInt(parser, SLEEP_ATT_START_MIN, 0);
287                    final int endHour = safeInt(parser, SLEEP_ATT_END_HR, 0);
288                    final int endMinute = safeInt(parser, SLEEP_ATT_END_MIN, 0);
289                    rt.sleepStartHour = isValidHour(startHour) ? startHour : 0;
290                    rt.sleepStartMinute = isValidMinute(startMinute) ? startMinute : 0;
291                    rt.sleepEndHour = isValidHour(endHour) ? endHour : 0;
292                    rt.sleepEndMinute = isValidMinute(endMinute) ? endMinute : 0;
293                } else if (CONDITION_TAG.equals(tag)) {
294                    final ComponentName component =
295                            safeComponentName(parser, CONDITION_ATT_COMPONENT);
296                    final Uri conditionId = safeUri(parser, CONDITION_ATT_ID);
297                    if (component != null && conditionId != null) {
298                        conditionComponents.add(component);
299                        conditionIds.add(conditionId);
300                    }
301                } else if (EXIT_CONDITION_TAG.equals(tag)) {
302                    rt.exitCondition = readConditionXml(parser);
303                    if (rt.exitCondition != null) {
304                        rt.exitConditionComponent =
305                                safeComponentName(parser, EXIT_CONDITION_ATT_COMPONENT);
306                    }
307                }
308            }
309        }
310        throw new IllegalStateException("Failed to reach END_DOCUMENT");
311    }
312
313    public void writeXml(XmlSerializer out) throws IOException {
314        out.startTag(null, ZEN_TAG);
315        out.attribute(null, ZEN_ATT_VERSION, Integer.toString(XML_VERSION));
316
317        out.startTag(null, ALLOW_TAG);
318        out.attribute(null, ALLOW_ATT_CALLS, Boolean.toString(allowCalls));
319        out.attribute(null, ALLOW_ATT_MESSAGES, Boolean.toString(allowMessages));
320        out.attribute(null, ALLOW_ATT_FROM, Integer.toString(allowFrom));
321        out.endTag(null, ALLOW_TAG);
322
323        out.startTag(null, SLEEP_TAG);
324        if (sleepMode != null) {
325            out.attribute(null, SLEEP_ATT_MODE, sleepMode);
326        }
327        out.attribute(null, SLEEP_ATT_START_HR, Integer.toString(sleepStartHour));
328        out.attribute(null, SLEEP_ATT_START_MIN, Integer.toString(sleepStartMinute));
329        out.attribute(null, SLEEP_ATT_END_HR, Integer.toString(sleepEndHour));
330        out.attribute(null, SLEEP_ATT_END_MIN, Integer.toString(sleepEndMinute));
331        out.endTag(null, SLEEP_TAG);
332
333        if (conditionComponents != null && conditionIds != null
334                && conditionComponents.length == conditionIds.length) {
335            for (int i = 0; i < conditionComponents.length; i++) {
336                out.startTag(null, CONDITION_TAG);
337                out.attribute(null, CONDITION_ATT_COMPONENT,
338                        conditionComponents[i].flattenToString());
339                out.attribute(null, CONDITION_ATT_ID, conditionIds[i].toString());
340                out.endTag(null, CONDITION_TAG);
341            }
342        }
343        if (exitCondition != null && exitConditionComponent != null) {
344            out.startTag(null, EXIT_CONDITION_TAG);
345            out.attribute(null, EXIT_CONDITION_ATT_COMPONENT,
346                    exitConditionComponent.flattenToString());
347            writeConditionXml(exitCondition, out);
348            out.endTag(null, EXIT_CONDITION_TAG);
349        }
350        out.endTag(null, ZEN_TAG);
351    }
352
353    public static Condition readConditionXml(XmlPullParser parser) {
354        final Uri id = safeUri(parser, CONDITION_ATT_ID);
355        final String summary = parser.getAttributeValue(null, CONDITION_ATT_SUMMARY);
356        final String line1 = parser.getAttributeValue(null, CONDITION_ATT_LINE1);
357        final String line2 = parser.getAttributeValue(null, CONDITION_ATT_LINE2);
358        final int icon = safeInt(parser, CONDITION_ATT_ICON, -1);
359        final int state = safeInt(parser, CONDITION_ATT_STATE, -1);
360        final int flags = safeInt(parser, CONDITION_ATT_FLAGS, -1);
361        try {
362            return new Condition(id, summary, line1, line2, icon, state, flags);
363        } catch (IllegalArgumentException e) {
364            Slog.w(TAG, "Unable to read condition xml", e);
365            return null;
366        }
367    }
368
369    public static void writeConditionXml(Condition c, XmlSerializer out) throws IOException {
370        out.attribute(null, CONDITION_ATT_ID, c.id.toString());
371        out.attribute(null, CONDITION_ATT_SUMMARY, c.summary);
372        out.attribute(null, CONDITION_ATT_LINE1, c.line1);
373        out.attribute(null, CONDITION_ATT_LINE2, c.line2);
374        out.attribute(null, CONDITION_ATT_ICON, Integer.toString(c.icon));
375        out.attribute(null, CONDITION_ATT_STATE, Integer.toString(c.state));
376        out.attribute(null, CONDITION_ATT_FLAGS, Integer.toString(c.flags));
377    }
378
379    public static boolean isValidHour(int val) {
380        return val >= 0 && val < 24;
381    }
382
383    public static boolean isValidMinute(int val) {
384        return val >= 0 && val < 60;
385    }
386
387    private static boolean safeBoolean(XmlPullParser parser, String att, boolean defValue) {
388        final String val = parser.getAttributeValue(null, att);
389        if (TextUtils.isEmpty(val)) return defValue;
390        return Boolean.valueOf(val);
391    }
392
393    private static int safeInt(XmlPullParser parser, String att, int defValue) {
394        final String val = parser.getAttributeValue(null, att);
395        return tryParseInt(val, defValue);
396    }
397
398    private static ComponentName safeComponentName(XmlPullParser parser, String att) {
399        final String val = parser.getAttributeValue(null, att);
400        if (TextUtils.isEmpty(val)) return null;
401        return ComponentName.unflattenFromString(val);
402    }
403
404    private static Uri safeUri(XmlPullParser parser, String att) {
405        final String val = parser.getAttributeValue(null, att);
406        if (TextUtils.isEmpty(val)) return null;
407        return Uri.parse(val);
408    }
409
410    @Override
411    public int describeContents() {
412        return 0;
413    }
414
415    public ZenModeConfig copy() {
416        final Parcel parcel = Parcel.obtain();
417        try {
418            writeToParcel(parcel, 0);
419            parcel.setDataPosition(0);
420            return new ZenModeConfig(parcel);
421        } finally {
422            parcel.recycle();
423        }
424    }
425
426    public static final Parcelable.Creator<ZenModeConfig> CREATOR
427            = new Parcelable.Creator<ZenModeConfig>() {
428        @Override
429        public ZenModeConfig createFromParcel(Parcel source) {
430            return new ZenModeConfig(source);
431        }
432
433        @Override
434        public ZenModeConfig[] newArray(int size) {
435            return new ZenModeConfig[size];
436        }
437    };
438
439    public DowntimeInfo toDowntimeInfo() {
440        final DowntimeInfo downtime = new DowntimeInfo();
441        downtime.startHour = sleepStartHour;
442        downtime.startMinute = sleepStartMinute;
443        downtime.endHour = sleepEndHour;
444        downtime.endMinute = sleepEndMinute;
445        return downtime;
446    }
447
448    // For built-in conditions
449    private static final String SYSTEM_AUTHORITY = "android";
450
451    // Built-in countdown conditions, e.g. condition://android/countdown/1399917958951
452    private static final String COUNTDOWN_PATH = "countdown";
453
454    public static Uri toCountdownConditionId(long time) {
455        return new Uri.Builder().scheme(Condition.SCHEME)
456                .authority(SYSTEM_AUTHORITY)
457                .appendPath(COUNTDOWN_PATH)
458                .appendPath(Long.toString(time))
459                .build();
460    }
461
462    public static long tryParseCountdownConditionId(Uri conditionId) {
463        if (!Condition.isValidId(conditionId, SYSTEM_AUTHORITY)) return 0;
464        if (conditionId.getPathSegments().size() != 2
465                || !COUNTDOWN_PATH.equals(conditionId.getPathSegments().get(0))) return 0;
466        try {
467            return Long.parseLong(conditionId.getPathSegments().get(1));
468        } catch (RuntimeException e) {
469            Slog.w(TAG, "Error parsing countdown condition: " + conditionId, e);
470            return 0;
471        }
472    }
473
474    public static boolean isValidCountdownConditionId(Uri conditionId) {
475        return tryParseCountdownConditionId(conditionId) != 0;
476    }
477
478    // Built-in downtime conditions, e.g. condition://android/downtime?start=10.00&end=7.00
479    private static final String DOWNTIME_PATH = "downtime";
480
481    public static Uri toDowntimeConditionId(DowntimeInfo downtime) {
482        return new Uri.Builder().scheme(Condition.SCHEME)
483                .authority(SYSTEM_AUTHORITY)
484                .appendPath(DOWNTIME_PATH)
485                .appendQueryParameter("start", downtime.startHour + "." + downtime.startMinute)
486                .appendQueryParameter("end", downtime.endHour + "." + downtime.endMinute)
487                .build();
488    }
489
490    public static DowntimeInfo tryParseDowntimeConditionId(Uri conditionId) {
491        if (!Condition.isValidId(conditionId, SYSTEM_AUTHORITY)
492                || conditionId.getPathSegments().size() != 1
493                || !DOWNTIME_PATH.equals(conditionId.getPathSegments().get(0))) {
494            return null;
495        }
496        final int[] start = tryParseHourAndMinute(conditionId.getQueryParameter("start"));
497        final int[] end = tryParseHourAndMinute(conditionId.getQueryParameter("end"));
498        if (start == null || end == null) return null;
499        final DowntimeInfo downtime = new DowntimeInfo();
500        downtime.startHour = start[0];
501        downtime.startMinute = start[1];
502        downtime.endHour = end[0];
503        downtime.endMinute = end[1];
504        return downtime;
505    }
506
507    private static int[] tryParseHourAndMinute(String value) {
508        if (TextUtils.isEmpty(value)) return null;
509        final int i = value.indexOf('.');
510        if (i < 1 || i >= value.length() - 1) return null;
511        final int hour = tryParseInt(value.substring(0, i), -1);
512        final int minute = tryParseInt(value.substring(i + 1), -1);
513        return isValidHour(hour) && isValidMinute(minute) ? new int[] { hour, minute } : null;
514    }
515
516    public static boolean isValidDowntimeConditionId(Uri conditionId) {
517        return tryParseDowntimeConditionId(conditionId) != null;
518    }
519
520    public static class DowntimeInfo {
521        public int startHour;   // 0-23
522        public int startMinute; // 0-59
523        public int endHour;
524        public int endMinute;
525
526        @Override
527        public int hashCode() {
528            return 0;
529        }
530
531        @Override
532        public boolean equals(Object o) {
533            if (!(o instanceof DowntimeInfo)) return false;
534            final DowntimeInfo other = (DowntimeInfo) o;
535            return startHour == other.startHour
536                    && startMinute == other.startMinute
537                    && endHour == other.endHour
538                    && endMinute == other.endMinute;
539        }
540    }
541}
542