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.ScheduleCalendar;
     33 import android.service.notification.ZenModeConfig;
     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.internal.annotations.GuardedBy;
     41 import com.android.internal.annotations.VisibleForTesting;
     42 import com.android.server.notification.NotificationManagerService.DumpFilter;
     43 
     44 import java.io.PrintWriter;
     45 import java.util.ArrayList;
     46 import java.util.Calendar;
     47 import java.util.List;
     48 
     49 /**
     50  * Built-in zen condition provider for daily scheduled time-based conditions.
     51  */
     52 public class ScheduleConditionProvider extends SystemConditionProviderService {
     53     static final String TAG = "ConditionProviders.SCP";
     54     static final boolean DEBUG = true || Log.isLoggable("ConditionProviders", Log.DEBUG);
     55 
     56     public static final ComponentName COMPONENT =
     57             new ComponentName("android", ScheduleConditionProvider.class.getName());
     58     private static final String NOT_SHOWN = "...";
     59     private static final String SIMPLE_NAME = ScheduleConditionProvider.class.getSimpleName();
     60     private static final String ACTION_EVALUATE =  SIMPLE_NAME + ".EVALUATE";
     61     private static final int REQUEST_CODE_EVALUATE = 1;
     62     private static final String EXTRA_TIME = "time";
     63     private static final String SEPARATOR = ";";
     64     private static final String SCP_SETTING = "snoozed_schedule_condition_provider";
     65 
     66     private final Context mContext = this;
     67     private final ArrayMap<Uri, ScheduleCalendar> mSubscriptions = new ArrayMap<>();
     68     private ArraySet<Uri> mSnoozedForAlarm = 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, mSnoozedForAlarm));
    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_ERROR, "invalidId"));
    133             return;
    134         }
    135         synchronized (mSubscriptions) {
    136             mSubscriptions.put(conditionId, ZenModeConfig.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                 Condition condition =
    173                         evaluateSubscriptionLocked(conditionId, mSubscriptions.get(conditionId),
    174                                 now, nextUserAlarmTime);
    175                 if (condition != null) {
    176                     conditionsToNotify.add(condition);
    177                 }
    178             }
    179         }
    180         notifyConditions(conditionsToNotify.toArray(new Condition[conditionsToNotify.size()]));
    181         updateAlarm(now, mNextAlarmTime);
    182     }
    183 
    184     @VisibleForTesting
    185     @GuardedBy("mSubscriptions")
    186     Condition evaluateSubscriptionLocked(Uri conditionId, ScheduleCalendar cal,
    187             long now, long nextUserAlarmTime) {
    188         if (DEBUG) Slog.d(TAG, String.format("evaluateSubscriptionLocked cal=%s, now=%s, "
    189                         + "nextUserAlarmTime=%s", cal, ts(now), ts(nextUserAlarmTime)));
    190         Condition condition;
    191         if (cal == null) {
    192             condition = createCondition(conditionId, Condition.STATE_ERROR, "!invalidId");
    193             removeSnoozed(conditionId);
    194             return condition;
    195         }
    196         if (cal.isInSchedule(now)) {
    197             if (conditionSnoozed(conditionId)) {
    198                 condition = createCondition(conditionId, Condition.STATE_FALSE, "snoozed");
    199             } else if (cal.shouldExitForAlarm(now)) {
    200                 condition = createCondition(conditionId, Condition.STATE_FALSE, "alarmCanceled");
    201                 addSnoozed(conditionId);
    202             } else {
    203                 condition = createCondition(conditionId, Condition.STATE_TRUE, "meetsSchedule");
    204             }
    205         } else {
    206             condition = createCondition(conditionId, Condition.STATE_FALSE, "!meetsSchedule");
    207             removeSnoozed(conditionId);
    208         }
    209         cal.maybeSetNextAlarm(now, nextUserAlarmTime);
    210         final long nextChangeTime = cal.getNextChangeTime(now);
    211         if (nextChangeTime > 0 && nextChangeTime > now) {
    212             if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
    213                 mNextAlarmTime = nextChangeTime;
    214             }
    215         }
    216         return condition;
    217     }
    218 
    219     private void updateAlarm(long now, long time) {
    220         final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
    221         final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
    222                 REQUEST_CODE_EVALUATE,
    223                 new Intent(ACTION_EVALUATE)
    224                         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
    225                         .putExtra(EXTRA_TIME, time),
    226                 PendingIntent.FLAG_UPDATE_CURRENT);
    227         alarms.cancel(pendingIntent);
    228         if (time > now) {
    229             if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s",
    230                     ts(time), formatDuration(time - now), ts(now)));
    231             alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
    232         } else {
    233             if (DEBUG) Slog.d(TAG, "Not scheduling evaluate");
    234         }
    235     }
    236 
    237     public long getNextAlarm() {
    238         final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock(
    239                 ActivityManager.getCurrentUser());
    240         return info != null ? info.getTriggerTime() : 0;
    241     }
    242 
    243     private boolean meetsSchedule(ScheduleCalendar cal, long time) {
    244         return cal != null && cal.isInSchedule(time);
    245     }
    246 
    247     private void setRegistered(boolean registered) {
    248         if (mRegistered == registered) return;
    249         if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
    250         mRegistered = registered;
    251         if (mRegistered) {
    252             final IntentFilter filter = new IntentFilter();
    253             filter.addAction(Intent.ACTION_TIME_CHANGED);
    254             filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
    255             filter.addAction(ACTION_EVALUATE);
    256             filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
    257             registerReceiver(mReceiver, filter);
    258         } else {
    259             unregisterReceiver(mReceiver);
    260         }
    261     }
    262 
    263     private Condition createCondition(Uri id, int state, String reason) {
    264         if (DEBUG) Slog.d(TAG, "notifyCondition " + id
    265                 + " " + Condition.stateToString(state)
    266                 + " reason=" + reason);
    267         final String summary = NOT_SHOWN;
    268         final String line1 = NOT_SHOWN;
    269         final String line2 = NOT_SHOWN;
    270         return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS);
    271     }
    272 
    273     private boolean conditionSnoozed(Uri conditionId) {
    274         synchronized (mSnoozedForAlarm) {
    275             return mSnoozedForAlarm.contains(conditionId);
    276         }
    277     }
    278 
    279     @VisibleForTesting
    280     void addSnoozed(Uri conditionId) {
    281         synchronized (mSnoozedForAlarm) {
    282             mSnoozedForAlarm.add(conditionId);
    283             saveSnoozedLocked();
    284         }
    285     }
    286 
    287     private void removeSnoozed(Uri conditionId) {
    288         synchronized (mSnoozedForAlarm) {
    289             mSnoozedForAlarm.remove(conditionId);
    290             saveSnoozedLocked();
    291         }
    292     }
    293 
    294     private void saveSnoozedLocked() {
    295         final String setting = TextUtils.join(SEPARATOR, mSnoozedForAlarm);
    296         final int currentUser = ActivityManager.getCurrentUser();
    297         Settings.Secure.putStringForUser(mContext.getContentResolver(),
    298                 SCP_SETTING,
    299                 setting,
    300                 currentUser);
    301     }
    302 
    303     private void readSnoozed() {
    304         synchronized (mSnoozedForAlarm) {
    305             long identity = Binder.clearCallingIdentity();
    306             try {
    307                 final String setting = Settings.Secure.getStringForUser(
    308                         mContext.getContentResolver(),
    309                         SCP_SETTING,
    310                         ActivityManager.getCurrentUser());
    311                 if (setting != null) {
    312                     final String[] tokens = setting.split(SEPARATOR);
    313                     for (int i = 0; i < tokens.length; i++) {
    314                         String token = tokens[i];
    315                         if (token != null) {
    316                             token = token.trim();
    317                         }
    318                         if (TextUtils.isEmpty(token)) {
    319                             continue;
    320                         }
    321                         mSnoozedForAlarm.add(Uri.parse(token));
    322                     }
    323                 }
    324             } finally {
    325                 Binder.restoreCallingIdentity(identity);
    326             }
    327         }
    328     }
    329 
    330     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
    331         @Override
    332         public void onReceive(Context context, Intent intent) {
    333             if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
    334             if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
    335                 synchronized (mSubscriptions) {
    336                     for (Uri conditionId : mSubscriptions.keySet()) {
    337                         final ScheduleCalendar cal = mSubscriptions.get(conditionId);
    338                         if (cal != null) {
    339                             cal.setTimeZone(Calendar.getInstance().getTimeZone());
    340                         }
    341                     }
    342                 }
    343             }
    344             evaluateSubscriptions();
    345         }
    346     };
    347 
    348 }
    349