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