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