Java tutorial
/* * MindBell - Aims to give you a support for staying mindful in a busy life - * for remembering what really counts * * Copyright (C) 2010-2014 Marc Schroeder * Copyright (C) 2014-2018 Uwe Damken * * 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.googlecode.mindbell.accessors; import android.Manifest; import android.app.AlarmManager; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.Uri; import android.os.Build; import android.os.Vibrator; import android.provider.Settings; import android.provider.Settings.Global; import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; import android.support.v4.content.ContextCompat; import android.telephony.TelephonyManager; import android.util.Log; import android.widget.Toast; import com.googlecode.mindbell.MindBell; import com.googlecode.mindbell.MindBellMain; import com.googlecode.mindbell.MindBellPreferences; import com.googlecode.mindbell.MuteActivity; import com.googlecode.mindbell.R; import com.googlecode.mindbell.Scheduler; import com.googlecode.mindbell.logic.SchedulerLogic; import com.googlecode.mindbell.util.AlarmManagerCompat; import com.googlecode.mindbell.util.TimeOfDay; import java.io.IOException; import java.text.MessageFormat; import java.util.Calendar; import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT; import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE; import static android.media.AudioManager.AUDIOFOCUS_LOSS; import static android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT; import static android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK; import static android.media.AudioManager.AUDIOFOCUS_REQUEST_FAILED; import static com.googlecode.mindbell.MindBellPreferences.TAG; import static com.googlecode.mindbell.accessors.PrefsAccessor.EXTRA_IS_RESCHEDULING; import static com.googlecode.mindbell.accessors.PrefsAccessor.EXTRA_MEDITATION_PERIOD; import static com.googlecode.mindbell.accessors.PrefsAccessor.EXTRA_NOW_TIME_MILLIS; public class ContextAccessor implements AudioManager.OnAudioFocusChangeListener { public static final float MINUS_ONE_DB = 0.891250938f; public static final float MINUS_THREE_DB = 0.707945784f; private static final int STATUS_NOTIFICATION_ID = 0x7f030001; // historically, has been R.layout.bell for a long time private static final int RING_NOTIFICATION_ID = STATUS_NOTIFICATION_ID + 1; private static final int SCHEDULER_REQUEST_CODE = 0; private static final int UPDATE_STATUS_NOTIFICATION_REQUEST_CODE = 1; private static final int UPDATE_STATUS_NOTIFICATION_MUTED_TILL_REQUEST_CODE = 2; private static final int UPDATE_STATUS_NOTIFICATION_START_REQUEST_CODE = 3; private static final int UPDATE_STATUS_NOTIFICATION_DAY_NIGHT_REQUEST_CODE = 4; // Keep MediaPlayer to finish a started sound explicitly, reclaimed when app gets destroyed: http://stackoverflow.com/a/2476171 private static MediaPlayer mediaPlayer = null; private static AudioManager audioManager = null; // ApplicationContext of MindBell private final Context context; // Accessor to all preferences protected PrefsAccessor prefs = null; /** * Constructor is private just in case we want to make this a singleton. */ private ContextAccessor(Context context, boolean logSettings) { this.context = context.getApplicationContext(); this.prefs = new PrefsAccessor(context, logSettings); } /** * Constructor is protected to allow for JUnit tests only. */ protected ContextAccessor(Context context, PrefsAccessor prefs) { this.context = context; this.prefs = prefs; } /** * Returns an accessor for the given context, this call also validates the preferences. */ public static ContextAccessor getInstance(Context context) { return new ContextAccessor(context, false); } /** * Returns an accessor for the given context, this call also validates the preferences. */ public static ContextAccessor getInstanceAndLogPreferences(Context context) { return new ContextAccessor(context, true); } public PrefsAccessor getPrefs() { return prefs; } /** * Return whether bell should be muted and show reason message if shouldShowMessage is true. */ public boolean isMuteRequested(boolean shouldShowMessage) { // FIXME dkn Always called with true return getMuteRequestReason(shouldShowMessage) != null; } /** * Check whether bell should be muted, show reason if requested, and return reason, null otherwise. */ public String getMuteRequestReason(boolean shouldShowMessage) { String reason = null; if (System.currentTimeMillis() < prefs.getMutedTill()) { // Muted manually? reason = getReasonMutedTill(); } else if (prefs.isMuteWithPhone() && isPhoneMuted()) { // Mute bell with phone? reason = getReasonMutedWithPhone(); } else if (prefs.isMuteWithAudioStream() && isAudioStreamMuted()) { // Mute bell with audio stream? reason = getReasonMutedWithAudioStream(); } else if (prefs.isMuteOffHook() && isPhoneOffHook()) { // Mute bell while phone is off hook (or ringing)? reason = getReasonMutedOffHook(); } else if (prefs.isMuteInFlightMode() && isPhoneInFlightMode()) { // Mute bell while in flight mode? reason = getReasonMutedInFlightMode(); } else if (!new TimeOfDay().isDaytime(prefs)) { // Always mute bell during nighttime reason = getReasonMutedDuringNighttime(); } if (reason != null && shouldShowMessage) { showMessage(reason); } return reason; } protected String getReasonMutedTill() { TimeOfDay mutedTill = new TimeOfDay(prefs.getMutedTill()); return MessageFormat.format(context.getText(R.string.reasonMutedTill).toString(), mutedTill.getDisplayString(context)); } public boolean isPhoneMuted() { final AudioManager audioMan = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); return audioMan.getStreamVolume(AudioManager.STREAM_RING) == 0; } protected String getReasonMutedWithPhone() { return context.getText(R.string.reasonMutedWithPhone).toString(); } public boolean isAudioStreamMuted() { final AudioManager audioMan = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); return audioMan.getStreamVolume(prefs.getAudioStream()) == 0; } protected String getReasonMutedWithAudioStream() { return context.getText(R.string.reasonMutedWithAudioStream).toString(); } public boolean isPhoneOffHook() { TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); return telephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE; } protected String getReasonMutedOffHook() { return context.getText(R.string.reasonMutedOffHook).toString(); } public boolean isPhoneInFlightMode() { return Settings.System.getInt(context.getContentResolver(), retrieveAirplaneModeOnConstantName(), 0) == 1; } protected String getReasonMutedInFlightMode() { return context.getText(R.string.reasonMutedInFlightMode).toString(); } protected String getReasonMutedDuringNighttime() { TimeOfDay nextStartTime = new TimeOfDay(SchedulerLogic.getNextDaytimeStartInMillis( Calendar.getInstance().getTimeInMillis(), prefs.getDaytimeStart(), prefs.getActiveOnDaysOfWeek())); String weekdayAbbreviation = prefs.getWeekdayAbbreviation(nextStartTime.getWeekday()); return MessageFormat.format(context.getText(R.string.reasonMutedDuringNighttime).toString(), weekdayAbbreviation, nextStartTime.getDisplayString(context)); } public void showMessage(String message) { MindBell.logDebug(message); Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } /** * Returns name of the airplane-mode-on constant depending on the version of Android. */ @NonNull private String retrieveAirplaneModeOnConstantName() { if (Build.VERSION.SDK_INT >= 17) { return Global.AIRPLANE_MODE_ON; } else { return Settings.System.AIRPLANE_MODE_ON; } } public void startPlayingSoundAndVibrate(final ActivityPrefsAccessor activityPrefs, final Runnable runWhenDone) { // Stop an already ongoing sound, this isn't wrong when phone and bell are muted, too finishBellSound(); // Update ring notification and vibrate on either phone or wearable if (activityPrefs.isNotification()) { updateRingNotification(activityPrefs); } // Raise alarm volume to the max but keep the original volume for reset by finishBellSound() and start playing sound if // requested by preferences boolean playingSoundStarted = false; if (activityPrefs.isSound()) { playingSoundStarted = startPlayingSound(activityPrefs, runWhenDone); } // Explicitly start vibration if not already done by ring notification if (activityPrefs.isVibrate() && !activityPrefs.isNotification()) { startVibration(); } // If ring notification and its dismissal is requested, then we have to wait for a while to dismiss the ring notification // afterwards. So a new thread is created that waits and dismisses the ring notification afterwards. if (activityPrefs.isNotification() && activityPrefs.isDismissNotification()) { startWaiting(new Runnable() { @Override public void run() { cancelRingNotification(activityPrefs); } }); } // A non-null runWhenDone means there is something to do at the end (hiding the bell after displaying or stopping the // meditation automatically). This is typically done when finishing playing the sound. But if playing a sound has not // been started because of preferences or because sound has been suppressed then we after to do it now - or after a // little while if the bell will be displayed by MindBell.onStart() after leaving this method. if (!playingSoundStarted && runWhenDone != null) { if (activityPrefs.isShow()) { startWaiting(runWhenDone); } else { runWhenDone.run(); } } } public void finishBellSound() { if (isBellSoundPlaying()) { // do we hold a reference to a MediaPlayer? if (mediaPlayer.isPlaying()) { mediaPlayer.stop(); MindBell.logDebug("Ongoing MediaPlayer stopped"); } mediaPlayer.reset(); // get rid of "mediaplayer went away with unhandled events" log entries mediaPlayer.release(); mediaPlayer = null; MindBell.logDebug("Reference to MediaPlayer released"); if (prefs.isPauseAudioOnSound()) { if (audioManager.abandonAudioFocus(this) == AUDIOFOCUS_REQUEST_FAILED) { MindBell.logDebug("Abandon of audio focus failed"); } else { MindBell.logDebug("Audio focus successfully abandoned"); } } } // Reset volume to originalVolume if it has been set before (does not equal -1) if (prefs.isUseAudioStreamVolumeSetting()) { // we don't care about setting the volume MindBell.logDebug("Finish bell sound found without touching audio stream volume"); } else { int originalVolume = prefs.getOriginalVolume(); if (originalVolume < 0) { MindBell.logDebug("Finish bell sound found originalVolume " + originalVolume + ", alarm volume left untouched"); } else { int alarmMaxVolume = getAlarmMaxVolume(); if (originalVolume == alarmMaxVolume) { // "someone" else set it to max, so we don't touch it MindBell.logDebug("Finish bell sound found originalVolume " + originalVolume + " to be max, alarm volume left untouched"); } else { MindBell.logDebug("Finish bell sound found originalVolume " + originalVolume + ", setting alarm volume to it"); setAlarmVolume(originalVolume); } prefs.resetOriginalVolume(); // no longer needed therefore invalidate it } } } /** * This is about updating the ring notification when ringing the bell. */ public void updateRingNotification(ActivityPrefsAccessor activityPrefs) { int visibility = (prefs.isNotificationVisibilityPublic()) ? NotificationCompat.VISIBILITY_PUBLIC : NotificationCompat.VISIBILITY_PRIVATE; NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( context.getApplicationContext()) // .setCategory(NotificationCompat.CATEGORY_ALARM) // .setAutoCancel(true) // cancel notification on touch .setColor(context.getResources().getColor(R.color.backgroundColor)) // .setContentTitle(prefs.getNotificationTitle()) // .setContentText(prefs.getNotificationText()).setSmallIcon(R.drawable.ic_stat_bell_ring) // .setVisibility(visibility); if (activityPrefs.isVibrate()) { notificationBuilder.setVibrate(prefs.getVibrationPattern()); } Notification notification = notificationBuilder.build(); NotificationManagerCompat.from(context).notify(RING_NOTIFICATION_ID, notification); } /** * Start playing bell sound and call runWhenDone when playing finishes but only if bell is not muted - returns true when * sound has been started, false otherwise. */ private boolean startPlayingSound(ActivityPrefsAccessor activityPrefs, final Runnable runWhenDone) { Uri bellUri = activityPrefs.getSoundUri(context); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); if (prefs.isNoSoundOnMusic() && audioManager.isMusicActive()) { MindBell.logDebug("Sound suppressed because setting is no sound on music and music is playing"); return false; } else if (bellUri == null) { MindBell.logDebug("Sound suppressed because no sound has been set"); return false; } else if (prefs.isPauseAudioOnSound()) { int requestResult = audioManager.requestAudioFocus(this, prefs.getAudioStream(), retrieveDurationHint()); if (requestResult == AUDIOFOCUS_REQUEST_FAILED) { MindBell.logDebug( "Sound suppressed because setting is pause audio on sound and request of audio focus failed"); return false; } MindBell.logDebug("Audio focus successfully requested"); } if (prefs.isUseAudioStreamVolumeSetting()) { // we don't care about setting the volume MindBell.logDebug("Start playing sound without touching audio stream volume"); } else { int originalVolume = getAlarmVolume(); int alarmMaxVolume = getAlarmMaxVolume(); if (originalVolume == alarmMaxVolume) { // "someone" else set it to max, so we don't touch it MindBell.logDebug("Start playing sound found originalVolume " + originalVolume + " to be max, alarm volume left untouched"); } else { MindBell.logDebug("Start playing sound found and stored originalVolume " + originalVolume + ", setting alarm volume to max"); setAlarmVolume(alarmMaxVolume); prefs.setOriginalVolume(originalVolume); } } mediaPlayer = new MediaPlayer(); mediaPlayer.setAudioStreamType(prefs.getAudioStream()); if (!prefs.isUseAudioStreamVolumeSetting()) { // care about setting the volume float bellVolume = activityPrefs.getVolume(); mediaPlayer.setVolume(bellVolume, bellVolume); } try { try { mediaPlayer.setDataSource(context, bellUri); } catch (IOException e) { // probably because of withdrawn permissions, hence use default bell mediaPlayer.setDataSource(context, prefs.getDefaultReminderBellSoundUri(context)); } mediaPlayer.prepare(); mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { public void onCompletion(MediaPlayer mp) { finishBellSound(); if (runWhenDone != null) { runWhenDone.run(); } } }); mediaPlayer.start(); return true; } catch (IOException e) { Log.e(TAG, "Could not start playing sound: " + e.getMessage(), e); finishBellSound(); return false; } } /** * Vibrate with the requested vibration pattern. */ private void startVibration() { Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); vibrator.vibrate(prefs.getVibrationPattern(), -1); } /** * Start waiting for a specific time period and call runWhenDone when time is over. */ private void startWaiting(final Runnable runWhenDone) { new Thread(new Runnable() { public void run() { try { Thread.sleep(PrefsAccessor.WAITING_TIME); } catch (InterruptedException e) { // doesn't care if sleep was interrupted, just move on } runWhenDone.run(); } }).start(); } /** * Cancel the ring notification (after ringing the bell). */ public void cancelRingNotification(ActivityPrefsAccessor activityPrefs) { NotificationManagerCompat.from(context).cancel(RING_NOTIFICATION_ID); } public boolean isBellSoundPlaying() { // if we hold a reference we haven't finished bell sound completely so only the reference is checked return mediaPlayer != null; } public int getAlarmMaxVolume() { AudioManager audioMan = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); return audioMan.getStreamMaxVolume(AudioManager.STREAM_ALARM); } /** * Returns duration hint for requesting audio focus depending on the version of Android. */ private int retrieveDurationHint() { if (Build.VERSION.SDK_INT >= 19) { return AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE; } else { return AUDIOFOCUS_GAIN_TRANSIENT; } } public int getAlarmVolume() { AudioManager audioMan = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); return audioMan.getStreamVolume(AudioManager.STREAM_ALARM); } public void setAlarmVolume(int volume) { AudioManager audioMan = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); audioMan.setStreamVolume(AudioManager.STREAM_ALARM, volume, 0); } /** * Send a newly created intent to Scheduler to update notification and setup a new bell schedule for reminder, if * requested cancel and newly setup alarms to update noticiation status depending on day-night mode. */ public void updateBellScheduleForReminder(boolean renewDayNightAlarm) { MindBell.logDebug("Update bell schedule for reminder requested, renewDayNightAlarm=" + renewDayNightAlarm); if (renewDayNightAlarm) { scheduleUpdateStatusNotificationDayNight(); } PendingIntent sender = createSchedulerBroadcastIntent(false, null, null); try { sender.send(); } catch (PendingIntent.CanceledException e) { Log.e(TAG, "Could not update bell schedule for reminder: " + e.getMessage(), e); } } /** * Schedule an update status notification for the start or the end of the active period. */ public void scheduleUpdateStatusNotificationDayNight() { long targetTimeMillis = SchedulerLogic .getNextDayNightChangeInMillis(Calendar.getInstance().getTimeInMillis(), prefs); scheduleUpdateStatusNotification(targetTimeMillis, UPDATE_STATUS_NOTIFICATION_DAY_NIGHT_REQUEST_CODE, "day-night"); } /** * Create an intent to be send to Scheduler to update notification and to (re-)schedule the bell. * * @param isRescheduling * True if the intents is meant for rescheduling instead of updating bell schedule. * @param nowTimeMillis * If not null millis to be given to Scheduler as now (or nextTargetTimeMillis from the perspective of the previous * call) * @param meditationPeriod * Zero: ramp-up, 1-(n-1): intermediate period, n: last period, n+1: beyond end */ private PendingIntent createSchedulerBroadcastIntent(boolean isRescheduling, Long nowTimeMillis, Integer meditationPeriod) { Log.d(TAG, "Creating scheduler intent: isRescheduling=" + isRescheduling + ", nowTimeMillis=" + nowTimeMillis + ", meditationPeriod=" + meditationPeriod); Intent intent = new Intent(context, Scheduler.class); if (isRescheduling) { intent.putExtra(EXTRA_IS_RESCHEDULING, true); } if (nowTimeMillis != null) { intent.putExtra(EXTRA_NOW_TIME_MILLIS, nowTimeMillis); } if (meditationPeriod != null) { intent.putExtra(EXTRA_MEDITATION_PERIOD, meditationPeriod); } return PendingIntent.getBroadcast(context, SCHEDULER_REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT); } /** * Schedule an update status notification for the future. */ private void scheduleUpdateStatusNotification(long targetTimeMillis, int requestCode, String info) { PendingIntent sender = createRefreshBroadcastIntent(requestCode); AlarmManagerCompat alarmManager = new AlarmManagerCompat(context); alarmManager.cancel(sender); // cancel old alarm, it has either gone away or became obsolete if (prefs.isActive()) { alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, targetTimeMillis, sender); TimeOfDay scheduledTime = new TimeOfDay(targetTimeMillis); Log.d(TAG, "Update status notification scheduled for " + scheduledTime.getLogString() + " (" + info + ")"); } } /** * Create an intent to be send to UpdateStatusNotification to update notification. */ private PendingIntent createRefreshBroadcastIntent(int requestCode) { return PendingIntent.getBroadcast(context, requestCode, new Intent("com.googlecode.mindbell.UPDATE_STATUS_NOTIFICATION"), PendingIntent.FLAG_UPDATE_CURRENT); } /** * Send a newly created intent to Scheduler to update notification and setup a new bell schedule for meditation. * * @param nextTargetTimeMillis * Millis to be given to Scheduler as now (or nextTargetTimeMillis from the perspective of the previous call) * @param meditationPeriod * Zero: ramp-up, 1-(n-1): intermediate period, n: last period, n+1: beyond end */ public void updateBellScheduleForMeditation(long nextTargetTimeMillis, int meditationPeriod) { MindBell.logDebug( "Update bell schedule for meditation requested, nextTargetTimeMillis=" + nextTargetTimeMillis); PendingIntent sender = createSchedulerBroadcastIntent(false, nextTargetTimeMillis, meditationPeriod); try { sender.send(); } catch (PendingIntent.CanceledException e) { Log.e(TAG, "Could not update bell schedule for meditation: " + e.getMessage(), e); } } /** * Reschedule the bell by letting AlarmManager send an intent to Scheduler. * * @param nextTargetTimeMillis * Millis to be given to Scheduler as now (or nextTargetTimeMillis from the perspective of the previous call) * @param nextMeditationPeriod * null if not meditating, otherwise 0: ramp-up, 1-(n-1): intermediate period, n: last period, n+1: beyond end */ public void reschedule(long nextTargetTimeMillis, Integer nextMeditationPeriod) { PendingIntent sender = createSchedulerBroadcastIntent(true, nextTargetTimeMillis, nextMeditationPeriod); AlarmManagerCompat alarmManager = new AlarmManagerCompat(context); alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextTargetTimeMillis, sender); TimeOfDay nextBellTime = new TimeOfDay(nextTargetTimeMillis); Log.d(TAG, "Scheduled next bell alarm for " + nextBellTime.getLogString()); } /** * Send an intent to MindBellMain to finally stop meditation (change status, stop countdown) automatically instead of * pressing the stop meditation button manually. */ public void stopMeditation() { Log.d(TAG, "Starting activity MindBellMain to stop meditation"); Intent intent = new Intent(context, MindBellMain.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK // context may be service context only, not an activity context | Intent.FLAG_ACTIVITY_CLEAR_TASK); // MindBellMain becomes the new root to let back button return to other apps intent.putExtra(PrefsAccessor.EXTRA_STOP_MEDITATION, true); context.startActivity(intent); } /** * Shows bell by starting activity MindBell */ public void showBell() { Log.d(TAG, "Starting activity MindBell to show bell"); Intent intent = new Intent(context, MindBell.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK // context may be service context only, not an activity context | Intent.FLAG_ACTIVITY_CLEAR_TASK); // MindBell becomes the new root to let back button return to other apps context.startActivity(intent); } /** * This is about updating the status notification on changes in system settings. */ public void updateStatusNotification() { if ((!prefs.isActive() && !prefs.isMeditating()) || !prefs.isStatus()) {// bell inactive or no notification wanted? Log.i(TAG, "Remove status notification because of inactive and non-meditating bell or unwanted status notification"); removeStatusNotification(); return; } // Choose material design or pre material design status icons int bellActiveDrawable; int bellActiveButMutedDrawable; if (prefs.useStatusIconMaterialDesign()) { bellActiveDrawable = R.drawable.ic_stat_bell_active; bellActiveButMutedDrawable = R.drawable.ic_stat_bell_active_but_muted; } else { bellActiveDrawable = R.drawable.golden_bell_status_active; bellActiveButMutedDrawable = R.drawable.golden_bell_status_active_but_muted; } // Suppose bell is active and not muted and all settings can be satisfied int statusDrawable = bellActiveDrawable; CharSequence contentTitle = context.getText(R.string.statusTitleBellActive); String contentText; String muteRequestReason = getMuteRequestReason(false); Class<?> targetClass = MindBellMain.class; // Override icon and notification text if bell is muted or permissions are insufficient if (!canSettingsBeSatisfied(prefs)) { // Insufficient permissions => override icon/text, switch notifications off statusDrawable = R.drawable.ic_warning_white_24dp; contentTitle = context.getText(R.string.statusTitleNotificationsDisabled); contentText = context.getText(R.string.statusTextNotificationsDisabled).toString(); targetClass = MindBellPreferences.class; // Status Notification would not be correct during incoming or outgoing calls because of the missing permission to // listen to phone state changes. Therefore we switch off notification and ask user for permission when he tries // to enable notification again. In this very moment we cannot ask for permission to avoid an ANR in receiver // UpdateStatusNotification. prefs.setStatus(false); } else if (prefs.isMeditating()) {// Bell meditation => override icon and notification text statusDrawable = R.drawable.ic_stat_bell_meditating; contentTitle = context.getText(R.string.statusTitleBellMeditating); contentText = MessageFormat.format(context.getText(R.string.statusTextBellMeditating).toString(), // prefs.getMeditationDuration().getInterval(), // new TimeOfDay(prefs.getMeditationEndingTimeMillis()).getDisplayString(context)); } else if (muteRequestReason != null) { // Bell muted => override icon and notification text statusDrawable = bellActiveButMutedDrawable; contentText = muteRequestReason; } else { // enrich standard notification by times and days contentText = MessageFormat.format(context.getText(R.string.statusTextBellActive).toString(), // prefs.getDaytimeStart().getDisplayString(context), // prefs.getDaytimeEnd().getDisplayString(context), // prefs.getActiveOnDaysOfWeekString()); } // Now do the notification update Log.i(TAG, "Update status notification: " + contentText); PendingIntent openAppIntent = PendingIntent.getActivity(context, 0, new Intent(context, targetClass), PendingIntent.FLAG_UPDATE_CURRENT); PendingIntent muteIntent = PendingIntent.getActivity(context, 2, new Intent(context, MuteActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); int visibility = (prefs.isStatusVisibilityPublic()) ? NotificationCompat.VISIBILITY_PUBLIC : NotificationCompat.VISIBILITY_PRIVATE; NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( context.getApplicationContext()) // .setCategory(NotificationCompat.CATEGORY_STATUS) // .setColor(context.getResources().getColor(R.color.backgroundColor)) // .setContentTitle(contentTitle) // .setContentText(contentText) // .setContentIntent(openAppIntent) // .setOngoing(true) // ongoing is *not* shown on wearable .setSmallIcon(statusDrawable) // .setVisibility(visibility); if (!prefs.isMeditating()) { // Do not allow other actions than stopping meditation while meditating notificationBuilder // .addAction(R.drawable.ic_action_refresh_status, context.getText(R.string.statusActionRefreshStatus), createRefreshBroadcastIntent(UPDATE_STATUS_NOTIFICATION_REQUEST_CODE)) // .addAction(R.drawable.ic_stat_bell_active_but_muted, context.getText(R.string.statusActionMuteFor), muteIntent); } Notification notification = notificationBuilder.build(); NotificationManagerCompat.from(context).notify(STATUS_NOTIFICATION_ID, notification); } private void removeStatusNotification() { NotificationManagerCompat.from(context).cancel(STATUS_NOTIFICATION_ID); } /** * Returns true if mute bell with phone isn't requested or if the app has the permission to be informed in case of incoming or * outgoing calls. Notification bell could not be turned over correctly if muting with phone were requested without permission * granted. */ private boolean canSettingsBeSatisfied(PrefsAccessor prefs) { boolean result = !prefs.isMuteOffHook() || ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED; Log.d(TAG, "Can settings be satisfied? -> " + result); return result; } /** * Schedule an update status notification for the end of a manual mute. */ public void scheduleUpdateStatusNotificationMutedTill(long targetTimeMillis) { scheduleUpdateStatusNotification(targetTimeMillis, UPDATE_STATUS_NOTIFICATION_MUTED_TILL_REQUEST_CODE, "muted till"); } @Override public void onAudioFocusChange(int focusChange) { MindBell.logDebug("Callback onAudioFocusChange() received focusChange=" + focusChange); switch (focusChange) { case AUDIOFOCUS_LOSS: case AUDIOFOCUS_LOSS_TRANSIENT: // could be handled by only pausing playback (not useful for bell sound) case AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: // could also be handled by lowering volume (not useful for bell sound) finishBellSound(); break; default: break; } } }