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 }