Home | History | Annotate | Download | only in notification
      1 /*
      2  * Copyright (C) 2015 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 
     17 package com.android.server.notification;
     18 
     19 import android.app.ActivityManager;
     20 import android.app.AlarmManager;
     21 import android.app.PendingIntent;
     22 import android.content.BroadcastReceiver;
     23 import android.content.ComponentName;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.IntentFilter;
     27 import android.net.Uri;
     28 import android.os.Binder;
     29 import android.provider.Settings;
     30 import android.service.notification.Condition;
     31 import android.service.notification.IConditionProvider;
     32 import android.service.notification.ZenModeConfig;
     33 import android.service.notification.ZenModeConfig.ScheduleInfo;
     34 import android.text.TextUtils;
     35 import android.util.ArrayMap;
     36 import android.util.ArraySet;
     37 import android.util.Log;
     38 import android.util.Slog;
     39 
     40 import com.android.server.notification.NotificationManagerService.DumpFilter;
     41 
     42 import java.io.PrintWriter;
     43 import java.util.Calendar;
     44 import java.util.TimeZone;
     45 
     46 /**
     47  * Built-in zen condition provider for daily scheduled time-based conditions.
     48  */
     49 public class ScheduleConditionProvider extends SystemConditionProviderService {
     50     static final String TAG = "ConditionProviders.SCP";
     51     static final boolean DEBUG = true || Log.isLoggable("ConditionProviders", Log.DEBUG);
     52 
     53     public static final ComponentName COMPONENT =
     54             new ComponentName("android", ScheduleConditionProvider.class.getName());
     55     private static final String NOT_SHOWN = "...";
     56     private static final String SIMPLE_NAME = ScheduleConditionProvider.class.getSimpleName();
     57     private static final String ACTION_EVALUATE =  SIMPLE_NAME + ".EVALUATE";
     58     private static final int REQUEST_CODE_EVALUATE = 1;
     59     private static final String EXTRA_TIME = "time";
     60     private static final String SEPARATOR = ";";
     61     private static final String SCP_SETTING = "snoozed_schedule_condition_provider";
     62 
     63 
     64     private final Context mContext = this;
     65     private final ArrayMap<Uri, ScheduleCalendar> mSubscriptions = new ArrayMap<>();
     66     private ArraySet<Uri> mSnoozed = new ArraySet<>();
     67 
     68     private AlarmManager mAlarmManager;
     69     private boolean mConnected;
     70     private boolean mRegistered;
     71     private long mNextAlarmTime;
     72 
     73     public ScheduleConditionProvider() {
     74         if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
     75     }
     76 
     77     @Override
     78     public ComponentName getComponent() {
     79         return COMPONENT;
     80     }
     81 
     82     @Override
     83     public boolean isValidConditionId(Uri id) {
     84         return ZenModeConfig.isValidScheduleConditionId(id);
     85     }
     86 
     87     @Override
     88     public void dump(PrintWriter pw, DumpFilter filter) {
     89         pw.print("    "); pw.print(SIMPLE_NAME); pw.println(":");
     90         pw.print("      mConnected="); pw.println(mConnected);
     91         pw.print("      mRegistered="); pw.println(mRegistered);
     92         pw.println("      mSubscriptions=");
     93         final long now = System.currentTimeMillis();
     94         for (Uri conditionId : mSubscriptions.keySet()) {
     95             pw.print("        ");
     96             pw.print(meetsSchedule(mSubscriptions.get(conditionId), now) ? "* " : "  ");
     97             pw.println(conditionId);
     98             pw.print("            ");
     99             pw.println(mSubscriptions.get(conditionId).toString());
    100         }
    101         pw.println("      snoozed due to alarm: " + TextUtils.join(SEPARATOR, mSnoozed));
    102         dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, now);
    103     }
    104 
    105     @Override
    106     public void onConnected() {
    107         if (DEBUG) Slog.d(TAG, "onConnected");
    108         mConnected = true;
    109         readSnoozed();
    110     }
    111 
    112     @Override
    113     public void onBootComplete() {
    114         // noop
    115     }
    116 
    117     @Override
    118     public void onDestroy() {
    119         super.onDestroy();
    120         if (DEBUG) Slog.d(TAG, "onDestroy");
    121         mConnected = false;
    122     }
    123 
    124     @Override
    125     public void onSubscribe(Uri conditionId) {
    126         if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
    127         if (!ZenModeConfig.isValidScheduleConditionId(conditionId)) {
    128             notifyCondition(conditionId, Condition.STATE_FALSE, "badCondition");
    129             return;
    130         }
    131         mSubscriptions.put(conditionId, toScheduleCalendar(conditionId));
    132         evaluateSubscriptions();
    133     }
    134 
    135     @Override
    136     public void onUnsubscribe(Uri conditionId) {
    137         if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId);
    138         mSubscriptions.remove(conditionId);
    139         removeSnoozed(conditionId);
    140         evaluateSubscriptions();
    141     }
    142 
    143     @Override
    144     public void attachBase(Context base) {
    145         attachBaseContext(base);
    146     }
    147 
    148     @Override
    149     public IConditionProvider asInterface() {
    150         return (IConditionProvider) onBind(null);
    151     }
    152 
    153     private void evaluateSubscriptions() {
    154         if (mAlarmManager == null) {
    155             mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
    156         }
    157         setRegistered(!mSubscriptions.isEmpty());
    158         final long now = System.currentTimeMillis();
    159         mNextAlarmTime = 0;
    160         long nextUserAlarmTime = getNextAlarm();
    161         for (Uri conditionId : mSubscriptions.keySet()) {
    162             final ScheduleCalendar cal = mSubscriptions.get(conditionId);
    163             if (cal != null && cal.isInSchedule(now)) {
    164                 if (conditionSnoozed(conditionId) || cal.shouldExitForAlarm(now)) {
    165                     notifyCondition(conditionId, Condition.STATE_FALSE, "alarmCanceled");
    166                     addSnoozed(conditionId);
    167                 } else {
    168                     notifyCondition(conditionId, Condition.STATE_TRUE, "meetsSchedule");
    169                 }
    170                 cal.maybeSetNextAlarm(now, nextUserAlarmTime);
    171             } else {
    172                 notifyCondition(conditionId, Condition.STATE_FALSE, "!meetsSchedule");
    173                 removeSnoozed(conditionId);
    174                 if (nextUserAlarmTime == 0) {
    175                     cal.maybeSetNextAlarm(now, nextUserAlarmTime);
    176                 }
    177             }
    178             if (cal != null) {
    179                 final long nextChangeTime = cal.getNextChangeTime(now);
    180                 if (nextChangeTime > 0 && nextChangeTime > now) {
    181                     if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
    182                         mNextAlarmTime = nextChangeTime;
    183                     }
    184                 }
    185             }
    186         }
    187         updateAlarm(now, mNextAlarmTime);
    188     }
    189 
    190     private void updateAlarm(long now, long time) {
    191         final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
    192         final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
    193                 REQUEST_CODE_EVALUATE,
    194                 new Intent(ACTION_EVALUATE)
    195                         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
    196                         .putExtra(EXTRA_TIME, time),
    197                 PendingIntent.FLAG_UPDATE_CURRENT);
    198         alarms.cancel(pendingIntent);
    199         if (time > now) {
    200             if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s",
    201                     ts(time), formatDuration(time - now), ts(now)));
    202             alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
    203         } else {
    204             if (DEBUG) Slog.d(TAG, "Not scheduling evaluate");
    205         }
    206     }
    207 
    208     public long getNextAlarm() {
    209         final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock(
    210                 ActivityManager.getCurrentUser());
    211         return info != null ? info.getTriggerTime() : 0;
    212     }
    213 
    214     private boolean meetsSchedule(ScheduleCalendar cal, long time) {
    215         return cal != null && cal.isInSchedule(time);
    216     }
    217 
    218     private static ScheduleCalendar toScheduleCalendar(Uri conditionId) {
    219         final ScheduleInfo schedule = ZenModeConfig.tryParseScheduleConditionId(conditionId);
    220         if (schedule == null || schedule.days == null || schedule.days.length == 0) return null;
    221         final ScheduleCalendar sc = new ScheduleCalendar();
    222         sc.setSchedule(schedule);
    223         sc.setTimeZone(TimeZone.getDefault());
    224         return sc;
    225     }
    226 
    227     private void setRegistered(boolean registered) {
    228         if (mRegistered == registered) return;
    229         if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
    230         mRegistered = registered;
    231         if (mRegistered) {
    232             final IntentFilter filter = new IntentFilter();
    233             filter.addAction(Intent.ACTION_TIME_CHANGED);
    234             filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
    235             filter.addAction(ACTION_EVALUATE);
    236             filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
    237             registerReceiver(mReceiver, filter);
    238         } else {
    239             unregisterReceiver(mReceiver);
    240         }
    241     }
    242 
    243     private void notifyCondition(Uri conditionId, int state, String reason) {
    244         if (DEBUG) Slog.d(TAG, "notifyCondition " + conditionId
    245                 + " " + Condition.stateToString(state)
    246                 + " reason=" + reason);
    247         notifyCondition(createCondition(conditionId, state));
    248     }
    249 
    250     private Condition createCondition(Uri id, int state) {
    251         final String summary = NOT_SHOWN;
    252         final String line1 = NOT_SHOWN;
    253         final String line2 = NOT_SHOWN;
    254         return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS);
    255     }
    256 
    257     private boolean conditionSnoozed(Uri conditionId) {
    258         synchronized (mSnoozed) {
    259             return mSnoozed.contains(conditionId);
    260         }
    261     }
    262 
    263     private void addSnoozed(Uri conditionId) {
    264         synchronized (mSnoozed) {
    265             mSnoozed.add(conditionId);
    266             saveSnoozedLocked();
    267         }
    268     }
    269 
    270     private void removeSnoozed(Uri conditionId) {
    271         synchronized (mSnoozed) {
    272             mSnoozed.remove(conditionId);
    273             saveSnoozedLocked();
    274         }
    275     }
    276 
    277     public void saveSnoozedLocked() {
    278         final String setting = TextUtils.join(SEPARATOR, mSnoozed);
    279         final int currentUser = ActivityManager.getCurrentUser();
    280         Settings.Secure.putStringForUser(mContext.getContentResolver(),
    281                 SCP_SETTING,
    282                 setting,
    283                 currentUser);
    284     }
    285 
    286     public void readSnoozed() {
    287         synchronized (mSnoozed) {
    288             long identity = Binder.clearCallingIdentity();
    289             try {
    290                 final String setting = Settings.Secure.getStringForUser(
    291                         mContext.getContentResolver(),
    292                         SCP_SETTING,
    293                         ActivityManager.getCurrentUser());
    294                 if (setting != null) {
    295                     final String[] tokens = setting.split(SEPARATOR);
    296                     for (int i = 0; i < tokens.length; i++) {
    297                         String token = tokens[i];
    298                         if (token != null) {
    299                             token = token.trim();
    300                         }
    301                         if (TextUtils.isEmpty(token)) {
    302                             continue;
    303                         }
    304                         mSnoozed.add(Uri.parse(token));
    305                     }
    306                 }
    307             } finally {
    308                 Binder.restoreCallingIdentity(identity);
    309             }
    310         }
    311     }
    312 
    313     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
    314         @Override
    315         public void onReceive(Context context, Intent intent) {
    316             if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
    317             if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
    318                 for (Uri conditionId : mSubscriptions.keySet()) {
    319                     final ScheduleCalendar cal = mSubscriptions.get(conditionId);
    320                     if (cal != null) {
    321                         cal.setTimeZone(Calendar.getInstance().getTimeZone());
    322                     }
    323                 }
    324             }
    325             evaluateSubscriptions();
    326         }
    327     };
    328 
    329 }
    330