From 57273ebfa13f96bf5aba9902b70e2b179fec9e4c Mon Sep 17 00:00:00 2001 From: Jake Hamby Date: Mon, 8 Oct 2012 19:41:30 -0700 Subject: Multiple fixes to CMAS app. * Allow screen to turn off after 30 seconds for emergency alerts. * When multiple alerts are received, show them in reverse order. * After showing the first CMAS alert received on device (other than Presidential Alert), show opt-out dialog to allow user to opt-in or out of the various levels of CMAS alert. * Start/stop animating warning icon in activity pause/resume. * When multiple non-emergency alerts are received, show them all (most recent displayed first) when user selects the notification. * For emergency alerts, start the dialog activity directly instead of creating a PendingIntent and full screen notification. * Correctly save/restore the list of alerts in the alert dialog in onSaveInstanceState() when recreated after screen rotation. * Fix test app to increment the message ID and to send the correct alerts when multiple alerts are selected with 5 second delay. Bug: 6993660 Bug: 7041847 Bug: 7045506 Change-Id: Ic3ec08f0ebd693244891a4bf3a29479b832c2a3e --- .../CellBroadcastAlertFullScreen.java | 403 +++++++++++++++++---- 1 file changed, 327 insertions(+), 76 deletions(-) (limited to 'src/com/android/cellbroadcastreceiver/CellBroadcastAlertFullScreen.java') 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 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,71 +254,130 @@ 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 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: -- cgit v1.2.3