• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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