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 android.annotation.Nullable;
20 import android.app.ActionBar;
21 import android.app.Activity;
22 import android.app.AlertDialog;
23 import android.app.FragmentManager;
24 import android.app.ListFragment;
25 import android.app.LoaderManager;
26 import android.app.NotificationManager;
27 import android.content.Context;
28 import android.content.CursorLoader;
29 import android.content.DialogInterface;
30 import android.content.DialogInterface.OnClickListener;
31 import android.content.Intent;
32 import android.content.Loader;
33 import android.database.Cursor;
34 import android.net.Uri;
35 import android.os.Bundle;
36 import android.provider.Telephony;
37 import android.telephony.SmsCbMessage;
38 import android.util.Log;
39 import android.view.LayoutInflater;
40 import android.view.Menu;
41 import android.view.MenuInflater;
42 import android.view.MenuItem;
43 import android.view.View;
44 import android.view.View.OnCreateContextMenuListener;
45 import android.view.ViewGroup;
46 import android.widget.CursorAdapter;
47 import android.widget.ListView;
48 import android.widget.TextView;
49 
50 import com.android.internal.annotations.VisibleForTesting;
51 
52 import java.util.ArrayList;
53 
54 /**
55  * This activity provides a list view of received cell broadcasts. Most of the work is handled
56  * in the inner CursorLoaderListFragment class.
57  */
58 public class CellBroadcastListActivity extends Activity {
59 
60     @VisibleForTesting
61     public CursorLoaderListFragment mListFragment;
62 
63     @Override
onCreate(Bundle savedInstanceState)64     protected void onCreate(Bundle savedInstanceState) {
65         super.onCreate(savedInstanceState);
66 
67         ActionBar actionBar = getActionBar();
68         if (actionBar != null) {
69             // android.R.id.home will be triggered in onOptionsItemSelected()
70             actionBar.setDisplayHomeAsUpEnabled(true);
71         }
72 
73         setTitle(getString(R.string.cb_list_activity_title));
74 
75         // Dismiss the notification that brought us here (if any).
76         ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE))
77                 .cancel(CellBroadcastAlertService.NOTIFICATION_ID);
78 
79         FragmentManager fm = getFragmentManager();
80 
81         // Create the list fragment and add it as our sole content.
82         if (fm.findFragmentById(android.R.id.content) == null) {
83             mListFragment = new CursorLoaderListFragment();
84             fm.beginTransaction().add(android.R.id.content, mListFragment).commit();
85         }
86     }
87 
88     @Override
onOptionsItemSelected(MenuItem item)89     public boolean onOptionsItemSelected(MenuItem item) {
90         switch (item.getItemId()) {
91             // Respond to the action bar's Up/Home button
92             case android.R.id.home:
93                 finish();
94                 return true;
95         }
96         return super.onOptionsItemSelected(item);
97     }
98 
99     /**
100      * List fragment queries SQLite database on worker thread.
101      */
102     public static class CursorLoaderListFragment extends ListFragment
103             implements LoaderManager.LoaderCallbacks<Cursor> {
104         private static final String TAG = CellBroadcastListActivity.class.getSimpleName();
105         private static final boolean DBG = true;
106 
107         // IDs of the main menu items.
108         @VisibleForTesting
109         public static final int MENU_DELETE_ALL            = 3;
110         @VisibleForTesting
111         public static final int MENU_SHOW_REGULAR_MESSAGES = 4;
112         @VisibleForTesting
113         public static final int MENU_SHOW_ALL_MESSAGES     = 5;
114 
115         // Load the history from cell broadcast receiver database
116         private static final int LOADER_NORMAL_HISTORY      = 1;
117         // Load the history from cell broadcast service. This will include all non-shown messages.
118         @VisibleForTesting
119         public static final int LOADER_HISTORY_FROM_CBS    = 2;
120 
121         @VisibleForTesting
122         public static final String KEY_LOADER_ID = "loader_id";
123 
124         // IDs of the context menu items (package local, accessed from inner DeleteThreadListener).
125         @VisibleForTesting
126         public static final int MENU_DELETE               = 0;
127         @VisibleForTesting
128         public static final int MENU_VIEW_DETAILS         = 1;
129 
130         // cell broadcast provider from cell broadcast service.
131         public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts");
132 
133         // Query columns for provider from cell broadcast service.
134         public static final String[] QUERY_COLUMNS = {
135                 Telephony.CellBroadcasts._ID,
136                 Telephony.CellBroadcasts.SLOT_INDEX,
137                 Telephony.CellBroadcasts.SUBSCRIPTION_ID,
138                 Telephony.CellBroadcasts.GEOGRAPHICAL_SCOPE,
139                 Telephony.CellBroadcasts.PLMN,
140                 Telephony.CellBroadcasts.LAC,
141                 Telephony.CellBroadcasts.CID,
142                 Telephony.CellBroadcasts.SERIAL_NUMBER,
143                 Telephony.CellBroadcasts.SERVICE_CATEGORY,
144                 Telephony.CellBroadcasts.LANGUAGE_CODE,
145                 Telephony.CellBroadcasts.DATA_CODING_SCHEME,
146                 Telephony.CellBroadcasts.MESSAGE_BODY,
147                 Telephony.CellBroadcasts.MESSAGE_FORMAT,
148                 Telephony.CellBroadcasts.MESSAGE_PRIORITY,
149                 Telephony.CellBroadcasts.ETWS_WARNING_TYPE,
150                 Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS,
151                 Telephony.CellBroadcasts.CMAS_CATEGORY,
152                 Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE,
153                 Telephony.CellBroadcasts.CMAS_SEVERITY,
154                 Telephony.CellBroadcasts.CMAS_URGENCY,
155                 Telephony.CellBroadcasts.CMAS_CERTAINTY,
156                 Telephony.CellBroadcasts.RECEIVED_TIME,
157                 Telephony.CellBroadcasts.LOCATION_CHECK_TIME,
158                 Telephony.CellBroadcasts.MESSAGE_BROADCASTED,
159                 Telephony.CellBroadcasts.MESSAGE_DISPLAYED,
160                 Telephony.CellBroadcasts.GEOMETRIES,
161                 Telephony.CellBroadcasts.MAXIMUM_WAIT_TIME
162         };
163 
164         // This is the Adapter being used to display the list's data.
165         @VisibleForTesting
166         public CursorAdapter mAdapter;
167 
168         private int mCurrentLoaderId = 0;
169 
170         @Override
onCreate(Bundle savedInstanceState)171         public void onCreate(Bundle savedInstanceState) {
172             super.onCreate(savedInstanceState);
173 
174             // We have a menu item to show in action bar.
175             setHasOptionsMenu(true);
176         }
177 
178         @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)179         public View onCreateView(LayoutInflater inflater, ViewGroup container,
180                 Bundle savedInstanceState) {
181             return inflater.inflate(R.layout.cell_broadcast_list_screen, container, false);
182         }
183 
184         @Override
onActivityCreated(Bundle savedInstanceState)185         public void onActivityCreated(Bundle savedInstanceState) {
186             super.onActivityCreated(savedInstanceState);
187 
188             // Set context menu for long-press.
189             ListView listView = getListView();
190             listView.setOnCreateContextMenuListener(mOnCreateContextMenuListener);
191 
192             // Create a cursor adapter to display the loaded data.
193             mAdapter = new CellBroadcastCursorAdapter(getActivity());
194             setListAdapter(mAdapter);
195 
196             mCurrentLoaderId = LOADER_NORMAL_HISTORY;
197             if (savedInstanceState != null && savedInstanceState.containsKey(KEY_LOADER_ID)) {
198                 mCurrentLoaderId = savedInstanceState.getInt(KEY_LOADER_ID);
199             }
200 
201             if (DBG) Log.d(TAG, "onActivityCreated: id=" + mCurrentLoaderId);
202 
203             // Prepare the loader.  Either re-connect with an existing one,
204             // or start a new one.
205             getLoaderManager().initLoader(mCurrentLoaderId, null, this);
206         }
207 
208         @Override
onSaveInstanceState(Bundle outState)209         public void onSaveInstanceState(Bundle outState) {
210             // Save the current id for later restoring activity.
211             if (DBG) Log.d(TAG, "onSaveInstanceState: id=" + mCurrentLoaderId);
212             outState.putInt(KEY_LOADER_ID, mCurrentLoaderId);
213         }
214 
215         @Override
onResume()216         public void onResume() {
217             super.onResume();
218             if (DBG) Log.d(TAG, "onResume");
219             if (mCurrentLoaderId != 0) {
220                 getLoaderManager().restartLoader(mCurrentLoaderId, null, this);
221             }
222         }
223 
224         @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)225         public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
226             menu.add(0, MENU_DELETE_ALL, 0, R.string.menu_delete_all).setIcon(
227                     android.R.drawable.ic_menu_delete);
228             menu.add(0, MENU_SHOW_ALL_MESSAGES, 0, R.string.show_all_messages);
229             menu.add(0, MENU_SHOW_REGULAR_MESSAGES, 0, R.string.show_regular_messages);
230         }
231 
232         @Override
onPrepareOptionsMenu(Menu menu)233         public void onPrepareOptionsMenu(Menu menu) {
234             boolean isTestingMode = CellBroadcastReceiver.isTestingMode(
235                     getContext());
236             // Only allowing delete all messages when not in testing mode because when testing mode
237             // is enabled, the database source is from cell broadcast service. Deleting them does
238             // not affect the database in cell broadcast receiver. Hide the options to reduce
239             // confusion.
240             menu.findItem(MENU_DELETE_ALL).setVisible(!mAdapter.isEmpty() && !isTestingMode);
241             menu.findItem(MENU_SHOW_ALL_MESSAGES).setVisible(isTestingMode
242                     && mCurrentLoaderId == LOADER_NORMAL_HISTORY);
243             menu.findItem(MENU_SHOW_REGULAR_MESSAGES).setVisible(isTestingMode
244                     && mCurrentLoaderId == LOADER_HISTORY_FROM_CBS);
245         }
246 
247         @Override
onListItemClick(ListView l, View v, int position, long id)248         public void onListItemClick(ListView l, View v, int position, long id) {
249             CellBroadcastListItem cbli = (CellBroadcastListItem) v;
250             showDialogAndMarkRead(cbli.getMessage());
251         }
252 
253         @Override
onCreateLoader(int id, Bundle args)254         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
255             mCurrentLoaderId = id;
256             if (id == LOADER_NORMAL_HISTORY) {
257                 Log.d(TAG, "onCreateLoader: normal history.");
258                 return new CursorLoader(getActivity(), CellBroadcastContentProvider.CONTENT_URI,
259                         CellBroadcastContentProvider.QUERY_COLUMNS, null, null,
260                         Telephony.CellBroadcasts.DELIVERY_TIME + " DESC");
261             } else if (id == LOADER_HISTORY_FROM_CBS) {
262                 Log.d(TAG, "onCreateLoader: history from cell broadcast service");
263                 return new CursorLoader(getActivity(), CONTENT_URI,
264                         QUERY_COLUMNS, null, null,
265                         Telephony.CellBroadcasts.RECEIVED_TIME + " DESC");
266             }
267 
268             return null;
269         }
270 
271         @Override
onLoadFinished(Loader<Cursor> loader, Cursor data)272         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
273             if (DBG) Log.d(TAG, "onLoadFinished");
274             // Swap the new cursor in.  (The framework will take care of closing the
275             // old cursor once we return.)
276             mAdapter.swapCursor(data);
277             getActivity().invalidateOptionsMenu();
278             updateNoAlertTextVisibility();
279         }
280 
281         @Override
onLoaderReset(Loader<Cursor> loader)282         public void onLoaderReset(Loader<Cursor> loader) {
283             if (DBG) Log.d(TAG, "onLoaderReset");
284             // This is called when the last Cursor provided to onLoadFinished()
285             // above is about to be closed.  We need to make sure we are no
286             // longer using it.
287             mAdapter.swapCursor(null);
288         }
289 
showDialogAndMarkRead(SmsCbMessage message)290         private void showDialogAndMarkRead(SmsCbMessage message) {
291             // show emergency alerts with the warning icon, but don't play alert tone
292             Intent i = new Intent(getActivity(), CellBroadcastAlertDialog.class);
293             ArrayList<SmsCbMessage> messageList = new ArrayList<>();
294             messageList.add(message);
295             i.putParcelableArrayListExtra(CellBroadcastAlertService.SMS_CB_MESSAGE_EXTRA,
296                     messageList);
297             startActivity(i);
298         }
299 
showBroadcastDetails(SmsCbMessage message, long locationCheckTime, boolean messageDisplayed, String geometry)300         private void showBroadcastDetails(SmsCbMessage message, long locationCheckTime,
301                                           boolean messageDisplayed, String geometry) {
302             // show dialog with delivery date/time and alert details
303             CharSequence details = CellBroadcastResources.getMessageDetails(getActivity(),
304                     mCurrentLoaderId == LOADER_HISTORY_FROM_CBS, message, locationCheckTime,
305                     messageDisplayed, geometry);
306             int titleId = (mCurrentLoaderId == LOADER_NORMAL_HISTORY)
307                     ? R.string.view_details_title : R.string.view_details_debugging_title;
308             new AlertDialog.Builder(getActivity())
309                     .setTitle(titleId)
310                     .setMessage(details)
311                     .setCancelable(true)
312                     .show();
313         }
314 
315         private final OnCreateContextMenuListener mOnCreateContextMenuListener =
316                 (menu, v, menuInfo) -> {
317                     menu.setHeaderTitle(R.string.message_options);
318                     menu.add(0, MENU_VIEW_DETAILS, 0, R.string.menu_view_details);
319                     if (mCurrentLoaderId == LOADER_NORMAL_HISTORY) {
320                         menu.add(0, MENU_DELETE, 0, R.string.menu_delete);
321                     }
322                 };
323 
updateNoAlertTextVisibility()324         private void updateNoAlertTextVisibility() {
325             TextView noAlertsTextView = getActivity().findViewById(R.id.empty);
326             if (noAlertsTextView != null) {
327                 noAlertsTextView.setVisibility(!hasAlertsInHistory()
328                         ? View.VISIBLE : View.INVISIBLE);
329             }
330         }
331 
332         /**
333          * @return {@code true} if the alert history database has any item
334          */
hasAlertsInHistory()335         private boolean hasAlertsInHistory() {
336             return mAdapter.getCursor().getCount() > 0;
337         }
338 
339         /**
340          * Get the location check time of the message.
341          *
342          * @param cursor The cursor of the database
343          * @return The EPOCH time in milliseconds that the location check was performed on the
344          * message. -1 if the information is not available.
345          */
getLocationCheckTime(Cursor cursor)346         private long getLocationCheckTime(Cursor cursor) {
347             if (mCurrentLoaderId != LOADER_HISTORY_FROM_CBS) return -1;
348             return cursor.getLong(cursor.getColumnIndex(
349                     Telephony.CellBroadcasts.LOCATION_CHECK_TIME));
350         }
351 
352         /**
353          * Check if the message has been displayed to the user or not
354          *
355          * @param cursor The cursor of the database
356          * @return {@code true} if the message was displayed to the user, otherwise {@code false}.
357          */
wasMessageDisplayed(Cursor cursor)358         private boolean wasMessageDisplayed(Cursor cursor) {
359             if (mCurrentLoaderId != LOADER_HISTORY_FROM_CBS) return true;
360             return cursor.getInt(cursor.getColumnIndex(
361                     Telephony.CellBroadcasts.MESSAGE_DISPLAYED)) != 0;
362         }
363 
364         /**
365          * Get the geometry string from the message if available.
366          *
367          * @param cursor The cursor of the database
368          * @return The geometry string
369          */
getGeometryString(Cursor cursor)370         private @Nullable String getGeometryString(Cursor cursor) {
371             if (mCurrentLoaderId != LOADER_HISTORY_FROM_CBS) return null;
372             if (cursor.getColumnIndex(Telephony.CellBroadcasts.GEOMETRIES) >= 0) {
373                 return cursor.getString(cursor.getColumnIndex(Telephony.CellBroadcasts.GEOMETRIES));
374             }
375             return null;
376         }
377 
378         @Override
onContextItemSelected(MenuItem item)379         public boolean onContextItemSelected(MenuItem item) {
380             Cursor cursor = mAdapter.getCursor();
381             if (cursor != null && cursor.getPosition() >= 0) {
382                 switch (item.getItemId()) {
383                     case MENU_DELETE:
384                         confirmDeleteThread(cursor.getLong(cursor.getColumnIndexOrThrow(
385                                 Telephony.CellBroadcasts._ID)));
386                         break;
387 
388                     case MENU_VIEW_DETAILS:
389                         showBroadcastDetails(CellBroadcastCursorAdapter.createFromCursor(
390                                 getContext(), cursor), getLocationCheckTime(cursor),
391                                 wasMessageDisplayed(cursor), getGeometryString(cursor));
392                         break;
393 
394                     default:
395                         break;
396                 }
397             }
398             return super.onContextItemSelected(item);
399         }
400 
401         @Override
onOptionsItemSelected(MenuItem item)402         public boolean onOptionsItemSelected(MenuItem item) {
403             switch(item.getItemId()) {
404                 case MENU_DELETE_ALL:
405                     confirmDeleteThread(-1);
406                     break;
407 
408                 case MENU_SHOW_ALL_MESSAGES:
409                     getLoaderManager().restartLoader(LOADER_HISTORY_FROM_CBS, null, this);
410                     break;
411 
412                 case MENU_SHOW_REGULAR_MESSAGES:
413                     getLoaderManager().restartLoader(LOADER_NORMAL_HISTORY, null, this);
414                     break;
415 
416                 default:
417                     return true;
418             }
419             return false;
420         }
421 
422         /**
423          * Start the process of putting up a dialog to confirm deleting a broadcast.
424          * @param rowId the row ID of the broadcast to delete, or -1 to delete all broadcasts
425          */
confirmDeleteThread(long rowId)426         public void confirmDeleteThread(long rowId) {
427             DeleteThreadListener listener = new DeleteThreadListener(rowId);
428             confirmDeleteThreadDialog(listener, (rowId == -1), getActivity());
429         }
430 
431         /**
432          * Build and show the proper delete broadcast dialog. The UI is slightly different
433          * depending on whether there are locked messages in the thread(s) and whether we're
434          * deleting a single broadcast or all broadcasts.
435          * @param listener gets called when the delete button is pressed
436          * @param deleteAll whether to show a single thread or all threads UI
437          * @param context used to load the various UI elements
438          */
confirmDeleteThreadDialog(DeleteThreadListener listener, boolean deleteAll, Context context)439         public static void confirmDeleteThreadDialog(DeleteThreadListener listener,
440                 boolean deleteAll, Context context) {
441             AlertDialog.Builder builder = new AlertDialog.Builder(context);
442             builder.setIconAttribute(android.R.attr.alertDialogIcon)
443                     .setCancelable(true)
444                     .setPositiveButton(R.string.button_delete, listener)
445                     .setNegativeButton(R.string.button_cancel, null)
446                     .setMessage(deleteAll ? R.string.confirm_delete_all_broadcasts
447                             : R.string.confirm_delete_broadcast)
448                     .show();
449         }
450 
451         public class DeleteThreadListener implements OnClickListener {
452             private final long mRowId;
453 
DeleteThreadListener(long rowId)454             public DeleteThreadListener(long rowId) {
455                 mRowId = rowId;
456             }
457 
458             @Override
onClick(DialogInterface dialog, int whichButton)459             public void onClick(DialogInterface dialog, int whichButton) {
460                 // delete from database on a background thread
461                 new CellBroadcastContentProvider.AsyncCellBroadcastTask(
462                         getActivity().getContentResolver()).execute(
463                                 (CellBroadcastContentProvider.CellBroadcastOperation) provider -> {
464                                     if (mRowId != -1) {
465                                         return provider.deleteBroadcast(mRowId);
466                                     } else {
467                                         return provider.deleteAllBroadcasts();
468                                     }
469                                 });
470 
471                 dialog.dismiss();
472             }
473         }
474     }
475 }
476