/* * Copyright (C) 2011 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.talkback; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; import android.os.PowerManager; import android.provider.Settings; import android.telephony.TelephonyManager; import android.text.SpannableStringBuilder; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.Log; import com.android.talkback.contextmenu.MenuManager; import com.android.talkback.controller.FeedbackController; import com.android.talkback.controller.TelevisionNavigationController; import com.android.utils.LogUtils; import com.android.utils.StringBuilderUtils; import com.google.android.marvin.talkback.TalkBackService; import java.util.HashSet; import java.util.LinkedList; import java.util.Set; // TODO: Refactor this class into two separate receivers // with listener interfaces. This will remove the need to hold dependencies // and call into other classes. /** * {@link BroadcastReceiver} for receiving updates for our context - device * state */ public class RingerModeAndScreenMonitor extends BroadcastReceiver { /** The intent filter to match phone and screen state changes. */ private static final IntentFilter STATE_CHANGE_FILTER = new IntentFilter(); static { STATE_CHANGE_FILTER.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); STATE_CHANGE_FILTER.addAction(Intent.ACTION_SCREEN_ON); STATE_CHANGE_FILTER.addAction(Intent.ACTION_SCREEN_OFF); STATE_CHANGE_FILTER.addAction(Intent.ACTION_USER_PRESENT); } private final Context mContext; private final SpeechController mSpeechController; private final ShakeDetector mShakeDetector; private final TelevisionNavigationController mTelevisionNavigationController; private final AudioManager mAudioManager; private final FeedbackController mFeedbackController; private final MenuManager mMenuManager; private final TelephonyManager mTelephonyManager; private final Set<DialogInterface> mOpenDialogs = new HashSet<>(); /** The current ringer mode. */ private int mRingerMode = AudioManager.RINGER_MODE_NORMAL; private boolean mIsScreenOn; /** * Creates a new instance. */ public RingerModeAndScreenMonitor(FeedbackController feedbackController, MenuManager menuManager, ShakeDetector shakeDetector, SpeechController speechController, TalkBackService context) { if (feedbackController == null) throw new IllegalStateException(); if (menuManager == null) throw new IllegalStateException(); if (speechController == null) throw new IllegalStateException(); if (shakeDetector == null) throw new IllegalStateException(); mContext = context; mFeedbackController = feedbackController; mMenuManager = menuManager; mSpeechController = speechController; mShakeDetector = shakeDetector; mTelevisionNavigationController = context.getTelevisionNavigationController(); mAudioManager = (AudioManager) context.getSystemService(Service.AUDIO_SERVICE); mTelephonyManager = (TelephonyManager) context.getSystemService(Service.TELEPHONY_SERVICE); //noinspection deprecation mIsScreenOn = ((PowerManager) context.getSystemService(Context.POWER_SERVICE)).isScreenOn(); } @Override public void onReceive(Context context, Intent intent) { if (!TalkBackService.isServiceActive()) return; String action = intent.getAction(); if (action == null) return; switch (action) { case AudioManager.RINGER_MODE_CHANGED_ACTION: handleRingerModeChanged(intent.getIntExtra( AudioManager.EXTRA_RINGER_MODE, AudioManager.RINGER_MODE_NORMAL)); break; case Intent.ACTION_SCREEN_ON: mIsScreenOn = true; handleScreenOn(); break; case Intent.ACTION_SCREEN_OFF: mIsScreenOn = false; handleScreenOff(); break; case Intent.ACTION_USER_PRESENT: handleDeviceUnlocked(); break; } } public void updateScreenState() { //noinspection deprecation mIsScreenOn = ((PowerManager) mContext.getSystemService(Context.POWER_SERVICE)).isScreenOn(); } public boolean isScreenOn() { return mIsScreenOn; } public IntentFilter getFilter() { return STATE_CHANGE_FILTER; } /** * Handles when the device is unlocked. Just speaks "unlocked." */ private void handleDeviceUnlocked() { if (isIdle()) { final String text = mContext.getString(R.string.value_device_unlocked); mSpeechController.speak(text, SpeechController.QUEUE_MODE_INTERRUPT, 0, null); } } /** * Handles when the screen is turned off. Announces "screen off" and * suspends the proximity sensor. */ @SuppressWarnings("deprecation") private void handleScreenOff() { mSpeechController.setScreenIsOn(false); mMenuManager.dismissAll(); // Iterate over a copy because dialog dismiss handlers might try to unregister dialogs. LinkedList<DialogInterface> openDialogsCopy = new LinkedList<>(mOpenDialogs); for (DialogInterface dialog : openDialogsCopy) { dialog.cancel(); } mOpenDialogs.clear(); final SpannableStringBuilder builder = new SpannableStringBuilder(mContext.getString(R.string.value_screen_off)); // Only announce ringer state if we're not in a call. if (isIdle()) { appendRingerStateAnnouncement(builder); } mShakeDetector.pausePolling(); if (mRingerMode == AudioManager.RINGER_MODE_NORMAL) { final int soundId; final float volume; final float musicVolume = getStreamVolume(AudioManager.STREAM_MUSIC); if ((musicVolume > 0) && (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn())) { // Play the ringer beep on the default (music) stream to avoid // issues with ringer audio (e.g. no speech on ICS and // interruption of music on JB). Adjust playback volume to // compensate for music volume. final float ringVolume = getStreamVolume(AudioManager.STREAM_RING); soundId = R.raw.volume_beep; volume = Math.min(1.0f, (ringVolume / musicVolume)); } else { // Normally we'll play the volume beep on the ring stream. soundId = R.raw.volume_beep; volume = 1.0f; } mFeedbackController.playAuditory(soundId, 1.0f /* rate */, volume); } // Always reset the television remote mode to the standard (navigate) mode on screen off. if (mTelevisionNavigationController != null) { mTelevisionNavigationController.resetToNavigateMode(); } mSpeechController.speak( builder, SpeechController.QUEUE_MODE_INTERRUPT, FeedbackItem.FLAG_NO_HISTORY, null); } /** * Handles when the screen is turned on. Announces the current time and the * current ringer state when phone is idle. */ private void handleScreenOn() { // TODO: This doesn't look right. Should probably be using a listener. mSpeechController.setScreenIsOn(true); final SpannableStringBuilder builder = new SpannableStringBuilder(); if (isIdle()) { // Need the old version to support version older than JB_MR1 //noinspection deprecation if (Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.DEVICE_PROVISIONED, 0) != 0) { appendCurrentTimeAnnouncement(builder); } else { // Device is not ready, just speak screen on builder.append(mContext.getString(R.string.value_screen_on)); } } mShakeDetector.resumePolling(); mSpeechController.speak(builder, SpeechController.QUEUE_MODE_INTERRUPT, 0, null); } /** * Return current phone's call state is idle or not. * @return true when phone is idle */ private boolean isIdle() { return mTelephonyManager != null && mTelephonyManager.getCallState() == TelephonyManager.CALL_STATE_IDLE; } /** * Handles when the ringer mode (ex. volume) changes. Announces the current * ringer state. */ private void handleRingerModeChanged(int ringerMode) { mRingerMode = ringerMode; } /** * Appends the current time announcement to a {@link StringBuilder}. * * @param builder The string to append to. */ @SuppressWarnings("deprecation") private void appendCurrentTimeAnnouncement(SpannableStringBuilder builder) { int timeFlags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; if (DateFormat.is24HourFormat(mContext)) { timeFlags |= DateUtils.FORMAT_24HOUR; } final CharSequence dateTime = DateUtils.formatDateTime(mContext, System.currentTimeMillis(), timeFlags); StringBuilderUtils.appendWithSeparator(builder, dateTime); } /** * Appends the ringer state announcement to a {@link StringBuilder}. * * @param builder The string to append to. */ private void appendRingerStateAnnouncement(SpannableStringBuilder builder) { if (mTelephonyManager == null) { return; } final String announcement; switch (mRingerMode) { case AudioManager.RINGER_MODE_SILENT: announcement = mContext.getString(R.string.value_ringer_silent); break; case AudioManager.RINGER_MODE_VIBRATE: announcement = mContext.getString(R.string.value_ringer_vibrate); break; case AudioManager.RINGER_MODE_NORMAL: return; default: LogUtils.log(TalkBackService.class, Log.ERROR, "Unknown ringer mode: %d", mRingerMode); return; } StringBuilderUtils.appendWithSeparator(builder, announcement); } /** * Returns the volume a stream as a fraction of its maximum volume. * * @param streamType The stream type for which to return the volume. * @return The stream volume as a fraction of its maximum volume. */ private float getStreamVolume(int streamType) { final int currentVolume = mAudioManager.getStreamVolume(streamType); final int maxVolume = mAudioManager.getStreamMaxVolume(streamType); return (currentVolume / (float) maxVolume); } /** Registers a dialog to be auto-cancelled when the screen turns off. */ public void registerDialog(DialogInterface dialog) { mOpenDialogs.add(dialog); } /** Removes a dialog from the list of dialogs to be auto-cancelled. */ public void unregisterDialog(DialogInterface dialog) { mOpenDialogs.remove(dialog); } }