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