blob: 052dbf39fc7b5a6530bdb2b7f0e6e27622593f19 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.deskclock.data;
import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED;
import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
import static android.content.Context.AUDIO_SERVICE;
import static android.content.Context.NOTIFICATION_SERVICE;
import static android.media.AudioManager.STREAM_ALARM;
import static android.media.RingtoneManager.TYPE_ALARM;
import static android.provider.Settings.System.CONTENT_URI;
import static android.provider.Settings.System.DEFAULT_ALARM_ALERT_URI;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.ContentObserver;
import android.media.AudioManager;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import androidx.core.app.NotificationManagerCompat;
import com.android.deskclock.data.DataModel.SilentSetting;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* This model fetches and stores reasons that alarms may be suppressed or silenced by system
* settings on the device. This information is displayed passively to notify the user of this
* condition and set their expectations for future firing alarms.
*/
final class SilentSettingsModel {
/** The Uri to the settings entry that stores alarm stream volume. */
private static final Uri VOLUME_URI = Uri.withAppendedPath(CONTENT_URI, "volume_alarm_speaker");
private final Context mContext;
/** Used to query the alarm volume and display the system control to change the alarm volume. */
private final AudioManager mAudioManager;
/** Used to query the do-not-disturb setting value, also called "interruption filter". */
private final NotificationManager mNotificationManager;
/** Used to determine if the application is in the foreground. */
private final NotificationModel mNotificationModel;
/** List of listeners to invoke upon silence state change. */
private final List<OnSilentSettingsListener> mListeners = new ArrayList<>(1);
/**
* The last setting known to be blocking alarms; {@code null} indicates no settings are
* blocking the app or the app is not in the foreground.
*/
private SilentSetting mSilentSetting;
/** The background task that checks the device system settings that influence alarm firing. */
private CheckSilenceSettingsTask mCheckSilenceSettingsTask;
SilentSettingsModel(Context context, NotificationModel notificationModel) {
mContext = context;
mNotificationModel = notificationModel;
mAudioManager = (AudioManager) context.getSystemService(AUDIO_SERVICE);
mNotificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
// Watch for changes to the settings that may silence alarms.
final ContentResolver cr = context.getContentResolver();
final ContentObserver contentChangeWatcher = new ContentChangeWatcher();
cr.registerContentObserver(VOLUME_URI, false, contentChangeWatcher);
cr.registerContentObserver(DEFAULT_ALARM_ALERT_URI, false, contentChangeWatcher);
final IntentFilter filter = new IntentFilter(ACTION_INTERRUPTION_FILTER_CHANGED);
context.registerReceiver(new DoNotDisturbChangeReceiver(), filter,
Context.RECEIVER_NOT_EXPORTED);
}
void addSilentSettingsListener(OnSilentSettingsListener listener) {
mListeners.add(listener);
}
void removeSilentSettingsListener(OnSilentSettingsListener listener) {
mListeners.remove(listener);
}
/**
* If the app is in the foreground, start a task to determine if any device setting will block
* alarms from firing. If the app is in the background, clear any results from the last time
* those settings were inspected.
*/
void updateSilentState() {
// Cancel any task in flight, the result is no longer relevant.
if (mCheckSilenceSettingsTask != null) {
mCheckSilenceSettingsTask.cancel();
mCheckSilenceSettingsTask = null;
}
if (mNotificationModel.isApplicationInForeground()) {
mCheckSilenceSettingsTask = new CheckSilenceSettingsTask();
mCheckSilenceSettingsTask.execute();
} else {
setSilentState(null);
}
}
/**
* @param silentSetting the latest notion of which setting is suppressing alarms; {@code null}
* if no settings are suppressing alarms
*/
private void setSilentState(SilentSetting silentSetting) {
if (mSilentSetting != silentSetting) {
mSilentSetting = silentSetting;
for (OnSilentSettingsListener listener : mListeners) {
listener.onSilentSettingsChange(silentSetting);
}
}
}
/**
* This task inspects a variety of system settings that can prevent alarms from firing or the
* associated ringtone from playing. If any of them would prevent an alarm from firing or
* making noise, a description of the setting is reported to this model on the main thread.
*/
private final class CheckSilenceSettingsTask {
final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
final Handler mHandler = new Handler(Looper.getMainLooper());
private void execute() {
mExecutor.execute(() -> {
final SilentSetting silentSetting;
if (isDoNotDisturbBlockingAlarms()) {
silentSetting = SilentSetting.DO_NOT_DISTURB;
} else if (isAlarmStreamMuted()) {
silentSetting = SilentSetting.MUTED_VOLUME;
} else if (isSystemAlarmRingtoneSilent()) {
silentSetting = SilentSetting.SILENT_RINGTONE;
} else if (isAppNotificationBlocked()) {
silentSetting = SilentSetting.BLOCKED_NOTIFICATIONS;
} else {
silentSetting = null;
}
mHandler.post(() -> {
if (mCheckSilenceSettingsTask == this) {
mCheckSilenceSettingsTask = null;
setSilentState(silentSetting);
}
});
});
}
private void cancel() {
mExecutor.shutdownNow();
}
private boolean isDoNotDisturbBlockingAlarms() {
try {
final int interruptionFilter = mNotificationManager.getCurrentInterruptionFilter();
return interruptionFilter == INTERRUPTION_FILTER_NONE;
} catch (Exception e) {
// Since this is purely informational, avoid crashing the app.
return false;
}
}
private boolean isAlarmStreamMuted() {
try {
return mAudioManager.getStreamVolume(STREAM_ALARM) <= 0;
} catch (Exception e) {
// Since this is purely informational, avoid crashing the app.
return false;
}
}
private boolean isSystemAlarmRingtoneSilent() {
try {
return RingtoneManager.getActualDefaultRingtoneUri(mContext, TYPE_ALARM) == null;
} catch (Exception e) {
// Since this is purely informational, avoid crashing the app.
return false;
}
}
private boolean isAppNotificationBlocked() {
try {
return !NotificationManagerCompat.from(mContext).areNotificationsEnabled();
} catch (Exception e) {
// Since this is purely informational, avoid crashing the app.
return false;
}
}
}
/**
* Observe changes to specific URI for settings that can silence firing alarms.
*/
private final class ContentChangeWatcher extends ContentObserver {
private ContentChangeWatcher() {
super(new Handler(Looper.myLooper()));
}
@Override
public void onChange(boolean selfChange) {
updateSilentState();
}
}
/**
* Observe changes to the do-not-disturb setting.
*/
private final class DoNotDisturbChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
updateSilentState();
}
}
}