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 static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; 20 21 import android.annotation.Nullable; 22 import android.app.ActionBar; 23 import android.app.AlertDialog; 24 import android.app.Dialog; 25 import android.app.DialogFragment; 26 import android.app.FragmentManager; 27 import android.app.ListFragment; 28 import android.app.LoaderManager; 29 import android.content.Context; 30 import android.content.CursorLoader; 31 import android.content.DialogInterface; 32 import android.content.DialogInterface.OnClickListener; 33 import android.content.Intent; 34 import android.content.Loader; 35 import android.content.pm.PackageManager; 36 import android.database.Cursor; 37 import android.net.Uri; 38 import android.os.Bundle; 39 import android.os.UserManager; 40 import android.provider.Telephony; 41 import android.telephony.SmsCbMessage; 42 import android.util.Log; 43 import android.util.SparseBooleanArray; 44 import android.view.ActionMode; 45 import android.view.LayoutInflater; 46 import android.view.Menu; 47 import android.view.MenuInflater; 48 import android.view.MenuItem; 49 import android.view.View; 50 import android.view.ViewGroup; 51 import android.view.WindowManager; 52 import android.widget.AbsListView.MultiChoiceModeListener; 53 import android.widget.ListView; 54 import android.widget.TextView; 55 56 import com.android.internal.annotations.VisibleForTesting; 57 import com.android.modules.utils.build.SdkLevel; 58 import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity; 59 60 import java.util.ArrayList; 61 62 /** 63 * This activity provides a list view of received cell broadcasts. Most of the work is handled 64 * in the inner CursorLoaderListFragment class. 65 */ 66 public class CellBroadcastListActivity extends CollapsingToolbarBaseActivity { 67 68 @VisibleForTesting 69 public CursorLoaderListFragment mListFragment; 70 71 @Override onCreate(Bundle savedInstanceState)72 protected void onCreate(Bundle savedInstanceState) { 73 boolean isWatch = getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH); 74 // for backward compatibility on R devices or wearable devices due to small screen device. 75 boolean hideToolbar = !SdkLevel.isAtLeastS() || isWatch; 76 if (hideToolbar) { 77 setCustomizeContentView(R.layout.cell_broadcast_list_collapsing_no_toobar); 78 } 79 super.onCreate(savedInstanceState); 80 if (hideToolbar) { 81 ActionBar actionBar = getActionBar(); 82 if (actionBar != null) { 83 // android.R.id.home will be triggered in onOptionsItemSelected() 84 actionBar.setDisplayHomeAsUpEnabled(true); 85 } 86 } 87 88 setTitle(getString(R.string.cb_list_activity_title)); 89 90 FragmentManager fm = getFragmentManager(); 91 92 // Create the list fragment and add it as our sole content. 93 if (fm.findFragmentById(com.android.settingslib.collapsingtoolbar.R.id.content_frame) 94 == null) { 95 mListFragment = new CursorLoaderListFragment(); 96 mListFragment.setActivity(this); 97 fm.beginTransaction().add(com.android.settingslib.collapsingtoolbar.R.id.content_frame, 98 mListFragment).commit(); 99 } 100 101 if (CellBroadcastSettings.getResourcesForDefaultSubId(getApplicationContext()).getBoolean( 102 R.bool.disable_capture_alert_dialog)) { 103 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); 104 } 105 } 106 107 @Override onStart()108 public void onStart() { 109 super.onStart(); 110 getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 111 } 112 113 @Override onOptionsItemSelected(MenuItem item)114 public boolean onOptionsItemSelected(MenuItem item) { 115 switch (item.getItemId()) { 116 // Respond to the action bar's Up/Home button 117 case android.R.id.home: 118 finish(); 119 return true; 120 } 121 return super.onOptionsItemSelected(item); 122 } 123 124 /** 125 * List fragment queries SQLite database on worker thread. 126 */ 127 public static class CursorLoaderListFragment extends ListFragment 128 implements LoaderManager.LoaderCallbacks<Cursor> { 129 private static final String TAG = CellBroadcastListActivity.class.getSimpleName(); 130 private static final boolean DBG = true; 131 132 // IDs of the main menu items. 133 @VisibleForTesting 134 public static final int MENU_DELETE_ALL = 3; 135 @VisibleForTesting 136 public static final int MENU_SHOW_REGULAR_MESSAGES = 4; 137 @VisibleForTesting 138 public static final int MENU_SHOW_ALL_MESSAGES = 5; 139 @VisibleForTesting 140 public static final int MENU_PREFERENCES = 6; 141 142 // Load the history from cell broadcast receiver database 143 private static final int LOADER_NORMAL_HISTORY = 1; 144 // Load the history from cell broadcast service. This will include all non-shown messages. 145 @VisibleForTesting 146 public static final int LOADER_HISTORY_FROM_CBS = 2; 147 148 @VisibleForTesting 149 public static final String KEY_LOADER_ID = "loader_id"; 150 151 public static final String KEY_DELETE_DIALOG = "delete_dialog"; 152 153 // IDs of the context menu items (package local, accessed from inner DeleteThreadListener). 154 @VisibleForTesting 155 public static final int MENU_DELETE = 0; 156 @VisibleForTesting 157 public static final int MENU_VIEW_DETAILS = 1; 158 159 // cell broadcast provider from cell broadcast service. 160 public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts"); 161 162 // Query columns for provider from cell broadcast service. 163 public static final String[] QUERY_COLUMNS = { 164 Telephony.CellBroadcasts._ID, 165 Telephony.CellBroadcasts.SLOT_INDEX, 166 Telephony.CellBroadcasts.SUBSCRIPTION_ID, 167 Telephony.CellBroadcasts.GEOGRAPHICAL_SCOPE, 168 Telephony.CellBroadcasts.PLMN, 169 Telephony.CellBroadcasts.LAC, 170 Telephony.CellBroadcasts.CID, 171 Telephony.CellBroadcasts.SERIAL_NUMBER, 172 Telephony.CellBroadcasts.SERVICE_CATEGORY, 173 Telephony.CellBroadcasts.LANGUAGE_CODE, 174 Telephony.CellBroadcasts.DATA_CODING_SCHEME, 175 Telephony.CellBroadcasts.MESSAGE_BODY, 176 Telephony.CellBroadcasts.MESSAGE_FORMAT, 177 Telephony.CellBroadcasts.MESSAGE_PRIORITY, 178 Telephony.CellBroadcasts.ETWS_WARNING_TYPE, 179 Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS, 180 Telephony.CellBroadcasts.CMAS_CATEGORY, 181 Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE, 182 Telephony.CellBroadcasts.CMAS_SEVERITY, 183 Telephony.CellBroadcasts.CMAS_URGENCY, 184 Telephony.CellBroadcasts.CMAS_CERTAINTY, 185 Telephony.CellBroadcasts.RECEIVED_TIME, 186 Telephony.CellBroadcasts.LOCATION_CHECK_TIME, 187 Telephony.CellBroadcasts.MESSAGE_BROADCASTED, 188 Telephony.CellBroadcasts.MESSAGE_DISPLAYED, 189 Telephony.CellBroadcasts.GEOMETRIES, 190 Telephony.CellBroadcasts.MAXIMUM_WAIT_TIME 191 }; 192 193 // This is the Adapter being used to display the list's data. 194 @VisibleForTesting 195 public CellBroadcastCursorAdapter mAdapter; 196 197 private int mCurrentLoaderId = 0; 198 199 private MenuItem mInformationMenuItem; 200 201 private MultiChoiceModeListener mListener; 202 203 private CellBroadcastListActivity mActivity; 204 205 private boolean mIsWatch; 206 207 @VisibleForTesting 208 public AlertDialog.Builder mInjectAlertDialogBuilder; 209 setActivity(CellBroadcastListActivity activity)210 void setActivity(CellBroadcastListActivity activity) { 211 mActivity = activity; 212 } 213 214 @Override onCreate(Bundle savedInstanceState)215 public void onCreate(Bundle savedInstanceState) { 216 super.onCreate(savedInstanceState); 217 218 // We have a menu item to show in action bar. 219 setHasOptionsMenu(true); 220 } 221 222 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)223 public View onCreateView(LayoutInflater inflater, ViewGroup container, 224 Bundle savedInstanceState) { 225 return inflater.inflate(R.layout.cell_broadcast_list_screen, container, false); 226 } 227 228 @Override onActivityCreated(Bundle savedInstanceState)229 public void onActivityCreated(Bundle savedInstanceState) { 230 super.onActivityCreated(savedInstanceState); 231 232 // Set context menu for long-press. 233 ListView listView = getListView(); 234 235 // Create a cursor adapter to display the loaded data. 236 mAdapter = new CellBroadcastCursorAdapter(getActivity(), listView); 237 setListAdapter(mAdapter); 238 // Watch UI does not support multi-choice deletion, so still needs to have 239 // the traditional per-item delete option. 240 mIsWatch = getContext().getPackageManager().hasSystemFeature( 241 PackageManager.FEATURE_WATCH); 242 if (mIsWatch) { 243 listView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { 244 menu.setHeaderTitle(R.string.message_options); 245 menu.add(0, MENU_VIEW_DETAILS, 0, R.string.menu_view_details); 246 if (mCurrentLoaderId == LOADER_NORMAL_HISTORY) { 247 menu.add(0, MENU_DELETE, 0, R.string.menu_delete); 248 } 249 }); 250 } else { 251 listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); 252 listView.setMultiChoiceModeListener(getMultiChoiceModeListener()); 253 } 254 255 mCurrentLoaderId = LOADER_NORMAL_HISTORY; 256 if (savedInstanceState != null && savedInstanceState.containsKey(KEY_LOADER_ID)) { 257 mCurrentLoaderId = savedInstanceState.getInt(KEY_LOADER_ID); 258 } 259 260 if (DBG) Log.d(TAG, "onActivityCreated: id=" + mCurrentLoaderId); 261 262 // Prepare the loader. Either re-connect with an existing one, 263 // or start a new one. 264 getLoaderManager().initLoader(mCurrentLoaderId, null, this); 265 } 266 267 @Override onSaveInstanceState(Bundle outState)268 public void onSaveInstanceState(Bundle outState) { 269 // Save the current id for later restoring activity. 270 if (DBG) Log.d(TAG, "onSaveInstanceState: id=" + mCurrentLoaderId); 271 outState.putInt(KEY_LOADER_ID, mCurrentLoaderId); 272 } 273 274 @Override onResume()275 public void onResume() { 276 super.onResume(); 277 if (DBG) Log.d(TAG, "onResume"); 278 if (mCurrentLoaderId != 0) { 279 getLoaderManager().restartLoader(mCurrentLoaderId, null, this); 280 } 281 } 282 283 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)284 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 285 menu.add(0, MENU_DELETE_ALL, 0, R.string.menu_delete_all).setIcon( 286 android.R.drawable.ic_menu_delete); 287 menu.add(0, MENU_SHOW_ALL_MESSAGES, 0, R.string.show_all_messages); 288 menu.add(0, MENU_SHOW_REGULAR_MESSAGES, 0, R.string.show_regular_messages); 289 final UserManager userManager = getContext().getSystemService(UserManager.class); 290 if (userManager.isAdminUser()) { 291 menu.add(0, MENU_PREFERENCES, 0, R.string.menu_preferences).setIcon( 292 android.R.drawable.ic_menu_preferences); 293 } 294 } 295 296 @Override onPrepareOptionsMenu(Menu menu)297 public void onPrepareOptionsMenu(Menu menu) { 298 boolean isTestingMode = CellBroadcastReceiver.isTestingMode( 299 getContext()); 300 // Only allowing delete all messages when not in testing mode because when testing mode 301 // is enabled, the database source is from cell broadcast service. Deleting them does 302 // not affect the database in cell broadcast receiver. Hide the options to reduce 303 // confusion. 304 menu.findItem(MENU_DELETE_ALL).setVisible(!mAdapter.isEmpty() && !isTestingMode); 305 menu.findItem(MENU_SHOW_ALL_MESSAGES).setVisible(isTestingMode 306 && mCurrentLoaderId == LOADER_NORMAL_HISTORY); 307 menu.findItem(MENU_SHOW_REGULAR_MESSAGES).setVisible(isTestingMode 308 && mCurrentLoaderId == LOADER_HISTORY_FROM_CBS); 309 } 310 311 @Override onListItemClick(ListView l, View v, int position, long id)312 public void onListItemClick(ListView l, View v, int position, long id) { 313 CellBroadcastListItem cbli = (CellBroadcastListItem) v; 314 showDialogAndMarkRead(cbli.getMessage()); 315 } 316 317 @Override onCreateLoader(int id, Bundle args)318 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 319 mCurrentLoaderId = id; 320 if (id == LOADER_NORMAL_HISTORY) { 321 Log.d(TAG, "onCreateLoader: normal history."); 322 return new CursorLoader(getActivity(), CellBroadcastContentProvider.CONTENT_URI, 323 CellBroadcastDatabaseHelper.QUERY_COLUMNS, null, null, 324 Telephony.CellBroadcasts.DELIVERY_TIME + " DESC"); 325 } else if (id == LOADER_HISTORY_FROM_CBS) { 326 Log.d(TAG, "onCreateLoader: history from cell broadcast service"); 327 return new CursorLoader(getActivity(), CONTENT_URI, 328 QUERY_COLUMNS, null, null, 329 Telephony.CellBroadcasts.RECEIVED_TIME + " DESC"); 330 } 331 332 return null; 333 } 334 335 @Override onLoadFinished(Loader<Cursor> loader, Cursor data)336 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 337 if (DBG) Log.d(TAG, "onLoadFinished"); 338 // Swap the new cursor in. (The framework will take care of closing the 339 // old cursor once we return.) 340 mAdapter.swapCursor(data); 341 getActivity().invalidateOptionsMenu(); 342 updateNoAlertTextVisibility(); 343 } 344 345 @Override onLoaderReset(Loader<Cursor> loader)346 public void onLoaderReset(Loader<Cursor> loader) { 347 if (DBG) Log.d(TAG, "onLoaderReset"); 348 // This is called when the last Cursor provided to onLoadFinished() 349 // above is about to be closed. We need to make sure we are no 350 // longer using it. 351 mAdapter.swapCursor(null); 352 } 353 showDialogAndMarkRead(SmsCbMessage message)354 private void showDialogAndMarkRead(SmsCbMessage message) { 355 // show emergency alerts with the warning icon, but don't play alert tone 356 Intent i = new Intent(getActivity(), CellBroadcastAlertDialog.class); 357 ArrayList<SmsCbMessage> messageList = new ArrayList<>(); 358 messageList.add(message); 359 i.putParcelableArrayListExtra(CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA, 360 messageList); 361 startActivity(i); 362 } 363 showBroadcastDetails(SmsCbMessage message, long locationCheckTime, boolean messageDisplayed, String geometry)364 private void showBroadcastDetails(SmsCbMessage message, long locationCheckTime, 365 boolean messageDisplayed, String geometry) { 366 // show dialog with delivery date/time and alert details 367 CharSequence details = CellBroadcastResources.getMessageDetails(getActivity(), 368 mCurrentLoaderId == LOADER_HISTORY_FROM_CBS, message, locationCheckTime, 369 messageDisplayed, geometry); 370 int titleId = (mCurrentLoaderId == LOADER_NORMAL_HISTORY) 371 ? R.string.view_details_title : R.string.view_details_debugging_title; 372 AlertDialog.Builder dialogBuilder = mInjectAlertDialogBuilder != null 373 ? mInjectAlertDialogBuilder : new AlertDialog.Builder(getActivity()); 374 dialogBuilder 375 .setTitle(titleId) 376 .setMessage(details) 377 .setCancelable(true) 378 .show(); 379 } 380 updateActionIconsVisibility()381 private void updateActionIconsVisibility() { 382 if (mInformationMenuItem != null) { 383 int checkedCount = getListView().getCheckedItemCount(); 384 if (checkedCount == 1) { 385 mInformationMenuItem.setVisible(true); 386 } else { 387 mInformationMenuItem.setVisible(false); 388 } 389 } 390 } 391 getSelectedItemSingle()392 private Cursor getSelectedItemSingle() { 393 int checkedCount = getListView().getCheckedItemCount(); 394 if (checkedCount == 1) { 395 SparseBooleanArray checkStates = getListView().getCheckedItemPositions(); 396 if (checkStates != null) { 397 int pos = checkStates.keyAt(0); 398 Cursor cursor = (Cursor) getListView().getItemAtPosition(pos); 399 return cursor; 400 } 401 } 402 return null; 403 } 404 getSelectedItemsRowId()405 private long[] getSelectedItemsRowId() { 406 if (mIsWatch) { 407 Cursor cursor = mAdapter.getCursor(); 408 long id = cursor.getLong(cursor.getColumnIndexOrThrow( 409 Telephony.CellBroadcasts._ID)); 410 return new long [] { id }; 411 } 412 413 SparseBooleanArray checkStates = getListView().getCheckedItemPositions(); 414 long[] arr = new long[checkStates.size()]; 415 for (int i = 0; i < checkStates.size(); i++) { 416 int pos = checkStates.keyAt(i); 417 Cursor cursor = (Cursor) getListView().getItemAtPosition(pos); 418 long rowId = cursor.getLong(cursor.getColumnIndex( 419 Telephony.CellBroadcasts._ID)); 420 arr[i] = rowId; 421 } 422 return arr; 423 } 424 updateNoAlertTextVisibility()425 private void updateNoAlertTextVisibility() { 426 TextView noAlertsTextView = getActivity().findViewById(R.id.empty); 427 if (noAlertsTextView != null) { 428 noAlertsTextView.setVisibility(!hasAlertsInHistory() 429 ? View.VISIBLE : View.INVISIBLE); 430 getListView().setLongClickable(hasAlertsInHistory()); 431 if (!hasAlertsInHistory()) { 432 getListView().setContentDescription(getString(R.string.no_cell_broadcasts)); 433 } 434 } 435 } 436 437 /** 438 * @return {@code true} if the alert history database has any item 439 */ hasAlertsInHistory()440 private boolean hasAlertsInHistory() { 441 return mAdapter.getCursor().getCount() > 0; 442 } 443 444 /** 445 * Get the location check time of the message. 446 * 447 * @param cursor The cursor of the database 448 * @return The EPOCH time in milliseconds that the location check was performed on the 449 * message. -1 if the information is not available. 450 */ getLocationCheckTime(Cursor cursor)451 private long getLocationCheckTime(Cursor cursor) { 452 if (mCurrentLoaderId != LOADER_HISTORY_FROM_CBS) return -1; 453 return cursor.getLong(cursor.getColumnIndex( 454 Telephony.CellBroadcasts.LOCATION_CHECK_TIME)); 455 } 456 457 /** 458 * Check if the message has been displayed to the user or not 459 * 460 * @param cursor The cursor of the database 461 * @return {@code true} if the message was displayed to the user, otherwise {@code false}. 462 */ wasMessageDisplayed(Cursor cursor)463 private boolean wasMessageDisplayed(Cursor cursor) { 464 if (mCurrentLoaderId != LOADER_HISTORY_FROM_CBS) return true; 465 return cursor.getInt(cursor.getColumnIndex( 466 Telephony.CellBroadcasts.MESSAGE_DISPLAYED)) != 0; 467 } 468 469 /** 470 * Get the geometry string from the message if available. 471 * 472 * @param cursor The cursor of the database 473 * @return The geometry string 474 */ getGeometryString(Cursor cursor)475 private @Nullable String getGeometryString(Cursor cursor) { 476 if (mCurrentLoaderId != LOADER_HISTORY_FROM_CBS) return null; 477 if (cursor.getColumnIndex(Telephony.CellBroadcasts.GEOMETRIES) >= 0) { 478 return cursor.getString(cursor.getColumnIndex(Telephony.CellBroadcasts.GEOMETRIES)); 479 } 480 return null; 481 } 482 483 @Override onContextItemSelected(MenuItem item)484 public boolean onContextItemSelected(MenuItem item) { 485 Cursor cursor = mAdapter.getCursor(); 486 if (cursor != null && cursor.getPosition() >= 0) { 487 switch (item.getItemId()) { 488 case MENU_DELETE: 489 long[] selectedRowId = getSelectedItemsRowId(); 490 confirmDeleteThread(selectedRowId); 491 break; 492 493 case MENU_VIEW_DETAILS: 494 showBroadcastDetails(CellBroadcastCursorAdapter.createFromCursor( 495 getContext(), cursor), getLocationCheckTime(cursor), 496 wasMessageDisplayed(cursor), getGeometryString(cursor)); 497 break; 498 499 default: 500 break; 501 } 502 } 503 return super.onContextItemSelected(item); 504 } 505 506 @Override onOptionsItemSelected(MenuItem item)507 public boolean onOptionsItemSelected(MenuItem item) { 508 switch(item.getItemId()) { 509 case MENU_DELETE_ALL: 510 long[] deleteAll = {-1}; 511 confirmDeleteThread(deleteAll); 512 break; 513 514 case MENU_SHOW_ALL_MESSAGES: 515 getLoaderManager().restartLoader(LOADER_HISTORY_FROM_CBS, null, this); 516 break; 517 518 case MENU_SHOW_REGULAR_MESSAGES: 519 getLoaderManager().restartLoader(LOADER_NORMAL_HISTORY, null, this); 520 break; 521 522 case MENU_PREFERENCES: 523 Intent intent = new Intent(getActivity(), CellBroadcastSettings.class); 524 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP 525 | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); 526 startActivity(intent); 527 if (mActivity != null) { 528 mActivity.finish(); 529 } 530 break; 531 532 default: 533 return true; 534 } 535 return false; 536 } 537 538 /** 539 * Get MultiChoiceModeListener object 540 * 541 * @return MultiChoiceModeListener 542 */ 543 @VisibleForTesting getMultiChoiceModeListener()544 public synchronized MultiChoiceModeListener getMultiChoiceModeListener() { 545 if (mListener == null) { 546 mListener = new MultiChoiceModeListener() { 547 @Override 548 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 549 mode.getMenuInflater().inflate(R.menu.cell_broadcast_list_action_menu, 550 menu); 551 mInformationMenuItem = menu.findItem(R.id.action_detail_info); 552 mAdapter.setIsActionMode(true); 553 mAdapter.notifyDataSetChanged(); 554 updateActionIconsVisibility(); 555 if (getListView().getCheckedItemCount() > 0) { 556 mode.setTitle(String.valueOf(getListView().getCheckedItemCount())); 557 } 558 return true; 559 } 560 561 @Override 562 public void onDestroyActionMode(ActionMode mode) { 563 mAdapter.setIsActionMode(false); 564 mAdapter.notifyDataSetChanged(); 565 } 566 567 @Override 568 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 569 if (item.getItemId() == R.id.action_detail_info) { 570 Cursor cursor = getSelectedItemSingle(); 571 if (cursor != null) { 572 showBroadcastDetails(CellBroadcastCursorAdapter.createFromCursor( 573 getContext(), cursor), getLocationCheckTime(cursor), 574 wasMessageDisplayed(cursor), getGeometryString(cursor)); 575 } else { 576 Log.e(TAG, "Multiple items selected with action_detail_info"); 577 } 578 mode.finish(); 579 return true; 580 } else if (item.getItemId() == R.id.action_delete) { 581 long[] selectedRowId = getSelectedItemsRowId(); 582 confirmDeleteThread(selectedRowId); 583 mode.finish(); 584 return true; 585 } else { 586 Log.e(TAG, "onActionItemClicked: unsupported action return false"); 587 return false; 588 } 589 } 590 591 @Override 592 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 593 return false; 594 } 595 596 @Override 597 public void onItemCheckedStateChanged( 598 ActionMode mode, int position, long id, boolean checked) { 599 int checkedCount = getListView().getCheckedItemCount(); 600 601 updateActionIconsVisibility(); 602 mode.setTitle(String.valueOf(checkedCount)); 603 mAdapter.notifyDataSetChanged(); 604 } 605 }; 606 } 607 return mListener; 608 } 609 610 /** 611 * Start the process of putting up a dialog to confirm deleting a broadcast. 612 * @param rowId array of the row ID that the broadcast to delete, 613 * or rowId[0] = -1 to delete all broadcasts 614 */ confirmDeleteThread(long[] rowId)615 public void confirmDeleteThread(long[] rowId) { 616 DeleteDialogFragment dialog = new DeleteDialogFragment(); 617 Bundle dialogArgs = new Bundle(); 618 dialogArgs.putLongArray(DeleteDialogFragment.ROW_ID, rowId); 619 dialog.setArguments(dialogArgs); 620 dialog.show(getFragmentManager(), KEY_DELETE_DIALOG); 621 } 622 623 public static class DeleteDialogFragment extends DialogFragment { 624 /** 625 * Key for the row id of the message to delete. If the row id is -1, the displayed 626 * dialog will indicate that all messages are to be deleted. 627 */ 628 public static final String ROW_ID = "row_id"; 629 @Override onCreateDialog(Bundle savedInstanceState)630 public Dialog onCreateDialog(Bundle savedInstanceState) { 631 setRetainInstance(true); 632 long[] rowId = getArguments().getLongArray(ROW_ID); 633 boolean deleteAll = rowId[0] == -1; 634 DeleteThreadListener listener = new DeleteThreadListener(getActivity(), rowId); 635 AlertDialog.Builder builder = new AlertDialog.Builder( 636 DeleteDialogFragment.this.getActivity()); 637 builder.setIconAttribute(android.R.attr.alertDialogIcon) 638 .setCancelable(true) 639 .setPositiveButton(R.string.button_delete, listener) 640 .setNegativeButton(R.string.button_cancel, null) 641 .setMessage(deleteAll ? R.string.confirm_delete_all_broadcasts 642 : R.string.confirm_delete_broadcast); 643 return builder.create(); 644 } 645 646 @Override onDestroyView()647 public void onDestroyView() { 648 Dialog dialog = getDialog(); 649 if (dialog != null && getRetainInstance()) { 650 dialog.setDismissMessage(null); 651 } 652 super.onDestroyView(); 653 } 654 } 655 656 public static class DeleteThreadListener implements OnClickListener { 657 private final long[] mRowId; 658 private final Context mContext; 659 DeleteThreadListener(Context context, long[] rowId)660 public DeleteThreadListener(Context context, long[] rowId) { 661 mContext = context; 662 mRowId = rowId; 663 } 664 665 @Override onClick(DialogInterface dialog, int whichButton)666 public void onClick(DialogInterface dialog, int whichButton) { 667 // delete from database on a background thread 668 new CellBroadcastContentProvider.AsyncCellBroadcastTask( 669 mContext.getContentResolver()).execute( 670 (CellBroadcastContentProvider.CellBroadcastOperation) provider -> { 671 if (mRowId[0] != -1) { 672 for (int i = 0; i < mRowId.length; i++) { 673 if (!provider.deleteBroadcast(mRowId[i])) { 674 Log.e(TAG, "failed to delete at row " + mRowId[i]); 675 } 676 } 677 return true; 678 } else { 679 return provider.deleteAllBroadcasts(); 680 } 681 }); 682 683 dialog.dismiss(); 684 } 685 } 686 } 687 } 688