diff options
Diffstat (limited to 'src/com/android/cellbroadcastreceiver')
12 files changed, 515 insertions, 225 deletions
diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertAudio.java b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertAudio.java index 2b9dabc0..bc44ed1d 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertAudio.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertAudio.java @@ -27,7 +27,6 @@ import android.media.MediaPlayer.OnErrorListener; import android.os.Handler; import android.os.IBinder; import android.os.Message; -import android.os.PowerManager; import android.os.Vibrator; import android.speech.tts.TextToSpeech; import android.telephony.PhoneStateListener; @@ -76,9 +75,6 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI private static final long[] sVibratePattern = { 0, 2000, 500, 1000, 500, 1000, 500, 2000, 500, 1000, 500, 1000}; - /** CPU wake lock while playing audio. */ - private PowerManager.WakeLock mWakeLock; - private static final int STATE_IDLE = 0; private static final int STATE_ALERTING = 1; private static final int STATE_PAUSING = 2; @@ -199,11 +195,6 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI @Override public void onCreate() { - // acquire CPU wake lock while playing audio - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); - mWakeLock.acquire(); - mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); // Listen for incoming calls to kill the alarm. @@ -228,8 +219,8 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI Log.e(TAG, "exception trying to shutdown text-to-speech"); } } - // release CPU wake lock - mWakeLock.release(); + // release CPU wake lock acquired by CellBroadcastAlertService + CellBroadcastAlertWakeLock.releaseCpuLock(); } @Override diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialog.java b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialog.java index 3be8439d..f2509284 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialog.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialog.java @@ -45,7 +45,7 @@ public class CellBroadcastAlertDialog extends CellBroadcastAlertFullScreen { // Listen for the screen turning off so that when the screen comes back // on, the user does not need to unlock the phone to dismiss the alert. - if (mMessage.isEmergencyAlertMessage()) { + if (CellBroadcastConfigService.isEmergencyAlertMessage(getLatestMessage())) { mScreenOffReceiver = new ScreenOffReceiver(); registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); @@ -74,8 +74,8 @@ public class CellBroadcastAlertDialog extends CellBroadcastAlertFullScreen { private void handleScreenOff() { // Launch the full screen activity but do not turn the screen on. Intent i = new Intent(this, CellBroadcastAlertFullScreen.class); - i.putExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, mMessage); - i.putExtra(SCREEN_OFF, true); + i.putParcelableArrayListExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, mMessageList); + i.putExtra(SCREEN_OFF_EXTRA, true); startActivity(i); finish(); } diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertFullScreen.java b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertFullScreen.java index 99404265..cf6d7e5a 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertFullScreen.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertFullScreen.java @@ -17,15 +17,21 @@ package com.android.cellbroadcastreceiver; import android.app.Activity; +import android.app.KeyguardManager; import android.app.NotificationManager; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.preference.PreferenceManager; import android.provider.Telephony; import android.telephony.CellBroadcastMessage; +import android.telephony.SmsCbCmasInfo; +import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; @@ -35,24 +41,31 @@ import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicInteger; + /** * Full-screen emergency alert with flashing warning icon. * Alert audio and text-to-speech handled by {@link CellBroadcastAlertAudio}. * Keyguard handling based on {@code AlarmAlertFullScreen} class from DeskClock app. */ public class CellBroadcastAlertFullScreen extends Activity { + private static final String TAG = "CellBroadcastAlertFullScreen"; /** * Intent extra for full screen alert launched from dialog subclass as a result of the * screen turning off. */ - static final String SCREEN_OFF = "screen_off"; + static final String SCREEN_OFF_EXTRA = "screen_off"; + + /** Intent extra for non-emergency alerts sent when user selects the notification. */ + static final String FROM_NOTIFICATION_EXTRA = "from_notification"; - /** Whether to show the flashing warning icon. */ - private boolean mIsEmergencyAlert; + /** List of cell broadcast messages to display (oldest to newest). */ + ArrayList<CellBroadcastMessage> mMessageList; - /** The cell broadcast message to display. */ - CellBroadcastMessage mMessage; + /** Whether a CMAS alert other than Presidential Alert was displayed. */ + private boolean mShowOptOutDialog; /** Length of time for the warning icon to be visible. */ private static final int WARNING_ICON_ON_DURATION_MSEC = 800; @@ -60,37 +73,172 @@ public class CellBroadcastAlertFullScreen extends Activity { /** Length of time for the warning icon to be off. */ private static final int WARNING_ICON_OFF_DURATION_MSEC = 800; - /** Warning icon state. false = visible, true = off */ - private boolean mIconAnimationState; + /** Length of time to keep the screen turned on. */ + private static final int KEEP_SCREEN_ON_DURATION_MSEC = 60000; + + /** Animation handler for the flashing warning icon (emergency alerts only). */ + private final AnimationHandler mAnimationHandler = new AnimationHandler(); + + /** Handler to add and remove screen on flags for emergency alerts. */ + private final ScreenOffHandler mScreenOffHandler = new ScreenOffHandler(); + + /** + * Animation handler for the flashing warning icon (emergency alerts only). + */ + private class AnimationHandler extends Handler { + /** Latest {@code message.what} value for detecting old messages. */ + private final AtomicInteger mCount = new AtomicInteger(); + + /** Warning icon state: visible == true, hidden == false. */ + private boolean mWarningIconVisible; - /** Stop animating icon after {@link #onStop()} is called. */ - private boolean mStopAnimation; + /** The warning icon Drawable. */ + private Drawable mWarningIcon; - /** The warning icon Drawable. */ - private Drawable mWarningIcon; + /** The View containing the warning icon. */ + private ImageView mWarningIconView; - /** The View containing the warning icon. */ - private ImageView mWarningIconView; + /** Package local constructor (called from outer class). */ + AnimationHandler() {} + + /** Start the warning icon animation. */ + void startIconAnimation() { + if (!initDrawableAndImageView()) { + return; // init failure + } + mWarningIconVisible = true; + mWarningIconView.setVisibility(View.VISIBLE); + updateIconState(); + queueAnimateMessage(); + } + + /** Stop the warning icon animation. */ + void stopIconAnimation() { + // Increment the counter so the handler will ignore the next message. + mCount.incrementAndGet(); + if (mWarningIconView != null) { + mWarningIconView.setVisibility(View.GONE); + } + } + + /** Update the visibility of the warning icon. */ + private void updateIconState() { + mWarningIconView.setImageAlpha(mWarningIconVisible ? 255 : 0); + mWarningIconView.invalidateDrawable(mWarningIcon); + } + + /** Queue a message to animate the warning icon. */ + private void queueAnimateMessage() { + int msgWhat = mCount.incrementAndGet(); + sendEmptyMessageDelayed(msgWhat, mWarningIconVisible ? WARNING_ICON_ON_DURATION_MSEC + : WARNING_ICON_OFF_DURATION_MSEC); + // Log.d(TAG, "queued animation message id = " + msgWhat); + } - /** Icon animation handler for flashing warning alerts. */ - private final Handler mAnimationHandler = new Handler() { @Override public void handleMessage(Message msg) { - if (mIconAnimationState) { - mWarningIconView.setImageAlpha(255); - if (!mStopAnimation) { - mAnimationHandler.sendEmptyMessageDelayed(0, WARNING_ICON_ON_DURATION_MSEC); + if (msg.what == mCount.get()) { + mWarningIconVisible = !mWarningIconVisible; + updateIconState(); + queueAnimateMessage(); + } + } + + /** + * Initialize the Drawable and ImageView fields. + * @return true if successful; false if any field failed to initialize + */ + private boolean initDrawableAndImageView() { + if (mWarningIcon == null) { + try { + mWarningIcon = getResources().getDrawable(R.drawable.ic_warning_large); + } catch (Resources.NotFoundException e) { + Log.e(TAG, "warning icon resource not found", e); + return false; } - } else { - mWarningIconView.setImageAlpha(0); - if (!mStopAnimation) { - mAnimationHandler.sendEmptyMessageDelayed(0, WARNING_ICON_OFF_DURATION_MSEC); + } + if (mWarningIconView == null) { + mWarningIconView = (ImageView) findViewById(R.id.icon); + if (mWarningIconView != null) { + mWarningIconView.setImageDrawable(mWarningIcon); + } else { + Log.e(TAG, "failed to get ImageView for warning icon"); + return false; } } - mIconAnimationState = !mIconAnimationState; - mWarningIconView.invalidateDrawable(mWarningIcon); + return true; + } + } + + /** + * Handler to add {@code FLAG_KEEP_SCREEN_ON} for emergency alerts. After a short delay, + * remove the flag so the screen can turn off to conserve the battery. + */ + private class ScreenOffHandler extends Handler { + /** Latest {@code message.what} value for detecting old messages. */ + private final AtomicInteger mCount = new AtomicInteger(); + + /** Package local constructor (called from outer class). */ + ScreenOffHandler() {} + + /** Add screen on window flags and queue a delayed message to remove them later. */ + void startScreenOnTimer() { + addWindowFlags(); + int msgWhat = mCount.incrementAndGet(); + removeMessages(msgWhat - 1); // Remove previous message, if any. + sendEmptyMessageDelayed(msgWhat, KEEP_SCREEN_ON_DURATION_MSEC); + Log.d(TAG, "added FLAG_KEEP_SCREEN_ON, queued screen off message id " + msgWhat); } - }; + + /** Remove the screen on window flags and any queued screen off message. */ + void stopScreenOnTimer() { + removeMessages(mCount.get()); + clearWindowFlags(); + } + + /** Set the screen on window flags. */ + private void addWindowFlags() { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + /** Clear the screen on window flags. */ + private void clearWindowFlags() { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + @Override + public void handleMessage(Message msg) { + int msgWhat = msg.what; + if (msgWhat == mCount.get()) { + clearWindowFlags(); + Log.d(TAG, "removed FLAG_KEEP_SCREEN_ON with id " + msgWhat); + } else { + Log.e(TAG, "discarding screen off message with id " + msgWhat); + } + } + } + + /** Returns the currently displayed message. */ + CellBroadcastMessage getLatestMessage() { + int index = mMessageList.size() - 1; + if (index >= 0) { + return mMessageList.get(index); + } else { + return null; + } + } + + /** Removes and returns the currently displayed message. */ + private CellBroadcastMessage removeLatestMessage() { + int index = mMessageList.size() - 1; + if (index >= 0) { + return mMessageList.remove(index); + } else { + return null; + } + } @Override protected void onCreate(Bundle savedInstanceState) { @@ -106,72 +254,131 @@ public class CellBroadcastAlertFullScreen extends Activity { | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); - // Turn on the screen unless we're being launched from the dialog subclass as a result of - // the screen turning off. - if (!getIntent().getBooleanExtra(SCREEN_OFF, false)) { - win.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + // Initialize the view. + LayoutInflater inflater = LayoutInflater.from(this); + setContentView(inflater.inflate(getLayoutResId(), null)); + + findViewById(R.id.dismissButton).setOnClickListener( + new Button.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + + // Get message list from saved Bundle or from Intent. + if (savedInstanceState != null) { + Log.d(TAG, "onCreate getting message list from saved instance state"); + mMessageList = savedInstanceState.getParcelableArrayList( + CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA); + } else { + Log.d(TAG, "onCreate getting message list from intent"); + Intent intent = getIntent(); + mMessageList = intent.getParcelableArrayListExtra( + CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA); + + // If we were started from a notification, dismiss it. + clearNotification(intent); } - // Save message for passing from dialog to fullscreen activity, and for marking read. - mMessage = getIntent().getParcelableExtra( + if (mMessageList != null) { + Log.d(TAG, "onCreate loaded message list of size " + mMessageList.size()); + } else { + Log.e(TAG, "onCreate failed to get message list from saved Bundle"); + finish(); + } + + // For emergency alerts, keep screen on so the user can read it, unless this is a + // full screen alert created by CellBroadcastAlertDialog when the screen turned off. + CellBroadcastMessage message = getLatestMessage(); + if (CellBroadcastConfigService.isEmergencyAlertMessage(message) && + (savedInstanceState != null || + !getIntent().getBooleanExtra(SCREEN_OFF_EXTRA, false))) { + Log.d(TAG, "onCreate setting screen on timer for emergency alert"); + mScreenOffHandler.startScreenOnTimer(); + } + + updateAlertText(message); + } + + /** + * Called by {@link CellBroadcastAlertService} to add a new alert to the stack. + * @param intent The new intent containing one or more {@link CellBroadcastMessage}s. + */ + @Override + protected void onNewIntent(Intent intent) { + ArrayList<CellBroadcastMessage> newMessageList = intent.getParcelableArrayListExtra( CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA); + if (newMessageList != null) { + Log.d(TAG, "onNewIntent called with message list of size " + newMessageList.size()); + mMessageList.addAll(newMessageList); + updateAlertText(getLatestMessage()); + // If the new intent was sent from a notification, dismiss it. + clearNotification(intent); + } else { + Log.e(TAG, "onNewIntent called without SMS_CB_MESSAGE_EXTRA, ignoring"); + } + } - updateLayout(mMessage); + /** Try to cancel any notification that may have started this activity. */ + private void clearNotification(Intent intent) { + if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) { + Log.d(TAG, "Dismissing notification"); + NotificationManager notificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(CellBroadcastAlertService.NOTIFICATION_ID); + CellBroadcastReceiverApp.clearNewMessageList(); + } } + /** + * Save the list of messages so the state can be restored later. + * @param outState Bundle in which to place the saved state. + */ + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelableArrayList(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, mMessageList); + Log.d(TAG, "onSaveInstanceState saved message list to bundle"); + } + + /** Returns the resource ID for either the full screen or dialog layout. */ protected int getLayoutResId() { return R.layout.cell_broadcast_alert_fullscreen; } - private void updateLayout(CellBroadcastMessage message) { - LayoutInflater inflater = LayoutInflater.from(this); - - setContentView(inflater.inflate(getLayoutResId(), null)); - - /* Initialize dialog text from alert message. */ + /** Update alert text when a new emergency alert arrives. */ + private void updateAlertText(CellBroadcastMessage message) { int titleId = CellBroadcastResources.getDialogTitleResource(message); setTitle(titleId); ((TextView) findViewById(R.id.alertTitle)).setText(titleId); ((TextView) findViewById(R.id.message)).setText(message.getMessageBody()); - - /* dismiss button: close notification */ - findViewById(R.id.dismissButton).setOnClickListener( - new Button.OnClickListener() { - public void onClick(View v) { - dismiss(); - } - }); - - mIsEmergencyAlert = message.isPublicAlertMessage() || CellBroadcastConfigService - .isOperatorDefinedEmergencyId(message.getServiceCategory()); - - if (mIsEmergencyAlert) { - mWarningIcon = getResources().getDrawable(R.drawable.ic_warning_large); - mWarningIconView = (ImageView) findViewById(R.id.icon); - if (mWarningIconView != null) { - mWarningIconView.setImageDrawable(mWarningIcon); - } - - // Dismiss the notification that brought us here - ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)) - .cancel((int) message.getDeliveryTime()); - } } /** * Start animating warning icon. */ @Override - protected void onStart() { - super.onStart(); - if (mIsEmergencyAlert) { - // start icon animation - mAnimationHandler.sendEmptyMessageDelayed(0, WARNING_ICON_ON_DURATION_MSEC); + protected void onResume() { + Log.d(TAG, "onResume called"); + super.onResume(); + CellBroadcastMessage message = getLatestMessage(); + if (message != null && CellBroadcastConfigService.isEmergencyAlertMessage(message)) { + mAnimationHandler.startIconAnimation(); } } /** + * Stop animating warning icon. + */ + @Override + protected void onPause() { + Log.d(TAG, "onPause called"); + mAnimationHandler.stopIconAnimation(); + super.onPause(); + } + + /** * Stop animating warning icon and stop the {@link CellBroadcastAlertAudio} * service if necessary. */ @@ -179,7 +386,15 @@ public class CellBroadcastAlertFullScreen extends Activity { // Stop playing alert sound/vibration/speech (if started) stopService(new Intent(this, CellBroadcastAlertAudio.class)); - final long deliveryTime = mMessage.getDeliveryTime(); + // Remove the current alert message from the list. + CellBroadcastMessage lastMessage = removeLatestMessage(); + if (lastMessage == null) { + Log.e(TAG, "dismiss() called with empty message list!"); + return; + } + + // Mark the alert as read. + final long deliveryTime = lastMessage.getDeliveryTime(); // Mark broadcast as read on a background thread. new CellBroadcastContentProvider.AsyncCellBroadcastTask(getContentResolver()) @@ -191,19 +406,55 @@ public class CellBroadcastAlertFullScreen extends Activity { } }); - if (mIsEmergencyAlert) { - // stop animating emergency alert icon - mStopAnimation = true; - } else { - // decrement unread non-emergency alert count - CellBroadcastReceiverApp.decrementUnreadAlertCount(); + // Set the opt-out dialog flag if this is a CMAS alert (other than Presidential Alert). + if (lastMessage.isCmasMessage() && lastMessage.getCmasMessageClass() != + SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT) { + mShowOptOutDialog = true; } + + // If there are older emergency alerts to display, update the alert text and return. + CellBroadcastMessage nextMessage = getLatestMessage(); + if (nextMessage != null) { + updateAlertText(nextMessage); + if (CellBroadcastConfigService.isEmergencyAlertMessage(nextMessage)) { + mAnimationHandler.startIconAnimation(); + } else { + mAnimationHandler.stopIconAnimation(); + } + return; + } + + // Remove pending screen-off messages (animation messages are removed in onPause()). + mScreenOffHandler.stopScreenOnTimer(); + + // Show opt-in/opt-out dialog when the first CMAS alert is received. + if (mShowOptOutDialog) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + if (prefs.getBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, true)) { + // Clear the flag so the user will only see the opt-out dialog once. + prefs.edit().putBoolean(CellBroadcastSettings.KEY_SHOW_CMAS_OPT_OUT_DIALOG, false) + .apply(); + + KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); + if (km.inKeyguardRestrictedInputMode()) { + Log.d(TAG, "Showing opt-out dialog in new activity (secure keyguard)"); + Intent intent = new Intent(this, CellBroadcastOptOutActivity.class); + startActivity(intent); + } else { + Log.d(TAG, "Showing opt-out dialog in current activity"); + CellBroadcastOptOutActivity.showOptOutDialog(this); + return; // don't call finish() until user dismisses the dialog + } + } + } + finish(); } @Override public boolean dispatchKeyEvent(KeyEvent event) { - if (!mMessage.isEtwsMessage()) { + CellBroadcastMessage message = getLatestMessage(); + if (message != null && !message.isEtwsMessage()) { switch (event.getKeyCode()) { // Volume keys and camera keys mute the alert sound/vibration (except ETWS). case KeyEvent.KEYCODE_VOLUME_UP: diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertService.java b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertService.java index 04fea0ff..07e1bfbb 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertService.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertService.java @@ -26,7 +26,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.os.IBinder; -import android.os.PowerManager; import android.preference.PreferenceManager; import android.provider.Telephony; import android.telephony.CellBroadcastMessage; @@ -35,6 +34,7 @@ import android.telephony.SmsCbLocation; import android.telephony.SmsCbMessage; import android.util.Log; +import java.util.ArrayList; import java.util.HashSet; /** @@ -46,25 +46,12 @@ import java.util.HashSet; public class CellBroadcastAlertService extends Service { private static final String TAG = "CellBroadcastAlertService"; - /** Identifier for notification ID extra. */ - public static final String SMS_CB_NOTIFICATION_ID_EXTRA = - "com.android.cellbroadcastreceiver.SMS_CB_NOTIFICATION_ID"; - - /** Intent extra to indicate a previously unread alert. */ - static final String NEW_ALERT_EXTRA = "com.android.cellbroadcastreceiver.NEW_ALERT"; - /** Intent action to display alert dialog/notification, after verifying the alert is new. */ static final String SHOW_NEW_ALERT_ACTION = "cellbroadcastreceiver.SHOW_NEW_ALERT"; /** Use the same notification ID for non-emergency alerts. */ static final int NOTIFICATION_ID = 1; - /** CPU wake lock while handling emergency alert notification. */ - private PowerManager.WakeLock mWakeLock; - - /** Hold the wake lock for 5 seconds, which should be enough time to display the alert. */ - private static final int WAKE_LOCK_TIMEOUT = 5000; - /** Container for message ID and geographical scope, for duplicate message detection. */ private static final class MessageIdAndScope { private final int mMessageId; @@ -178,8 +165,7 @@ public class CellBroadcastAlertService extends Service { return; } - if (cbm.isEmergencyAlertMessage() || CellBroadcastConfigService - .isOperatorDefinedEmergencyId(cbm.getServiceCategory())) { + if (CellBroadcastConfigService.isEmergencyAlertMessage(cbm)) { // start alert sound / vibration / TTS and display full-screen alert openEmergencyAlertNotification(cbm); } else { @@ -233,25 +219,13 @@ public class CellBroadcastAlertService extends Service { return true; // other broadcast messages are always enabled } - private void acquireTimedWakelock(int timeout) { - if (mWakeLock == null) { - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - // Note: acquiring a PARTIAL_WAKE_LOCK and setting window flag FLAG_TURN_SCREEN_ON in - // CellBroadcastAlertFullScreen is not sufficient to turn on the screen by itself. - // Use SCREEN_BRIGHT_WAKE_LOCK here as a workaround to ensure the screen turns on. - mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK - | PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG); - } - mWakeLock.acquire(timeout); - } - /** * Display a full-screen alert message for emergency alerts. * @param message the alert to display */ private void openEmergencyAlertNotification(CellBroadcastMessage message) { // Acquire a CPU wake lock until the alert dialog and audio start playing. - acquireTimedWakelock(WAKE_LOCK_TIMEOUT); + CellBroadcastAlertWakeLock.acquireScreenCpuWakeLock(this); // Close dialogs and window shade Intent closeDialogs = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); @@ -283,8 +257,6 @@ public class CellBroadcastAlertService extends Service { prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_VIBRATE, true)); } - int channelTitleId = CellBroadcastResources.getDialogTitleResource(message); - CharSequence channelName = getText(channelTitleId); String messageBody = message.getMessageBody(); if (prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_SPEECH, true)) { @@ -303,9 +275,6 @@ public class CellBroadcastAlertService extends Service { } startService(audioIntent); - // Use lower 32 bits of emergency alert delivery time for notification ID - int notificationId = (int) message.getDeliveryTime(); - // Decide which activity to start based on the state of the keyguard. Class c = CellBroadcastAlertDialog.class; KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); @@ -314,23 +283,12 @@ public class CellBroadcastAlertService extends Service { c = CellBroadcastAlertFullScreen.class; } - Intent notify = createDisplayMessageIntent(this, c, message, notificationId); - PendingIntent pi = PendingIntent.getActivity(this, notificationId, notify, 0); - - Notification.Builder builder = new Notification.Builder(this) - .setSmallIcon(R.drawable.ic_notify_alert) - .setTicker(getText(CellBroadcastResources.getDialogTitleResource(message))) - .setWhen(System.currentTimeMillis()) - .setContentIntent(pi) - .setFullScreenIntent(pi, true) - .setContentTitle(channelName) - .setContentText(messageBody) - .setDefaults(Notification.DEFAULT_LIGHTS); + ArrayList<CellBroadcastMessage> messageList = new ArrayList<CellBroadcastMessage>(1); + messageList.add(message); - NotificationManager notificationManager = - (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); - - notificationManager.notify(notificationId, builder.getNotification()); + Intent alertDialogIntent = createDisplayMessageIntent(this, c, messageList); + alertDialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(alertDialogIntent); } /** @@ -343,11 +301,17 @@ public class CellBroadcastAlertService extends Service { CharSequence channelName = getText(channelTitleId); String messageBody = message.getMessageBody(); - // Use the same ID to create a single notification for multiple non-emergency alerts. - int notificationId = NOTIFICATION_ID; + // Pass the list of unread non-emergency CellBroadcastMessages + ArrayList<CellBroadcastMessage> messageList = CellBroadcastReceiverApp + .addNewMessageToList(message); + + // Create intent to show the new messages when user selects the notification. + Intent intent = createDisplayMessageIntent(this, CellBroadcastAlertDialog.class, + messageList); + intent.putExtra(CellBroadcastAlertFullScreen.FROM_NOTIFICATION_EXTRA, true); - PendingIntent pi = PendingIntent.getActivity(this, 0, createDisplayMessageIntent( - this, CellBroadcastListActivity.class, message, notificationId), 0); + PendingIntent pi = PendingIntent.getActivity(this, 0, intent, + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); // use default sound/vibration/lights for non-emergency broadcasts Notification.Builder builder = new Notification.Builder(this) @@ -360,7 +324,7 @@ public class CellBroadcastAlertService extends Service { builder.setDefaults(Notification.DEFAULT_ALL); // increment unread alert count (decremented when user dismisses alert dialog) - int unreadCount = CellBroadcastReceiverApp.incrementUnreadAlertCount(); + int unreadCount = messageList.size(); if (unreadCount > 1) { // use generic count of unread broadcasts if more than one unread builder.setContentTitle(getString(R.string.notification_multiple_title)); @@ -369,27 +333,17 @@ public class CellBroadcastAlertService extends Service { builder.setContentTitle(channelName).setContentText(messageBody); } - Log.i(TAG, "addToNotificationBar notificationId: " + notificationId); - NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(notificationId, builder.getNotification()); + notificationManager.notify(NOTIFICATION_ID, builder.build()); } static Intent createDisplayMessageIntent(Context context, Class intentClass, - CellBroadcastMessage message, int notificationId) { + ArrayList<CellBroadcastMessage> messageList) { // Trigger the list activity to fire up a dialog that shows the received messages Intent intent = new Intent(context, intentClass); - intent.putExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, message); - intent.putExtra(SMS_CB_NOTIFICATION_ID_EXTRA, notificationId); - intent.putExtra(NEW_ALERT_EXTRA, true); - - // This line is needed to make this intent compare differently than the other intents - // created here for other messages. Without this line, the PendingIntent always gets the - // intent of a previous message and notification. - intent.setType(Integer.toString(notificationId)); - + intent.putParcelableArrayListExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, messageList); return intent; } diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertWakeLock.java b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertWakeLock.java new file mode 100644 index 00000000..a1360b8d --- /dev/null +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertWakeLock.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 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.cellbroadcastreceiver; + +import android.content.Context; +import android.os.PowerManager; +import android.util.Log; + +/** + * Hold a wakelock that can be acquired in the CellBroadcastAlertService and + * released in the CellBroadcastAlertFullScreen Activity. + */ +class CellBroadcastAlertWakeLock { + private static final String TAG = "CellBroadcastAlertWakeLock"; + + private static PowerManager.WakeLock sCpuWakeLock; + + private CellBroadcastAlertWakeLock() {} + + static void acquireScreenCpuWakeLock(Context context) { + if (sCpuWakeLock != null) { + return; + } + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + sCpuWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK + | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE, TAG); + sCpuWakeLock.acquire(); + Log.d(TAG, "acquired screen + CPU wake lock"); + } + + static void releaseCpuLock() { + if (sCpuWakeLock != null) { + sCpuWakeLock.release(); + sCpuWakeLock = null; + Log.d(TAG, "released screen + CPU wake lock"); + } + } +} diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastConfigService.java b/src/com/android/cellbroadcastreceiver/CellBroadcastConfigService.java index ad77dc25..dd99dc56 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastConfigService.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastConfigService.java @@ -22,6 +22,7 @@ import android.content.SharedPreferences; import android.content.res.Resources; import android.os.SystemProperties; import android.preference.PreferenceManager; +import android.telephony.CellBroadcastMessage; import android.telephony.SmsManager; import android.text.TextUtils; import android.util.Log; @@ -80,13 +81,24 @@ public class CellBroadcastConfigService extends IntentService { } } - static boolean isOperatorDefinedEmergencyId(int messageId) { + /** + * Returns true if this is a standard or operator-defined emergency alert message. + * This includes all ETWS and CMAS alerts, except for AMBER alerts. + * @param message the message to test + * @return true if the message is an emergency alert; false otherwise + */ + static boolean isEmergencyAlertMessage(CellBroadcastMessage message) { + if (message.isEmergencyAlertMessage()) { + return true; + } + // Check for system property defining the emergency channel ranges to enable String emergencyIdRange = SystemProperties.get("ro.cellbroadcast.emergencyids"); if (TextUtils.isEmpty(emergencyIdRange)) { return false; } try { + int messageId = message.getServiceCategory(); for (String channelRange : emergencyIdRange.split(",")) { int dashIndex = channelRange.indexOf('-'); if (dashIndex != -1) { diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastContentProvider.java b/src/com/android/cellbroadcastreceiver/CellBroadcastContentProvider.java index f3687254..7460f784 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastContentProvider.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastContentProvider.java @@ -252,19 +252,15 @@ public class CellBroadcastContentProvider extends ContentProvider { /** * Internal method to delete a cell broadcast by row ID and notify observers. * @param rowId the row ID of the broadcast to delete - * @param decrementUnreadCount true to decrement the count of unread alerts * @return true if the database was updated, false otherwise */ - boolean deleteBroadcast(long rowId, boolean decrementUnreadCount) { + boolean deleteBroadcast(long rowId) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME, Telephony.CellBroadcasts._ID + "=?", new String[]{Long.toString(rowId)}); if (rowCount != 0) { - if (decrementUnreadCount) { - CellBroadcastReceiverApp.decrementUnreadAlertCount(); - } return true; } else { Log.e(TAG, "failed to delete broadcast at row " + rowId); @@ -281,7 +277,6 @@ public class CellBroadcastContentProvider extends ContentProvider { int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME, null, null); if (rowCount != 0) { - CellBroadcastReceiverApp.resetUnreadAlertCount(); return true; } else { Log.e(TAG, "failed to delete all broadcasts"); diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastListActivity.java b/src/com/android/cellbroadcastreceiver/CellBroadcastListActivity.java index 112cb92a..777c24ef 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastListActivity.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastListActivity.java @@ -44,6 +44,8 @@ import android.view.ViewGroup; import android.widget.CursorAdapter; import android.widget.ListView; +import java.util.ArrayList; + /** * This activity provides a list view of received cell broadcasts. Most of the work is handled * in the inner CursorLoaderListFragment class. @@ -159,7 +161,9 @@ public class CellBroadcastListActivity extends Activity { private void showDialogAndMarkRead(CellBroadcastMessage cbm) { // show emergency alerts with the warning icon, but don't play alert tone Intent i = new Intent(getActivity(), CellBroadcastAlertDialog.class); - i.putExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, cbm); + ArrayList<CellBroadcastMessage> messageList = new ArrayList<CellBroadcastMessage>(1); + messageList.add(cbm); + i.putParcelableArrayListExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, messageList); startActivity(i); } @@ -190,11 +194,8 @@ public class CellBroadcastListActivity extends Activity { if (cursor != null && cursor.getPosition() >= 0) { switch (item.getItemId()) { case MENU_DELETE: - // We need to decrement the unread alert count if deleting unread alert - boolean isUnread = (cursor.getInt(cursor.getColumnIndexOrThrow( - Telephony.CellBroadcasts.MESSAGE_READ)) == 0); confirmDeleteThread(cursor.getLong(cursor.getColumnIndexOrThrow( - Telephony.CellBroadcasts._ID)), isUnread); + Telephony.CellBroadcasts._ID))); break; case MENU_VIEW_DETAILS: @@ -212,7 +213,7 @@ public class CellBroadcastListActivity extends Activity { public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case MENU_DELETE_ALL: - confirmDeleteThread(-1, false); + confirmDeleteThread(-1); break; case MENU_PREFERENCES: @@ -229,10 +230,9 @@ public class CellBroadcastListActivity extends Activity { /** * Start the process of putting up a dialog to confirm deleting a broadcast. * @param rowId the row ID of the broadcast to delete, or -1 to delete all broadcasts - * @param unread true if the alert was not already marked as read */ - public void confirmDeleteThread(long rowId, boolean unread) { - DeleteThreadListener listener = new DeleteThreadListener(rowId, unread); + public void confirmDeleteThread(long rowId) { + DeleteThreadListener listener = new DeleteThreadListener(rowId); confirmDeleteThreadDialog(listener, (rowId == -1), getActivity()); } @@ -258,11 +258,9 @@ public class CellBroadcastListActivity extends Activity { public class DeleteThreadListener implements OnClickListener { private final long mRowId; - private final boolean mIsUnread; - public DeleteThreadListener(long rowId, boolean unread) { + public DeleteThreadListener(long rowId) { mRowId = rowId; - mIsUnread = unread; } @Override @@ -274,7 +272,7 @@ public class CellBroadcastListActivity extends Activity { @Override public boolean execute(CellBroadcastContentProvider provider) { if (mRowId != -1) { - return provider.deleteBroadcast(mRowId, mIsUnread); + return provider.deleteBroadcast(mRowId); } else { return provider.deleteAllBroadcasts(); } @@ -285,29 +283,4 @@ public class CellBroadcastListActivity extends Activity { } } } - - @Override - protected void onNewIntent(Intent intent) { - if (intent == null) { - return; - } - - Bundle extras = intent.getExtras(); - if (extras == null) { - return; - } - - CellBroadcastMessage cbm = extras.getParcelable(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA); - int notificationId = extras.getInt(CellBroadcastAlertService.SMS_CB_NOTIFICATION_ID_EXTRA); - - // Dismiss the notification that brought us here. - NotificationManager notificationManager = - (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(notificationId); - - // launch the dialog activity to show the alert - Intent i = new Intent(this, CellBroadcastAlertDialog.class); - i.putExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, cbm); - startActivity(i); - } } diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastOptOutActivity.java b/src/com/android/cellbroadcastreceiver/CellBroadcastOptOutActivity.java new file mode 100644 index 00000000..76ed5375 --- /dev/null +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastOptOutActivity.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2012 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.cellbroadcastreceiver; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +/** + * Container activity for CMAS opt-in/opt-out alert dialog. + */ +public class CellBroadcastOptOutActivity extends Activity { + private static final String TAG = "CellBroadcastOptOutActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.d(TAG, "created activity"); + showOptOutDialog(this); + } + + /** + * Show the opt-out dialog. Uses the CellBroadcastAlertDialog activity unless the device is + * in restricted keyguard mode, in which case we create a new CellBroadcastOptOutActivity + * so that the dialog appears underneath the lock screen. The user must unlock the device + * to configure the settings, so we don't want to show the opt-in dialog before then. + */ + static void showOptOutDialog(final Activity activity) { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setMessage(R.string.cmas_opt_out_dialog_text) + .setPositiveButton(R.string.cmas_opt_out_button_yes, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.d(TAG, "User clicked Yes"); + activity.finish(); + } + }) + .setNegativeButton(R.string.cmas_opt_out_button_no, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.d(TAG, "User clicked No"); + Intent intent = new Intent(activity, CellBroadcastSettings.class); + activity.startActivity(intent); + activity.finish(); + } + }) + .create().show(); + } +} diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastReceiverApp.java b/src/com/android/cellbroadcastreceiver/CellBroadcastReceiverApp.java index eb21e17e..65e8c72b 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastReceiverApp.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastReceiverApp.java @@ -17,9 +17,11 @@ package com.android.cellbroadcastreceiver; import android.app.Application; +import android.telephony.CellBroadcastMessage; import android.util.Log; import android.preference.PreferenceManager; +import java.util.ArrayList; import java.util.concurrent.atomic.AtomicInteger; /** @@ -36,29 +38,18 @@ public class CellBroadcastReceiverApp extends Application { PreferenceManager.setDefaultValues(this, R.xml.preferences, false); } - /** Number of unread non-emergency alerts since the device was booted. */ - private static AtomicInteger sUnreadAlertCount = new AtomicInteger(); + /** List of unread non-emergency alerts to show when user selects the notification. */ + private static final ArrayList<CellBroadcastMessage> sNewMessageList = + new ArrayList<CellBroadcastMessage>(4); - /** - * Increments the number of unread non-emergency alerts, returning the new value. - * @return the updated number of unread non-emergency alerts, after incrementing - */ - static int incrementUnreadAlertCount() { - return sUnreadAlertCount.incrementAndGet(); + /** Adds a new unread non-emergency message and returns the current list. */ + static ArrayList<CellBroadcastMessage> addNewMessageToList(CellBroadcastMessage message) { + sNewMessageList.add(message); + return sNewMessageList; } - /** - * Decrements the number of unread non-emergency alerts after the user reads it. - */ - static void decrementUnreadAlertCount() { - if (sUnreadAlertCount.decrementAndGet() < 0) { - Log.e(TAG, "mUnreadAlertCount < 0, resetting to 0"); - sUnreadAlertCount.set(0); - } - } - - /** Resets the unread alert count to zero after user deletes all alerts. */ - static void resetUnreadAlertCount() { - sUnreadAlertCount.set(0); + /** Clears the list of unread non-emergency messages. */ + static void clearNewMessageList() { + sNewMessageList.clear(); } } diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastResources.java b/src/com/android/cellbroadcastreceiver/CellBroadcastResources.java index fedd1533..76d4b42b 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastResources.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastResources.java @@ -289,7 +289,7 @@ public class CellBroadcastResources { } } - if (cbm.isPublicAlertMessage()) { + if (CellBroadcastConfigService.isEmergencyAlertMessage(cbm)) { return R.string.pws_other_message_identifiers; } else { return R.string.cb_other_message_identifiers; diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastSettings.java b/src/com/android/cellbroadcastreceiver/CellBroadcastSettings.java index 7e7915da..a7c74824 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastSettings.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastSettings.java @@ -79,6 +79,9 @@ public class CellBroadcastSettings extends PreferenceActivity { // Enabled by default for phones sold in Brazil, otherwise this setting may be hidden. public static final String KEY_ENABLE_CHANNEL_50_ALERTS = "enable_channel_50_alerts"; + // Preference key for initial opt-in/opt-out dialog. + public static final String KEY_SHOW_CMAS_OPT_OUT_DIALOG = "show_cmas_opt_out_dialog"; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); |