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.widget.R.id.content_frame) 94 == null) { 95 mListFragment = new CursorLoaderListFragment(); 96 mListFragment.setActivity(this); 97 fm.beginTransaction().add(com.android.settingslib.widget.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 setActivity(CellBroadcastListActivity activity)207 void setActivity(CellBroadcastListActivity activity) { 208 mActivity = activity; 209 } 210 211 @Override onCreate(Bundle savedInstanceState)212 public void onCreate(Bundle savedInstanceState) { 213 super.onCreate(savedInstanceState); 214 215 // We have a menu item to show in action bar. 216 setHasOptionsMenu(true); 217 } 218 219 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)220 public View onCreateView(LayoutInflater inflater, ViewGroup container, 221 Bundle savedInstanceState) { 222 return inflater.inflate(R.layout.cell_broadcast_list_screen, container, false); 223 } 224 225 @Override onActivityCreated(Bundle savedInstanceState)226 public void onActivityCreated(Bundle savedInstanceState) { 227 super.onActivityCreated(savedInstanceState); 228 229 // Set context menu for long-press. 230 ListView listView = getListView(); 231 232 // Create a cursor adapter to display the loaded data. 233 mAdapter = new CellBroadcastCursorAdapter(getActivity(), listView); 234 setListAdapter(mAdapter); 235 // Watch UI does not support multi-choice deletion, so still needs to have 236 // the traditional per-item delete option. 237 mIsWatch = getContext().getPackageManager().hasSystemFeature( 238 PackageManager.FEATURE_WATCH); 239 if (mIsWatch) { 240 listView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { 241 menu.setHeaderTitle(R.string.message_options); 242 menu.add(0, MENU_VIEW_DETAILS, 0, R.string.menu_view_details); 243 if (mCurrentLoaderId == LOADER_NORMAL_HISTORY) { 244 menu.add(0, MENU_DELETE, 0, R.string.menu_delete); 245 } 246 }); 247 } else { 248 listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); 249 listView.setMultiChoiceModeListener(getMultiChoiceModeListener()); 250 } 251 252 mCurrentLoaderId = LOADER_NORMAL_HISTORY; 253 if (savedInstanceState != null && savedInstanceState.containsKey(KEY_LOADER_ID)) { 254 mCurrentLoaderId = savedInstanceState.getInt(KEY_LOADER_ID); 255 } 256 257 if (DBG) Log.d(TAG, "onActivityCreated: id=" + mCurrentLoaderId); 258 259 // Prepare the loader. Either re-connect with an existing one, 260 // or start a new one. 261 getLoaderManager().initLoader(mCurrentLoaderId, null, this); 262 } 263 264 @Override onSaveInstanceState(Bundle outState)265 public void onSaveInstanceState(Bundle outState) { 266 // Save the current id for later restoring activity. 267 if (DBG) Log.d(TAG, "onSaveInstanceState: id=" + mCurrentLoaderId); 268 outState.putInt(KEY_LOADER_ID, mCurrentLoaderId); 269 } 270 271 @Override onResume()272 public void onResume() { 273 super.onResume(); 274 if (DBG) Log.d(TAG, "onResume"); 275 if (mCurrentLoaderId != 0) { 276 getLoaderManager().restartLoader(mCurrentLoaderId, null, this); 277 } 278 } 279 280 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)281 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 282 menu.add(0, MENU_DELETE_ALL, 0, R.string.menu_delete_all).setIcon( 283 android.R.drawable.ic_menu_delete); 284 menu.add(0, MENU_SHOW_ALL_MESSAGES, 0, R.string.show_all_messages); 285 menu.add(0, MENU_SHOW_REGULAR_MESSAGES, 0, R.string.show_regular_messages); 286 final UserManager userManager = getContext().getSystemService(UserManager.class); 287 if (userManager.isAdminUser()) { 288 menu.add(0, MENU_PREFERENCES, 0, R.string.menu_preferences).setIcon( 289 android.R.drawable.ic_menu_preferences); 290 } 291 } 292 293 @Override onPrepareOptionsMenu(Menu menu)294 public void onPrepareOptionsMenu(Menu menu) { 295 boolean isTestingMode = CellBroadcastReceiver.isTestingMode( 296 getContext()); 297 // Only allowing delete all messages when not in testing mode because when testing mode 298 // is enabled, the database source is from cell broadcast service. Deleting them does 299 // not affect the database in cell broadcast receiver. Hide the options to reduce 300 // confusion. 301 menu.findItem(MENU_DELETE_ALL).setVisible(!mAdapter.isEmpty() && !isTestingMode); 302 menu.findItem(MENU_SHOW_ALL_MESSAGES).setVisible(isTestingMode 303 && mCurrentLoaderId == LOADER_NORMAL_HISTORY); 304 menu.findItem(MENU_SHOW_REGULAR_MESSAGES).setVisible(isTestingMode 305 && mCurrentLoaderId == LOADER_HISTORY_FROM_CBS); 306 } 307 308 @Override onListItemClick(ListView l, View v, int position, long id)309 public void onListItemClick(ListView l, View v, int position, long id) { 310 CellBroadcastListItem cbli = (CellBroadcastListItem) v; 311 showDialogAndMarkRead(cbli.getMessage()); 312 } 313 314 @Override onCreateLoader(int id, Bundle args)315 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 316 mCurrentLoaderId = id; 317 if (id == LOADER_NORMAL_HISTORY) { 318 Log.d(TAG, "onCreateLoader: normal history."); 319 return new CursorLoader(getActivity(), CellBroadcastContentProvider.CONTENT_URI, 320 CellBroadcastDatabaseHelper.QUERY_COLUMNS, null, null, 321 Telephony.CellBroadcasts.DELIVERY_TIME + " DESC"); 322 } else if (id == LOADER_HISTORY_FROM_CBS) { 323 Log.d(TAG, "onCreateLoader: history from cell broadcast service"); 324 return new CursorLoader(getActivity(), CONTENT_URI, 325 QUERY_COLUMNS, null, null, 326 Telephony.CellBroadcasts.RECEIVED_TIME + " DESC"); 327 } 328 329 return null; 330 } 331 332 @Override onLoadFinished(Loader<Cursor> loader, Cursor data)333 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 334 if (DBG) Log.d(TAG, "onLoadFinished"); 335 // Swap the new cursor in. (The framework will take care of closing the 336 // old cursor once we return.) 337 mAdapter.swapCursor(data); 338 getActivity().invalidateOptionsMenu(); 339 updateNoAlertTextVisibility(); 340 } 341 342 @Override onLoaderReset(Loader<Cursor> loader)343 public void onLoaderReset(Loader<Cursor> loader) { 344 if (DBG) Log.d(TAG, "onLoaderReset"); 345 // This is called when the last Cursor provided to onLoadFinished() 346 // above is about to be closed. We need to make sure we are no 347 // longer using it. 348 mAdapter.swapCursor(null); 349 } 350 showDialogAndMarkRead(SmsCbMessage message)351 private void showDialogAndMarkRead(SmsCbMessage message) { 352 // show emergency alerts with the warning icon, but don't play alert tone 353 Intent i = new Intent(getActivity(), CellBroadcastAlertDialog.class); 354 ArrayList<SmsCbMessage> messageList = new ArrayList<>(); 355 messageList.add(message); 356 i.putParcelableArrayListExtra(CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA, 357 messageList); 358 startActivity(i); 359 } 360 showBroadcastDetails(SmsCbMessage message, long locationCheckTime, boolean messageDisplayed, String geometry)361 private void showBroadcastDetails(SmsCbMessage message, long locationCheckTime, 362 boolean messageDisplayed, String geometry) { 363 // show dialog with delivery date/time and alert details 364 CharSequence details = CellBroadcastResources.getMessageDetails(getActivity(), 365 mCurrentLoaderId == LOADER_HISTORY_FROM_CBS, message, locationCheckTime, 366 messageDisplayed, geometry); 367 int titleId = (mCurrentLoaderId == LOADER_NORMAL_HISTORY) 368 ? R.string.view_details_title : R.string.view_details_debugging_title; 369 new AlertDialog.Builder(getActivity()) 370 .setTitle(titleId) 371 .setMessage(details) 372 .setCancelable(true) 373 .show(); 374 } 375 updateActionIconsVisibility()376 private void updateActionIconsVisibility() { 377 if (mInformationMenuItem != null) { 378 int checkedCount = getListView().getCheckedItemCount(); 379 if (checkedCount == 1) { 380 mInformationMenuItem.setVisible(true); 381 } else { 382 mInformationMenuItem.setVisible(false); 383 } 384 } 385 } 386 getSelectedItemSingle()387 private Cursor getSelectedItemSingle() { 388 int checkedCount = getListView().getCheckedItemCount(); 389 if (checkedCount == 1) { 390 SparseBooleanArray checkStates = getListView().getCheckedItemPositions(); 391 if (checkStates != null) { 392 int pos = checkStates.keyAt(0); 393 Cursor cursor = (Cursor) getListView().getItemAtPosition(pos); 394 return cursor; 395 } 396 } 397 return null; 398 } 399 getSelectedItemsRowId()400 private long[] getSelectedItemsRowId() { 401 if (mIsWatch) { 402 Cursor cursor = mAdapter.getCursor(); 403 long id = cursor.getLong(cursor.getColumnIndexOrThrow( 404 Telephony.CellBroadcasts._ID)); 405 return new long [] { id }; 406 } 407 408 SparseBooleanArray checkStates = getListView().getCheckedItemPositions(); 409 long[] arr = new long[checkStates.size()]; 410 for (int i = 0; i < checkStates.size(); i++) { 411 int pos = checkStates.keyAt(i); 412 Cursor cursor = (Cursor) getListView().getItemAtPosition(pos); 413 long rowId = cursor.getLong(cursor.getColumnIndex( 414 Telephony.CellBroadcasts._ID)); 415 arr[i] = rowId; 416 } 417 return arr; 418 } 419 updateNoAlertTextVisibility()420 private void updateNoAlertTextVisibility() { 421 TextView noAlertsTextView = getActivity().findViewById(R.id.empty); 422 if (noAlertsTextView != null) { 423 noAlertsTextView.setVisibility(!hasAlertsInHistory() 424 ? View.VISIBLE : View.INVISIBLE); 425 if (!hasAlertsInHistory()) { 426 getListView().setContentDescription(getString(R.string.no_cell_broadcasts)); 427 } 428 } 429 } 430 431 /** 432 * @return {@code true} if the alert history database has any item 433 */ hasAlertsInHistory()434 private boolean hasAlertsInHistory() { 435 return mAdapter.getCursor().getCount() > 0; 436 } 437 438 /** 439 * Get the location check time of the message. 440 * 441 * @param cursor The cursor of the database 442 * @return The EPOCH time in milliseconds that the location check was performed on the 443 * message. -1 if the information is not available. 444 */ getLocationCheckTime(Cursor cursor)445 private long getLocationCheckTime(Cursor cursor) { 446 if (mCurrentLoaderId != LOADER_HISTORY_FROM_CBS) return -1; 447 return cursor.getLong(cursor.getColumnIndex( 448 Telephony.CellBroadcasts.LOCATION_CHECK_TIME)); 449 } 450 451 /** 452 * Check if the message has been displayed to the user or not 453 * 454 * @param cursor The cursor of the database 455 * @return {@code true} if the message was displayed to the user, otherwise {@code false}. 456 */ wasMessageDisplayed(Cursor cursor)457 private boolean wasMessageDisplayed(Cursor cursor) { 458 if (mCurrentLoaderId != LOADER_HISTORY_FROM_CBS) return true; 459 return cursor.getInt(cursor.getColumnIndex( 460 Telephony.CellBroadcasts.MESSAGE_DISPLAYED)) != 0; 461 } 462 463 /** 464 * Get the geometry string from the message if available. 465 * 466 * @param cursor The cursor of the database 467 * @return The geometry string 468 */ getGeometryString(Cursor cursor)469 private @Nullable String getGeometryString(Cursor cursor) { 470 if (mCurrentLoaderId != LOADER_HISTORY_FROM_CBS) return null; 471 if (cursor.getColumnIndex(Telephony.CellBroadcasts.GEOMETRIES) >= 0) { 472 return cursor.getString(cursor.getColumnIndex(Telephony.CellBroadcasts.GEOMETRIES)); 473 } 474 return null; 475 } 476 477 @Override onContextItemSelected(MenuItem item)478 public boolean onContextItemSelected(MenuItem item) { 479 Cursor cursor = mAdapter.getCursor(); 480 if (cursor != null && cursor.getPosition() >= 0) { 481 switch (item.getItemId()) { 482 case MENU_DELETE: 483 long[] selectedRowId = getSelectedItemsRowId(); 484 confirmDeleteThread(selectedRowId); 485 break; 486 487 case MENU_VIEW_DETAILS: 488 showBroadcastDetails(CellBroadcastCursorAdapter.createFromCursor( 489 getContext(), cursor), getLocationCheckTime(cursor), 490 wasMessageDisplayed(cursor), getGeometryString(cursor)); 491 break; 492 493 default: 494 break; 495 } 496 } 497 return super.onContextItemSelected(item); 498 } 499 500 @Override onOptionsItemSelected(MenuItem item)501 public boolean onOptionsItemSelected(MenuItem item) { 502 switch(item.getItemId()) { 503 case MENU_DELETE_ALL: 504 long[] deleteAll = {-1}; 505 confirmDeleteThread(deleteAll); 506 break; 507 508 case MENU_SHOW_ALL_MESSAGES: 509 getLoaderManager().restartLoader(LOADER_HISTORY_FROM_CBS, null, this); 510 break; 511 512 case MENU_SHOW_REGULAR_MESSAGES: 513 getLoaderManager().restartLoader(LOADER_NORMAL_HISTORY, null, this); 514 break; 515 516 case MENU_PREFERENCES: 517 Intent intent = new Intent(getActivity(), CellBroadcastSettings.class); 518 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP 519 | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); 520 startActivity(intent); 521 if (mActivity != null) { 522 mActivity.finish(); 523 } 524 break; 525 526 default: 527 return true; 528 } 529 return false; 530 } 531 532 /** 533 * Get MultiChoiceModeListener object 534 * 535 * @return MultiChoiceModeListener 536 */ 537 @VisibleForTesting getMultiChoiceModeListener()538 public synchronized MultiChoiceModeListener getMultiChoiceModeListener() { 539 if (mListener == null) { 540 mListener = new MultiChoiceModeListener() { 541 @Override 542 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 543 mode.getMenuInflater().inflate(R.menu.cell_broadcast_list_action_menu, 544 menu); 545 mInformationMenuItem = menu.findItem(R.id.action_detail_info); 546 mAdapter.setIsActionMode(true); 547 mAdapter.notifyDataSetChanged(); 548 updateActionIconsVisibility(); 549 if (getListView().getCheckedItemCount() > 0) { 550 mode.setTitle(String.valueOf(getListView().getCheckedItemCount())); 551 } 552 return true; 553 } 554 555 @Override 556 public void onDestroyActionMode(ActionMode mode) { 557 mAdapter.setIsActionMode(false); 558 mAdapter.notifyDataSetChanged(); 559 } 560 561 @Override 562 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 563 if (item.getItemId() == R.id.action_detail_info) { 564 Cursor cursor = getSelectedItemSingle(); 565 if (cursor != null) { 566 showBroadcastDetails(CellBroadcastCursorAdapter.createFromCursor( 567 getContext(), cursor), getLocationCheckTime(cursor), 568 wasMessageDisplayed(cursor), getGeometryString(cursor)); 569 } else { 570 Log.e(TAG, "Multiple items selected with action_detail_info"); 571 } 572 mode.finish(); 573 return true; 574 } else if (item.getItemId() == R.id.action_delete) { 575 long[] selectedRowId = getSelectedItemsRowId(); 576 confirmDeleteThread(selectedRowId); 577 mode.finish(); 578 return true; 579 } else { 580 Log.e(TAG, "onActionItemClicked: unsupported action return false"); 581 return false; 582 } 583 } 584 585 @Override 586 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 587 return false; 588 } 589 590 @Override 591 public void onItemCheckedStateChanged( 592 ActionMode mode, int position, long id, boolean checked) { 593 int checkedCount = getListView().getCheckedItemCount(); 594 595 updateActionIconsVisibility(); 596 mode.setTitle(String.valueOf(checkedCount)); 597 mAdapter.notifyDataSetChanged(); 598 } 599 }; 600 } 601 return mListener; 602 } 603 604 /** 605 * Start the process of putting up a dialog to confirm deleting a broadcast. 606 * @param rowId array of the row ID that the broadcast to delete, 607 * or rowId[0] = -1 to delete all broadcasts 608 */ confirmDeleteThread(long[] rowId)609 public void confirmDeleteThread(long[] rowId) { 610 DeleteDialogFragment dialog = new DeleteDialogFragment(); 611 Bundle dialogArgs = new Bundle(); 612 dialogArgs.putLongArray(DeleteDialogFragment.ROW_ID, rowId); 613 dialog.setArguments(dialogArgs); 614 dialog.show(getFragmentManager(), KEY_DELETE_DIALOG); 615 } 616 617 public static class DeleteDialogFragment extends DialogFragment { 618 /** 619 * Key for the row id of the message to delete. If the row id is -1, the displayed 620 * dialog will indicate that all messages are to be deleted. 621 */ 622 public static final String ROW_ID = "row_id"; 623 @Override onCreateDialog(Bundle savedInstanceState)624 public Dialog onCreateDialog(Bundle savedInstanceState) { 625 setRetainInstance(true); 626 long[] rowId = getArguments().getLongArray(ROW_ID); 627 boolean deleteAll = rowId[0] == -1; 628 DeleteThreadListener listener = new DeleteThreadListener(getActivity(), rowId); 629 AlertDialog.Builder builder = new AlertDialog.Builder( 630 DeleteDialogFragment.this.getActivity()); 631 builder.setIconAttribute(android.R.attr.alertDialogIcon) 632 .setCancelable(true) 633 .setPositiveButton(R.string.button_delete, listener) 634 .setNegativeButton(R.string.button_cancel, null) 635 .setMessage(deleteAll ? R.string.confirm_delete_all_broadcasts 636 : R.string.confirm_delete_broadcast); 637 return builder.create(); 638 } 639 640 @Override onDestroyView()641 public void onDestroyView() { 642 Dialog dialog = getDialog(); 643 if (dialog != null && getRetainInstance()) { 644 dialog.setDismissMessage(null); 645 } 646 super.onDestroyView(); 647 } 648 } 649 650 public static class DeleteThreadListener implements OnClickListener { 651 private final long[] mRowId; 652 private final Context mContext; 653 DeleteThreadListener(Context context, long[] rowId)654 public DeleteThreadListener(Context context, long[] rowId) { 655 mContext = context; 656 mRowId = rowId; 657 } 658 659 @Override onClick(DialogInterface dialog, int whichButton)660 public void onClick(DialogInterface dialog, int whichButton) { 661 // delete from database on a background thread 662 new CellBroadcastContentProvider.AsyncCellBroadcastTask( 663 mContext.getContentResolver()).execute( 664 (CellBroadcastContentProvider.CellBroadcastOperation) provider -> { 665 if (mRowId[0] != -1) { 666 for (int i = 0; i < mRowId.length; i++) { 667 if (!provider.deleteBroadcast(mRowId[i])) { 668 Log.e(TAG, "failed to delete at row " + mRowId[i]); 669 } 670 } 671 return true; 672 } else { 673 return provider.deleteAllBroadcasts(); 674 } 675 }); 676 677 dialog.dismiss(); 678 } 679 } 680 } 681 } 682