1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.cellbroadcastreceiver; 18 19 import android.annotation.Nullable; 20 import android.app.ActionBar; 21 import android.app.Activity; 22 import android.app.AlertDialog; 23 import android.app.FragmentManager; 24 import android.app.ListFragment; 25 import android.app.LoaderManager; 26 import android.app.NotificationManager; 27 import android.content.Context; 28 import android.content.CursorLoader; 29 import android.content.DialogInterface; 30 import android.content.DialogInterface.OnClickListener; 31 import android.content.Intent; 32 import android.content.Loader; 33 import android.database.Cursor; 34 import android.net.Uri; 35 import android.os.Bundle; 36 import android.provider.Telephony; 37 import android.telephony.SmsCbMessage; 38 import android.util.Log; 39 import android.view.LayoutInflater; 40 import android.view.Menu; 41 import android.view.MenuInflater; 42 import android.view.MenuItem; 43 import android.view.View; 44 import android.view.View.OnCreateContextMenuListener; 45 import android.view.ViewGroup; 46 import android.widget.CursorAdapter; 47 import android.widget.ListView; 48 import android.widget.TextView; 49 50 import com.android.internal.annotations.VisibleForTesting; 51 52 import java.util.ArrayList; 53 54 /** 55 * This activity provides a list view of received cell broadcasts. Most of the work is handled 56 * in the inner CursorLoaderListFragment class. 57 */ 58 public class CellBroadcastListActivity extends Activity { 59 60 @VisibleForTesting 61 public CursorLoaderListFragment mListFragment; 62 63 @Override onCreate(Bundle savedInstanceState)64 protected void onCreate(Bundle savedInstanceState) { 65 super.onCreate(savedInstanceState); 66 67 ActionBar actionBar = getActionBar(); 68 if (actionBar != null) { 69 // android.R.id.home will be triggered in onOptionsItemSelected() 70 actionBar.setDisplayHomeAsUpEnabled(true); 71 } 72 73 setTitle(getString(R.string.cb_list_activity_title)); 74 75 // Dismiss the notification that brought us here (if any). 76 ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)) 77 .cancel(CellBroadcastAlertService.NOTIFICATION_ID); 78 79 FragmentManager fm = getFragmentManager(); 80 81 // Create the list fragment and add it as our sole content. 82 if (fm.findFragmentById(android.R.id.content) == null) { 83 mListFragment = new CursorLoaderListFragment(); 84 fm.beginTransaction().add(android.R.id.content, mListFragment).commit(); 85 } 86 } 87 88 @Override onOptionsItemSelected(MenuItem item)89 public boolean onOptionsItemSelected(MenuItem item) { 90 switch (item.getItemId()) { 91 // Respond to the action bar's Up/Home button 92 case android.R.id.home: 93 finish(); 94 return true; 95 } 96 return super.onOptionsItemSelected(item); 97 } 98 99 /** 100 * List fragment queries SQLite database on worker thread. 101 */ 102 public static class CursorLoaderListFragment extends ListFragment 103 implements LoaderManager.LoaderCallbacks<Cursor> { 104 private static final String TAG = CellBroadcastListActivity.class.getSimpleName(); 105 private static final boolean DBG = true; 106 107 // IDs of the main menu items. 108 @VisibleForTesting 109 public static final int MENU_DELETE_ALL = 3; 110 @VisibleForTesting 111 public static final int MENU_SHOW_REGULAR_MESSAGES = 4; 112 @VisibleForTesting 113 public static final int MENU_SHOW_ALL_MESSAGES = 5; 114 115 // Load the history from cell broadcast receiver database 116 private static final int LOADER_NORMAL_HISTORY = 1; 117 // Load the history from cell broadcast service. This will include all non-shown messages. 118 @VisibleForTesting 119 public static final int LOADER_HISTORY_FROM_CBS = 2; 120 121 @VisibleForTesting 122 public static final String KEY_LOADER_ID = "loader_id"; 123 124 // IDs of the context menu items (package local, accessed from inner DeleteThreadListener). 125 @VisibleForTesting 126 public static final int MENU_DELETE = 0; 127 @VisibleForTesting 128 public static final int MENU_VIEW_DETAILS = 1; 129 130 // cell broadcast provider from cell broadcast service. 131 public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts"); 132 133 // Query columns for provider from cell broadcast service. 134 public static final String[] QUERY_COLUMNS = { 135 Telephony.CellBroadcasts._ID, 136 Telephony.CellBroadcasts.SLOT_INDEX, 137 Telephony.CellBroadcasts.SUBSCRIPTION_ID, 138 Telephony.CellBroadcasts.GEOGRAPHICAL_SCOPE, 139 Telephony.CellBroadcasts.PLMN, 140 Telephony.CellBroadcasts.LAC, 141 Telephony.CellBroadcasts.CID, 142 Telephony.CellBroadcasts.SERIAL_NUMBER, 143 Telephony.CellBroadcasts.SERVICE_CATEGORY, 144 Telephony.CellBroadcasts.LANGUAGE_CODE, 145 Telephony.CellBroadcasts.DATA_CODING_SCHEME, 146 Telephony.CellBroadcasts.MESSAGE_BODY, 147 Telephony.CellBroadcasts.MESSAGE_FORMAT, 148 Telephony.CellBroadcasts.MESSAGE_PRIORITY, 149 Telephony.CellBroadcasts.ETWS_WARNING_TYPE, 150 Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS, 151 Telephony.CellBroadcasts.CMAS_CATEGORY, 152 Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE, 153 Telephony.CellBroadcasts.CMAS_SEVERITY, 154 Telephony.CellBroadcasts.CMAS_URGENCY, 155 Telephony.CellBroadcasts.CMAS_CERTAINTY, 156 Telephony.CellBroadcasts.RECEIVED_TIME, 157 Telephony.CellBroadcasts.LOCATION_CHECK_TIME, 158 Telephony.CellBroadcasts.MESSAGE_BROADCASTED, 159 Telephony.CellBroadcasts.MESSAGE_DISPLAYED, 160 Telephony.CellBroadcasts.GEOMETRIES, 161 Telephony.CellBroadcasts.MAXIMUM_WAIT_TIME 162 }; 163 164 // This is the Adapter being used to display the list's data. 165 @VisibleForTesting 166 public CursorAdapter mAdapter; 167 168 private int mCurrentLoaderId = 0; 169 170 @Override onCreate(Bundle savedInstanceState)171 public void onCreate(Bundle savedInstanceState) { 172 super.onCreate(savedInstanceState); 173 174 // We have a menu item to show in action bar. 175 setHasOptionsMenu(true); 176 } 177 178 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)179 public View onCreateView(LayoutInflater inflater, ViewGroup container, 180 Bundle savedInstanceState) { 181 return inflater.inflate(R.layout.cell_broadcast_list_screen, container, false); 182 } 183 184 @Override onActivityCreated(Bundle savedInstanceState)185 public void onActivityCreated(Bundle savedInstanceState) { 186 super.onActivityCreated(savedInstanceState); 187 188 // Set context menu for long-press. 189 ListView listView = getListView(); 190 listView.setOnCreateContextMenuListener(mOnCreateContextMenuListener); 191 192 // Create a cursor adapter to display the loaded data. 193 mAdapter = new CellBroadcastCursorAdapter(getActivity()); 194 setListAdapter(mAdapter); 195 196 mCurrentLoaderId = LOADER_NORMAL_HISTORY; 197 if (savedInstanceState != null && savedInstanceState.containsKey(KEY_LOADER_ID)) { 198 mCurrentLoaderId = savedInstanceState.getInt(KEY_LOADER_ID); 199 } 200 201 if (DBG) Log.d(TAG, "onActivityCreated: id=" + mCurrentLoaderId); 202 203 // Prepare the loader. Either re-connect with an existing one, 204 // or start a new one. 205 getLoaderManager().initLoader(mCurrentLoaderId, null, this); 206 } 207 208 @Override onSaveInstanceState(Bundle outState)209 public void onSaveInstanceState(Bundle outState) { 210 // Save the current id for later restoring activity. 211 if (DBG) Log.d(TAG, "onSaveInstanceState: id=" + mCurrentLoaderId); 212 outState.putInt(KEY_LOADER_ID, mCurrentLoaderId); 213 } 214 215 @Override onResume()216 public void onResume() { 217 super.onResume(); 218 if (DBG) Log.d(TAG, "onResume"); 219 if (mCurrentLoaderId != 0) { 220 getLoaderManager().restartLoader(mCurrentLoaderId, null, this); 221 } 222 } 223 224 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)225 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 226 menu.add(0, MENU_DELETE_ALL, 0, R.string.menu_delete_all).setIcon( 227 android.R.drawable.ic_menu_delete); 228 menu.add(0, MENU_SHOW_ALL_MESSAGES, 0, R.string.show_all_messages); 229 menu.add(0, MENU_SHOW_REGULAR_MESSAGES, 0, R.string.show_regular_messages); 230 } 231 232 @Override onPrepareOptionsMenu(Menu menu)233 public void onPrepareOptionsMenu(Menu menu) { 234 boolean isTestingMode = CellBroadcastReceiver.isTestingMode( 235 getContext()); 236 // Only allowing delete all messages when not in testing mode because when testing mode 237 // is enabled, the database source is from cell broadcast service. Deleting them does 238 // not affect the database in cell broadcast receiver. Hide the options to reduce 239 // confusion. 240 menu.findItem(MENU_DELETE_ALL).setVisible(!mAdapter.isEmpty() && !isTestingMode); 241 menu.findItem(MENU_SHOW_ALL_MESSAGES).setVisible(isTestingMode 242 && mCurrentLoaderId == LOADER_NORMAL_HISTORY); 243 menu.findItem(MENU_SHOW_REGULAR_MESSAGES).setVisible(isTestingMode 244 && mCurrentLoaderId == LOADER_HISTORY_FROM_CBS); 245 } 246 247 @Override onListItemClick(ListView l, View v, int position, long id)248 public void onListItemClick(ListView l, View v, int position, long id) { 249 CellBroadcastListItem cbli = (CellBroadcastListItem) v; 250 showDialogAndMarkRead(cbli.getMessage()); 251 } 252 253 @Override onCreateLoader(int id, Bundle args)254 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 255 mCurrentLoaderId = id; 256 if (id == LOADER_NORMAL_HISTORY) { 257 Log.d(TAG, "onCreateLoader: normal history."); 258 return new CursorLoader(getActivity(), CellBroadcastContentProvider.CONTENT_URI, 259 CellBroadcastContentProvider.QUERY_COLUMNS, null, null, 260 Telephony.CellBroadcasts.DELIVERY_TIME + " DESC"); 261 } else if (id == LOADER_HISTORY_FROM_CBS) { 262 Log.d(TAG, "onCreateLoader: history from cell broadcast service"); 263 return new CursorLoader(getActivity(), CONTENT_URI, 264 QUERY_COLUMNS, null, null, 265 Telephony.CellBroadcasts.RECEIVED_TIME + " DESC"); 266 } 267 268 return null; 269 } 270 271 @Override onLoadFinished(Loader<Cursor> loader, Cursor data)272 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 273 if (DBG) Log.d(TAG, "onLoadFinished"); 274 // Swap the new cursor in. (The framework will take care of closing the 275 // old cursor once we return.) 276 mAdapter.swapCursor(data); 277 getActivity().invalidateOptionsMenu(); 278 updateNoAlertTextVisibility(); 279 } 280 281 @Override onLoaderReset(Loader<Cursor> loader)282 public void onLoaderReset(Loader<Cursor> loader) { 283 if (DBG) Log.d(TAG, "onLoaderReset"); 284 // This is called when the last Cursor provided to onLoadFinished() 285 // above is about to be closed. We need to make sure we are no 286 // longer using it. 287 mAdapter.swapCursor(null); 288 } 289 showDialogAndMarkRead(SmsCbMessage message)290 private void showDialogAndMarkRead(SmsCbMessage message) { 291 // show emergency alerts with the warning icon, but don't play alert tone 292 Intent i = new Intent(getActivity(), CellBroadcastAlertDialog.class); 293 ArrayList<SmsCbMessage> messageList = new ArrayList<>(); 294 messageList.add(message); 295 i.putParcelableArrayListExtra(CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA, 296 messageList); 297 startActivity(i); 298 } 299 showBroadcastDetails(SmsCbMessage message, long locationCheckTime, boolean messageDisplayed, String geometry)300 private void showBroadcastDetails(SmsCbMessage message, long locationCheckTime, 301 boolean messageDisplayed, String geometry) { 302 // show dialog with delivery date/time and alert details 303 CharSequence details = CellBroadcastResources.getMessageDetails(getActivity(), 304 mCurrentLoaderId == LOADER_HISTORY_FROM_CBS, message, locationCheckTime, 305 messageDisplayed, geometry); 306 int titleId = (mCurrentLoaderId == LOADER_NORMAL_HISTORY) 307 ? R.string.view_details_title : R.string.view_details_debugging_title; 308 new AlertDialog.Builder(getActivity()) 309 .setTitle(titleId) 310 .setMessage(details) 311 .setCancelable(true) 312 .show(); 313 } 314 315 private final OnCreateContextMenuListener mOnCreateContextMenuListener = 316 (menu, v, menuInfo) -> { 317 menu.setHeaderTitle(R.string.message_options); 318 menu.add(0, MENU_VIEW_DETAILS, 0, R.string.menu_view_details); 319 if (mCurrentLoaderId == LOADER_NORMAL_HISTORY) { 320 menu.add(0, MENU_DELETE, 0, R.string.menu_delete); 321 } 322 }; 323 updateNoAlertTextVisibility()324 private void updateNoAlertTextVisibility() { 325 TextView noAlertsTextView = getActivity().findViewById(R.id.empty); 326 if (noAlertsTextView != null) { 327 noAlertsTextView.setVisibility(!hasAlertsInHistory() 328 ? View.VISIBLE : View.INVISIBLE); 329 } 330 } 331 332 /** 333 * @return {@code true} if the alert history database has any item 334 */ hasAlertsInHistory()335 private boolean hasAlertsInHistory() { 336 return mAdapter.getCursor().getCount() > 0; 337 } 338 339 /** 340 * Get the location check time of the message. 341 * 342 * @param cursor The cursor of the database 343 * @return The EPOCH time in milliseconds that the location check was performed on the 344 * message. -1 if the information is not available. 345 */ getLocationCheckTime(Cursor cursor)346 private long getLocationCheckTime(Cursor cursor) { 347 if (mCurrentLoaderId != LOADER_HISTORY_FROM_CBS) return -1; 348 return cursor.getLong(cursor.getColumnIndex( 349 Telephony.CellBroadcasts.LOCATION_CHECK_TIME)); 350 } 351 352 /** 353 * Check if the message has been displayed to the user or not 354 * 355 * @param cursor The cursor of the database 356 * @return {@code true} if the message was displayed to the user, otherwise {@code false}. 357 */ wasMessageDisplayed(Cursor cursor)358 private boolean wasMessageDisplayed(Cursor cursor) { 359 if (mCurrentLoaderId != LOADER_HISTORY_FROM_CBS) return true; 360 return cursor.getInt(cursor.getColumnIndex( 361 Telephony.CellBroadcasts.MESSAGE_DISPLAYED)) != 0; 362 } 363 364 /** 365 * Get the geometry string from the message if available. 366 * 367 * @param cursor The cursor of the database 368 * @return The geometry string 369 */ getGeometryString(Cursor cursor)370 private @Nullable String getGeometryString(Cursor cursor) { 371 if (mCurrentLoaderId != LOADER_HISTORY_FROM_CBS) return null; 372 if (cursor.getColumnIndex(Telephony.CellBroadcasts.GEOMETRIES) >= 0) { 373 return cursor.getString(cursor.getColumnIndex(Telephony.CellBroadcasts.GEOMETRIES)); 374 } 375 return null; 376 } 377 378 @Override onContextItemSelected(MenuItem item)379 public boolean onContextItemSelected(MenuItem item) { 380 Cursor cursor = mAdapter.getCursor(); 381 if (cursor != null && cursor.getPosition() >= 0) { 382 switch (item.getItemId()) { 383 case MENU_DELETE: 384 confirmDeleteThread(cursor.getLong(cursor.getColumnIndexOrThrow( 385 Telephony.CellBroadcasts._ID))); 386 break; 387 388 case MENU_VIEW_DETAILS: 389 showBroadcastDetails(CellBroadcastCursorAdapter.createFromCursor( 390 getContext(), cursor), getLocationCheckTime(cursor), 391 wasMessageDisplayed(cursor), getGeometryString(cursor)); 392 break; 393 394 default: 395 break; 396 } 397 } 398 return super.onContextItemSelected(item); 399 } 400 401 @Override onOptionsItemSelected(MenuItem item)402 public boolean onOptionsItemSelected(MenuItem item) { 403 switch(item.getItemId()) { 404 case MENU_DELETE_ALL: 405 confirmDeleteThread(-1); 406 break; 407 408 case MENU_SHOW_ALL_MESSAGES: 409 getLoaderManager().restartLoader(LOADER_HISTORY_FROM_CBS, null, this); 410 break; 411 412 case MENU_SHOW_REGULAR_MESSAGES: 413 getLoaderManager().restartLoader(LOADER_NORMAL_HISTORY, null, this); 414 break; 415 416 default: 417 return true; 418 } 419 return false; 420 } 421 422 /** 423 * Start the process of putting up a dialog to confirm deleting a broadcast. 424 * @param rowId the row ID of the broadcast to delete, or -1 to delete all broadcasts 425 */ confirmDeleteThread(long rowId)426 public void confirmDeleteThread(long rowId) { 427 DeleteThreadListener listener = new DeleteThreadListener(rowId); 428 confirmDeleteThreadDialog(listener, (rowId == -1), getActivity()); 429 } 430 431 /** 432 * Build and show the proper delete broadcast dialog. The UI is slightly different 433 * depending on whether there are locked messages in the thread(s) and whether we're 434 * deleting a single broadcast or all broadcasts. 435 * @param listener gets called when the delete button is pressed 436 * @param deleteAll whether to show a single thread or all threads UI 437 * @param context used to load the various UI elements 438 */ confirmDeleteThreadDialog(DeleteThreadListener listener, boolean deleteAll, Context context)439 public static void confirmDeleteThreadDialog(DeleteThreadListener listener, 440 boolean deleteAll, Context context) { 441 AlertDialog.Builder builder = new AlertDialog.Builder(context); 442 builder.setIconAttribute(android.R.attr.alertDialogIcon) 443 .setCancelable(true) 444 .setPositiveButton(R.string.button_delete, listener) 445 .setNegativeButton(R.string.button_cancel, null) 446 .setMessage(deleteAll ? R.string.confirm_delete_all_broadcasts 447 : R.string.confirm_delete_broadcast) 448 .show(); 449 } 450 451 public class DeleteThreadListener implements OnClickListener { 452 private final long mRowId; 453 DeleteThreadListener(long rowId)454 public DeleteThreadListener(long rowId) { 455 mRowId = rowId; 456 } 457 458 @Override onClick(DialogInterface dialog, int whichButton)459 public void onClick(DialogInterface dialog, int whichButton) { 460 // delete from database on a background thread 461 new CellBroadcastContentProvider.AsyncCellBroadcastTask( 462 getActivity().getContentResolver()).execute( 463 (CellBroadcastContentProvider.CellBroadcastOperation) provider -> { 464 if (mRowId != -1) { 465 return provider.deleteBroadcast(mRowId); 466 } else { 467 return provider.deleteAllBroadcasts(); 468 } 469 }); 470 471 dialog.dismiss(); 472 } 473 } 474 } 475 } 476