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 com.android.server.notification;
18
19import android.app.AlarmManager;
20import android.app.AlarmManager.AlarmClockInfo;
21import android.content.ComponentName;
22import android.content.Context;
23import android.net.Uri;
24import android.service.notification.Condition;
25import android.service.notification.ConditionProviderService;
26import android.service.notification.IConditionProvider;
27import android.service.notification.ZenModeConfig;
28import android.text.TextUtils;
29import android.util.ArraySet;
30import android.util.Log;
31import android.util.Slog;
32import android.util.TimeUtils;
33
34import com.android.internal.R;
35import com.android.server.notification.NotificationManagerService.DumpFilter;
36
37import java.io.PrintWriter;
38
39/**
40 * Built-in zen condition provider for alarm-clock-based conditions.
41 *
42 * <p>If the user's next alarm is within a lookahead threshold (config, default 12hrs), advertise
43 * it as an exit condition for zen mode.
44 *
45 * <p>The next alarm is defined as {@link AlarmManager#getNextAlarmClock(int)}, which does not
46 * survive a reboot.  Maintain the illusion of a consistent next alarm value by holding on to
47 * a persisted condition until we receive the first value after reboot, or timeout with no value.
48 */
49public class NextAlarmConditionProvider extends ConditionProviderService {
50    private static final String TAG = "NextAlarmConditions";
51    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
52
53    private static final long SECONDS = 1000;
54    private static final long MINUTES = 60 * SECONDS;
55    private static final long HOURS = 60 * MINUTES;
56
57    private static final long BAD_CONDITION = -1;
58
59    public static final ComponentName COMPONENT =
60            new ComponentName("android", NextAlarmConditionProvider.class.getName());
61
62    private final Context mContext = this;
63    private final NextAlarmTracker mTracker;
64    private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>();
65
66    private boolean mConnected;
67    private long mLookaheadThreshold;
68    private boolean mRequesting;
69
70    public NextAlarmConditionProvider(NextAlarmTracker tracker) {
71        if (DEBUG) Slog.d(TAG, "new NextAlarmConditionProvider()");
72        mTracker = tracker;
73    }
74
75    public void dump(PrintWriter pw, DumpFilter filter) {
76        pw.println("    NextAlarmConditionProvider:");
77        pw.print("      mConnected="); pw.println(mConnected);
78        pw.print("      mLookaheadThreshold="); pw.print(mLookaheadThreshold);
79        pw.print(" ("); TimeUtils.formatDuration(mLookaheadThreshold, pw); pw.println(")");
80        pw.print("      mSubscriptions="); pw.println(mSubscriptions);
81        pw.print("      mRequesting="); pw.println(mRequesting);
82    }
83
84    @Override
85    public void onConnected() {
86        if (DEBUG) Slog.d(TAG, "onConnected");
87        mLookaheadThreshold = PropConfig.getInt(mContext, "nextalarm.condition.lookahead",
88                R.integer.config_next_alarm_condition_lookahead_threshold_hrs) * HOURS;
89        mConnected = true;
90        mTracker.addCallback(mTrackerCallback);
91    }
92
93    @Override
94    public void onDestroy() {
95        super.onDestroy();
96        if (DEBUG) Slog.d(TAG, "onDestroy");
97        mTracker.removeCallback(mTrackerCallback);
98        mConnected = false;
99    }
100
101    @Override
102    public void onRequestConditions(int relevance) {
103        if (DEBUG) Slog.d(TAG, "onRequestConditions relevance=" + relevance);
104        if (!mConnected) return;
105        mRequesting = (relevance & Condition.FLAG_RELEVANT_NOW) != 0;
106        mTracker.evaluate();
107    }
108
109    @Override
110    public void onSubscribe(Uri conditionId) {
111        if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
112        if (tryParseNextAlarmCondition(conditionId) == BAD_CONDITION) {
113            notifyCondition(conditionId, null, Condition.STATE_FALSE, "badCondition");
114            return;
115        }
116        mSubscriptions.add(conditionId);
117        mTracker.evaluate();
118    }
119
120    @Override
121    public void onUnsubscribe(Uri conditionId) {
122        if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId);
123        mSubscriptions.remove(conditionId);
124    }
125
126    public void attachBase(Context base) {
127        attachBaseContext(base);
128    }
129
130    public IConditionProvider asInterface() {
131        return (IConditionProvider) onBind(null);
132    }
133
134    private boolean isWithinLookaheadThreshold(AlarmClockInfo alarm) {
135        if (alarm == null) return false;
136        final long delta = NextAlarmTracker.getEarlyTriggerTime(alarm) - System.currentTimeMillis();
137        return delta > 0 && (mLookaheadThreshold <= 0 || delta < mLookaheadThreshold);
138    }
139
140    private void notifyCondition(Uri id, AlarmClockInfo alarm, int state, String reason) {
141        final String formattedAlarm = alarm == null ? "" : mTracker.formatAlarm(alarm);
142        if (DEBUG) Slog.d(TAG, "notifyCondition " + Condition.stateToString(state)
143                + " alarm=" + formattedAlarm + " reason=" + reason);
144        notifyCondition(new Condition(id,
145                mContext.getString(R.string.zen_mode_next_alarm_summary, formattedAlarm),
146                mContext.getString(R.string.zen_mode_next_alarm_line_one),
147                formattedAlarm, 0, state, Condition.FLAG_RELEVANT_NOW));
148    }
149
150    private Uri newConditionId(AlarmClockInfo nextAlarm) {
151        return new Uri.Builder().scheme(Condition.SCHEME)
152                .authority(ZenModeConfig.SYSTEM_AUTHORITY)
153                .appendPath(ZenModeConfig.NEXT_ALARM_PATH)
154                .appendPath(Integer.toString(mTracker.getCurrentUserId()))
155                .appendPath(Long.toString(nextAlarm.getTriggerTime()))
156                .build();
157    }
158
159    private long tryParseNextAlarmCondition(Uri conditionId) {
160        return conditionId != null && conditionId.getScheme().equals(Condition.SCHEME)
161                && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY)
162                && conditionId.getPathSegments().size() == 3
163                && conditionId.getPathSegments().get(0).equals(ZenModeConfig.NEXT_ALARM_PATH)
164                && conditionId.getPathSegments().get(1)
165                        .equals(Integer.toString(mTracker.getCurrentUserId()))
166                                ? tryParseLong(conditionId.getPathSegments().get(2), BAD_CONDITION)
167                                : BAD_CONDITION;
168    }
169
170    private static long tryParseLong(String value, long defValue) {
171        if (TextUtils.isEmpty(value)) return defValue;
172        try {
173            return Long.valueOf(value);
174        } catch (NumberFormatException e) {
175            return defValue;
176        }
177    }
178
179    private void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) {
180        final boolean withinThreshold = isWithinLookaheadThreshold(nextAlarm);
181        final long nextAlarmTime = nextAlarm != null ? nextAlarm.getTriggerTime() : 0;
182        if (DEBUG) Slog.d(TAG, "onEvaluate mSubscriptions=" + mSubscriptions
183                + " nextAlarmTime=" +  mTracker.formatAlarmDebug(nextAlarmTime)
184                + " nextAlarmWakeup=" + mTracker.formatAlarmDebug(wakeupTime)
185                + " withinThreshold=" + withinThreshold
186                + " booted=" + booted);
187
188        ArraySet<Uri> conditions = mSubscriptions;
189        if (mRequesting && nextAlarm != null && withinThreshold) {
190            final Uri id = newConditionId(nextAlarm);
191            if (!conditions.contains(id)) {
192                conditions = new ArraySet<Uri>(conditions);
193                conditions.add(id);
194            }
195        }
196        for (Uri conditionId : conditions) {
197            final long time = tryParseNextAlarmCondition(conditionId);
198            if (time == BAD_CONDITION) {
199                notifyCondition(conditionId, nextAlarm, Condition.STATE_FALSE, "badCondition");
200            } else if (!booted) {
201                // we don't know yet
202                if (mSubscriptions.contains(conditionId)) {
203                    notifyCondition(conditionId, nextAlarm, Condition.STATE_UNKNOWN, "!booted");
204                }
205            } else if (time != nextAlarmTime) {
206                // next alarm changed since subscription, consider obsolete
207                notifyCondition(conditionId, nextAlarm, Condition.STATE_FALSE, "changed");
208            } else if (!withinThreshold) {
209                // next alarm outside threshold or in the past, condition = false
210                notifyCondition(conditionId, nextAlarm, Condition.STATE_FALSE, "!within");
211            } else {
212                // next alarm within threshold and in the future, condition = true
213                notifyCondition(conditionId, nextAlarm, Condition.STATE_TRUE, "within");
214            }
215        }
216    }
217
218    private final NextAlarmTracker.Callback mTrackerCallback = new NextAlarmTracker.Callback() {
219        @Override
220        public void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) {
221            NextAlarmConditionProvider.this.onEvaluate(nextAlarm, wakeupTime, booted);
222        }
223    };
224}
225