1 /*
2  * Copyright (C) 2016 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.deskclock.data;
18 
19 import android.annotation.TargetApi;
20 import android.app.NotificationManager;
21 import android.content.BroadcastReceiver;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.database.ContentObserver;
27 import android.media.AudioManager;
28 import android.media.RingtoneManager;
29 import android.net.Uri;
30 import android.os.AsyncTask;
31 import android.os.Build;
32 import android.os.Handler;
33 import android.support.v4.app.NotificationManagerCompat;
34 
35 import com.android.deskclock.Utils;
36 import com.android.deskclock.data.DataModel.SilentSetting;
37 
38 import java.util.ArrayList;
39 import java.util.List;
40 
41 import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED;
42 import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
43 import static android.content.Context.AUDIO_SERVICE;
44 import static android.content.Context.NOTIFICATION_SERVICE;
45 import static android.media.AudioManager.STREAM_ALARM;
46 import static android.media.RingtoneManager.TYPE_ALARM;
47 import static android.provider.Settings.System.CONTENT_URI;
48 import static android.provider.Settings.System.DEFAULT_ALARM_ALERT_URI;
49 
50 /**
51  * This model fetches and stores reasons that alarms may be suppressed or silenced by system
52  * settings on the device. This information is displayed passively to notify the user of this
53  * condition and set their expectations for future firing alarms.
54  */
55 final class SilentSettingsModel {
56 
57     /** The Uri to the settings entry that stores alarm stream volume. */
58     private static final Uri VOLUME_URI = Uri.withAppendedPath(CONTENT_URI, "volume_alarm_speaker");
59 
60     private final Context mContext;
61 
62     /** Used to query the alarm volume and display the system control to change the alarm volume. */
63     private final AudioManager mAudioManager;
64 
65     /** Used to query the do-not-disturb setting value, also called "interruption filter". */
66     private final NotificationManager mNotificationManager;
67 
68     /** Used to determine if the application is in the foreground. */
69     private final NotificationModel mNotificationModel;
70 
71     /** List of listeners to invoke upon silence state change. */
72     private final List<OnSilentSettingsListener> mListeners = new ArrayList<>(1);
73 
74     /**
75      * The last setting known to be blocking alarms; {@code null} indicates no settings are
76      * blocking the app or the app is not in the foreground.
77      */
78     private SilentSetting mSilentSetting;
79 
80     /** The background task that checks the device system settings that influence alarm firing. */
81     private CheckSilenceSettingsTask mCheckSilenceSettingsTask;
82 
SilentSettingsModel(Context context, NotificationModel notificationModel)83     SilentSettingsModel(Context context, NotificationModel notificationModel) {
84         mContext = context;
85         mNotificationModel = notificationModel;
86 
87         mAudioManager = (AudioManager) context.getSystemService(AUDIO_SERVICE);
88         mNotificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
89 
90         // Watch for changes to the settings that may silence alarms.
91         final ContentResolver cr = context.getContentResolver();
92         final ContentObserver contentChangeWatcher = new ContentChangeWatcher();
93         cr.registerContentObserver(VOLUME_URI, false, contentChangeWatcher);
94         cr.registerContentObserver(DEFAULT_ALARM_ALERT_URI, false, contentChangeWatcher);
95         if (Utils.isMOrLater()) {
96             final IntentFilter filter = new IntentFilter(ACTION_INTERRUPTION_FILTER_CHANGED);
97             context.registerReceiver(new DoNotDisturbChangeReceiver(), filter);
98         }
99     }
100 
addSilentSettingsListener(OnSilentSettingsListener listener)101     void addSilentSettingsListener(OnSilentSettingsListener listener) {
102         mListeners.add(listener);
103     }
104 
removeSilentSettingsListener(OnSilentSettingsListener listener)105     void removeSilentSettingsListener(OnSilentSettingsListener listener) {
106         mListeners.remove(listener);
107     }
108 
109     /**
110      * If the app is in the foreground, start a task to determine if any device setting will block
111      * alarms from firing. If the app is in the background, clear any results from the last time
112      * those settings were inspected.
113      */
updateSilentState()114     void updateSilentState() {
115         // Cancel any task in flight, the result is no longer relevant.
116         if (mCheckSilenceSettingsTask != null) {
117             mCheckSilenceSettingsTask.cancel(true);
118             mCheckSilenceSettingsTask = null;
119         }
120 
121         if (mNotificationModel.isApplicationInForeground()) {
122             mCheckSilenceSettingsTask = new CheckSilenceSettingsTask();
123             mCheckSilenceSettingsTask.execute();
124         } else {
125             setSilentState(null);
126         }
127     }
128 
129     /**
130      * @param silentSetting the latest notion of which setting is suppressing alarms; {@code null}
131      *      if no settings are suppressing alarms
132      */
setSilentState(SilentSetting silentSetting)133     private void setSilentState(SilentSetting silentSetting) {
134         if (mSilentSetting != silentSetting) {
135             final SilentSetting oldReason = mSilentSetting;
136             mSilentSetting = silentSetting;
137 
138             for (OnSilentSettingsListener listener : mListeners) {
139                 listener.onSilentSettingsChange(oldReason, silentSetting);
140             }
141         }
142     }
143 
144     /**
145      * This task inspects a variety of system settings that can prevent alarms from firing or the
146      * associated ringtone from playing. If any of them would prevent an alarm from firing or
147      * making noise, a description of the setting is reported to this model on the main thread.
148      */
149     private final class CheckSilenceSettingsTask extends AsyncTask<Void, Void, SilentSetting> {
150         @Override
doInBackground(Void... parameters)151         protected SilentSetting doInBackground(Void... parameters) {
152             if (!isCancelled() && isDoNotDisturbBlockingAlarms()) {
153                 return SilentSetting.DO_NOT_DISTURB;
154             } else if (!isCancelled() && isAlarmStreamMuted()) {
155                 return SilentSetting.MUTED_VOLUME;
156             } else if (!isCancelled() && isSystemAlarmRingtoneSilent()) {
157                 return SilentSetting.SILENT_RINGTONE;
158             } else if (!isCancelled() && isAppNotificationBlocked()) {
159                 return SilentSetting.BLOCKED_NOTIFICATIONS;
160             }
161             return null;
162         }
163 
164         @Override
onCancelled()165         protected void onCancelled() {
166             super.onCancelled();
167             if (mCheckSilenceSettingsTask == this) {
168                 mCheckSilenceSettingsTask = null;
169             }
170         }
171 
172         @Override
onPostExecute(SilentSetting silentSetting)173         protected void onPostExecute(SilentSetting silentSetting) {
174             if (mCheckSilenceSettingsTask == this) {
175                 mCheckSilenceSettingsTask = null;
176                 setSilentState(silentSetting);
177             }
178         }
179 
180         @TargetApi(Build.VERSION_CODES.M)
isDoNotDisturbBlockingAlarms()181         private boolean isDoNotDisturbBlockingAlarms() {
182             if (!Utils.isMOrLater()) {
183                 return false;
184             }
185 
186             try {
187                 final int interruptionFilter = mNotificationManager.getCurrentInterruptionFilter();
188                 return interruptionFilter == INTERRUPTION_FILTER_NONE;
189             } catch (Exception e) {
190                 // Since this is purely informational, avoid crashing the app.
191                 return false;
192             }
193         }
194 
isAlarmStreamMuted()195         private boolean isAlarmStreamMuted() {
196             try {
197                 return mAudioManager.getStreamVolume(STREAM_ALARM) <= 0;
198             } catch (Exception e) {
199                 // Since this is purely informational, avoid crashing the app.
200                 return false;
201             }
202         }
203 
isSystemAlarmRingtoneSilent()204         private boolean isSystemAlarmRingtoneSilent() {
205             try {
206                 return RingtoneManager.getActualDefaultRingtoneUri(mContext, TYPE_ALARM) == null;
207             } catch (Exception e) {
208                 // Since this is purely informational, avoid crashing the app.
209                 return false;
210             }
211         }
212 
isAppNotificationBlocked()213         private boolean isAppNotificationBlocked() {
214             try {
215                 return !NotificationManagerCompat.from(mContext).areNotificationsEnabled();
216             } catch (Exception e) {
217                 // Since this is purely informational, avoid crashing the app.
218                 return false;
219             }
220         }
221     }
222 
223     /**
224      * Observe changes to specific URI for settings that can silence firing alarms.
225      */
226     private final class ContentChangeWatcher extends ContentObserver {
ContentChangeWatcher()227         private ContentChangeWatcher() {
228             super(new Handler());
229         }
230 
231         @Override
onChange(boolean selfChange)232         public void onChange(boolean selfChange) {
233             updateSilentState();
234         }
235     }
236 
237     /**
238      * Observe changes to the do-not-disturb setting.
239      */
240     private final class DoNotDisturbChangeReceiver extends BroadcastReceiver {
241         @Override
onReceive(Context context, Intent intent)242         public void onReceive(Context context, Intent intent) {
243             updateSilentState();
244         }
245     }
246 }