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.app.ActivityManager;
20 import android.content.BroadcastReceiver;
21 import android.content.ComponentName;
22 import android.content.ContentProviderClient;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.SharedPreferences;
26 import android.content.SharedPreferences.Editor;
27 import android.content.pm.ActivityInfo;
28 import android.content.pm.PackageInfo;
29 import android.content.pm.PackageManager;
30 import android.content.res.Resources;
31 import android.os.Bundle;
32 import android.os.RemoteException;
33 import android.os.SystemProperties;
34 import android.os.UserManager;
35 import android.preference.PreferenceManager;
36 import android.provider.Telephony;
37 import android.provider.Telephony.CellBroadcasts;
38 import android.telephony.CarrierConfigManager;
39 import android.telephony.ServiceState;
40 import android.telephony.SubscriptionManager;
41 import android.telephony.TelephonyManager;
42 import android.telephony.cdma.CdmaSmsCbProgramData;
43 import android.text.TextUtils;
44 import android.util.Log;
45 import android.widget.Toast;
46 
47 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
48 
49 import com.android.cellbroadcastservice.CellBroadcastStatsLog;
50 import com.android.internal.annotations.VisibleForTesting;
51 
52 import java.util.ArrayList;
53 
54 public class CellBroadcastReceiver extends BroadcastReceiver {
55     private static final String TAG = "CellBroadcastReceiver";
56     static final boolean DBG = true;
57     static final boolean VDBG = false;    // STOPSHIP: change to false before ship
58 
59     // Key to access the shared preference of reminder interval default value.
60     @VisibleForTesting
61     public static final String CURRENT_INTERVAL_DEFAULT = "current_interval_default";
62 
63     // Key to access the shared preference of cell broadcast testing mode.
64     @VisibleForTesting
65     public static final String TESTING_MODE = "testing_mode";
66 
67     // Key to access the shared preference of service state.
68     private static final String SERVICE_STATE = "service_state";
69 
70     // shared preference under developer settings
71     private static final String ENABLE_ALERT_MASTER_PREF = "enable_alerts_master_toggle";
72 
73     public static final String ACTION_SERVICE_STATE = "android.intent.action.SERVICE_STATE";
74     public static final String EXTRA_VOICE_REG_STATE = "voiceRegState";
75 
76     // Intent actions and extras
77     public static final String CELLBROADCAST_START_CONFIG_ACTION =
78             "com.android.cellbroadcastreceiver.intent.START_CONFIG";
79     public static final String ACTION_MARK_AS_READ =
80             "com.android.cellbroadcastreceiver.intent.action.MARK_AS_READ";
81     public static final String EXTRA_DELIVERY_TIME =
82             "com.android.cellbroadcastreceiver.intent.extra.ID";
83 
84     public static final String ACTION_TESTING_MODE_CHANGED =
85             "com.android.cellbroadcastreceiver.intent.ACTION_TESTING_MODE_CHANGED";
86 
87     private Context mContext;
88 
89     /**
90      * helper method for easier testing. To generate a new CellBroadcastTask
91      * @param deliveryTime message delivery time
92      */
93     @VisibleForTesting
getCellBroadcastTask(final long deliveryTime)94     public void getCellBroadcastTask(final long deliveryTime) {
95         new CellBroadcastContentProvider.AsyncCellBroadcastTask(mContext.getContentResolver())
96                 .execute(new CellBroadcastContentProvider.CellBroadcastOperation() {
97                     @Override
98                     public boolean execute(CellBroadcastContentProvider provider) {
99                         return provider.markBroadcastRead(CellBroadcasts.DELIVERY_TIME,
100                                 deliveryTime);
101                     }
102                 });
103     }
104 
105     /**
106      * this method is to make this class unit-testable, because CellBroadcastSettings.getResources()
107      * is a static method and cannot be stubbed.
108      * @return resources
109      */
110     @VisibleForTesting
getResourcesMethod()111     public Resources getResourcesMethod() {
112         return CellBroadcastSettings.getResources(mContext,
113                 SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
114     }
115 
116     @Override
onReceive(Context context, Intent intent)117     public void onReceive(Context context, Intent intent) {
118         if (DBG) log("onReceive " + intent);
119 
120         mContext = context.getApplicationContext();
121         String action = intent.getAction();
122         Resources res = getResourcesMethod();
123 
124         if (ACTION_MARK_AS_READ.equals(action)) {
125             final long deliveryTime = intent.getLongExtra(EXTRA_DELIVERY_TIME, -1);
126             getCellBroadcastTask(deliveryTime);
127         } else if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action)) {
128             initializeSharedPreference();
129             enableLauncher();
130             startConfigService();
131         } else if (ACTION_SERVICE_STATE.equals(action)) {
132             // lower layer clears channel configurations under APM, thus need to resend
133             // configurations once moving back from APM. This should be fixed in lower layer
134             // going forward.
135             int ss = intent.getIntExtra(EXTRA_VOICE_REG_STATE, ServiceState.STATE_IN_SERVICE);
136             if (ss != ServiceState.STATE_POWER_OFF
137                     && getServiceState(context) == ServiceState.STATE_POWER_OFF) {
138                 startConfigService();
139             }
140             setServiceState(ss);
141         } else if (CELLBROADCAST_START_CONFIG_ACTION.equals(action)
142                 || SubscriptionManager.ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED.equals(action)) {
143             startConfigService();
144         } else if (Telephony.Sms.Intents.ACTION_SMS_EMERGENCY_CB_RECEIVED.equals(action) ||
145                 Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION.equals(action)) {
146             intent.setClass(mContext, CellBroadcastAlertService.class);
147             mContext.startService(intent);
148         } else if (Telephony.Sms.Intents.SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED_ACTION
149                 .equals(action)) {
150             ArrayList<CdmaSmsCbProgramData> programDataList =
151                     intent.getParcelableArrayListExtra("program_data");
152             CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_REPORTED,
153                     CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_REPORTED__TYPE__CDMA_SPC,
154                     CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_REPORTED__SOURCE__CB_RECEIVER_APP);
155             if (programDataList != null) {
156                 handleCdmaSmsCbProgramData(programDataList);
157             } else {
158                 loge("SCPD intent received with no program_data");
159             }
160         } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
161             // rename registered notification channels on locale change
162             CellBroadcastAlertService.createNotificationChannels(mContext);
163         } else if (TelephonyManager.ACTION_SECRET_CODE.equals(action)) {
164             if (SystemProperties.getInt("ro.debuggable", 0) == 1
165                     || res.getBoolean(R.bool.allow_testing_mode_on_user_build)) {
166                 setTestingMode(!isTestingMode(mContext));
167                 int msgId = (isTestingMode(mContext)) ? R.string.testing_mode_enabled
168                         : R.string.testing_mode_disabled;
169                 String msg =  res.getString(msgId);
170                 Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
171                 LocalBroadcastManager.getInstance(mContext)
172                         .sendBroadcast(new Intent(ACTION_TESTING_MODE_CHANGED));
173                 log(msg);
174             }
175         } else {
176             Log.w(TAG, "onReceive() unexpected action " + action);
177         }
178     }
179 
180     /**
181      * Enable/disable cell broadcast receiver testing mode.
182      *
183      * @param on {@code true} if testing mode is on, otherwise off.
184      */
185     @VisibleForTesting
setTestingMode(boolean on)186     public void setTestingMode(boolean on) {
187         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
188         sp.edit().putBoolean(TESTING_MODE, on).commit();
189     }
190 
191     /**
192      * @return {@code true} if operating in testing mode, which enables some features for testing
193      * purposes.
194      */
isTestingMode(Context context)195     public static boolean isTestingMode(Context context) {
196         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
197         return sp.getBoolean(TESTING_MODE, false);
198     }
199 
200     /**
201      * Store the current service state for voice registration.
202      *
203      * @param ss current voice registration service state.
204      */
setServiceState(int ss)205     private void setServiceState(int ss) {
206         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
207         sp.edit().putInt(SERVICE_STATE, ss).commit();
208     }
209 
210     /**
211      * @return the stored voice registration service state
212      */
getServiceState(Context context)213     private static int getServiceState(Context context) {
214         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
215         return sp.getInt(SERVICE_STATE, ServiceState.STATE_IN_SERVICE);
216     }
217 
218     /**
219      * update reminder interval
220      */
221     @VisibleForTesting
adjustReminderInterval()222     public void adjustReminderInterval() {
223         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
224         String currentIntervalDefault = sp.getString(CURRENT_INTERVAL_DEFAULT, "0");
225 
226         // If interval default changes, reset the interval to the new default value.
227         String newIntervalDefault = CellBroadcastSettings.getResources(mContext,
228                 SubscriptionManager.DEFAULT_SUBSCRIPTION_ID).getString(
229                         R.string.alert_reminder_interval_in_min_default);
230         if (!newIntervalDefault.equals(currentIntervalDefault)) {
231             Log.d(TAG, "Default interval changed from " + currentIntervalDefault + " to " +
232                     newIntervalDefault);
233 
234             Editor editor = sp.edit();
235             // Reset the value to default.
236             editor.putString(
237                     CellBroadcastSettings.KEY_ALERT_REMINDER_INTERVAL, newIntervalDefault);
238             // Save the new default value.
239             editor.putString(CURRENT_INTERVAL_DEFAULT, newIntervalDefault);
240             editor.commit();
241         } else {
242             if (DBG) Log.d(TAG, "Default interval " + currentIntervalDefault + " did not change.");
243         }
244     }
245     /**
246      * This method's purpose if to enable unit testing
247      * @return sharedePreferences for mContext
248      */
249     @VisibleForTesting
getDefaultSharedPreferences()250     public SharedPreferences getDefaultSharedPreferences() {
251         return PreferenceManager.getDefaultSharedPreferences(mContext);
252     }
253 
254     /**
255      * return if there are default values in shared preferences
256      * @return boolean
257      */
258     @VisibleForTesting
sharedPrefsHaveDefaultValues()259     public Boolean sharedPrefsHaveDefaultValues() {
260         return mContext.getSharedPreferences(PreferenceManager.KEY_HAS_SET_DEFAULT_VALUES,
261                 Context.MODE_PRIVATE).getBoolean(PreferenceManager.KEY_HAS_SET_DEFAULT_VALUES,
262                 false);
263     }
264     /**
265      * initialize shared preferences before starting services
266      */
267     @VisibleForTesting
initializeSharedPreference()268     public void initializeSharedPreference() {
269         if (isSystemUser()) {
270             Log.d(TAG, "initializeSharedPreference");
271             SharedPreferences sp = getDefaultSharedPreferences();
272 
273             if (!sharedPrefsHaveDefaultValues()) {
274                 // Sets the default values of the shared preference if there isn't any.
275                 PreferenceManager.setDefaultValues(mContext, R.xml.preferences, false);
276 
277                 sp.edit().putBoolean(CellBroadcastSettings.KEY_OVERRIDE_DND_SETTINGS_CHANGED,
278                         false).apply();
279 
280                 // migrate sharedpref from legacy app
281                 migrateSharedPreferenceFromLegacy();
282 
283                 // If the device is in test harness mode, we need to disable emergency alert by
284                 // default.
285                 if (ActivityManager.isRunningInUserTestHarness()) {
286                     Log.d(TAG, "In test harness mode. Turn off emergency alert by default.");
287                     sp.edit().putBoolean(CellBroadcastSettings.KEY_ENABLE_ALERTS_MASTER_TOGGLE,
288                             false).apply();
289                 }
290 
291             } else {
292                 Log.d(TAG, "Skip setting default values of shared preference.");
293             }
294 
295             adjustReminderInterval();
296         } else {
297             Log.e(TAG, "initializeSharedPreference: Not system user.");
298         }
299     }
300 
301     /**
302      * migrate shared preferences from legacy content provider client
303      */
304     @VisibleForTesting
migrateSharedPreferenceFromLegacy()305     public void migrateSharedPreferenceFromLegacy() {
306         String[] PREF_KEYS = {
307                 CellBroadcasts.Preference.ENABLE_CMAS_AMBER_PREF,
308                 CellBroadcasts.Preference.ENABLE_AREA_UPDATE_INFO_PREF,
309                 CellBroadcasts.Preference.ENABLE_TEST_ALERT_PREF,
310                 CellBroadcasts.Preference.ENABLE_STATE_LOCAL_TEST_PREF,
311                 CellBroadcasts.Preference.ENABLE_PUBLIC_SAFETY_PREF,
312                 CellBroadcasts.Preference.ENABLE_CMAS_SEVERE_THREAT_PREF,
313                 CellBroadcasts.Preference.ENABLE_CMAS_EXTREME_THREAT_PREF,
314                 CellBroadcasts.Preference.ENABLE_CMAS_PRESIDENTIAL_PREF,
315                 CellBroadcasts.Preference.ENABLE_EMERGENCY_PERF,
316                 CellBroadcasts.Preference.ENABLE_ALERT_VIBRATION_PREF,
317                 CellBroadcasts.Preference.ENABLE_CMAS_IN_SECOND_LANGUAGE_PREF,
318                 ENABLE_ALERT_MASTER_PREF,
319         };
320         try (ContentProviderClient client = mContext.getContentResolver()
321                 .acquireContentProviderClient(Telephony.CellBroadcasts.AUTHORITY_LEGACY)) {
322             if (client == null) {
323                 Log.d(TAG, "No legacy provider available for sharedpreference migration");
324                 return;
325             }
326             SharedPreferences.Editor sp = PreferenceManager
327                     .getDefaultSharedPreferences(mContext).edit();
328             for (String key : PREF_KEYS) {
329                 try {
330                     Bundle pref = client.call(
331                             CellBroadcasts.AUTHORITY_LEGACY,
332                             CellBroadcasts.CALL_METHOD_GET_PREFERENCE,
333                             key, null);
334                     if (pref != null && pref.containsKey(key)) {
335                         Log.d(TAG, "migrateSharedPreferenceFromLegacy: " + key + "val: "
336                                 + pref.getBoolean(key));
337                         sp.putBoolean(key, pref.getBoolean(key));
338                     } else {
339                         Log.d(TAG, "migrateSharedPreferenceFromLegacy: unsupported key: " + key);
340                     }
341                 } catch (RemoteException e) {
342                     Log.e(TAG, "fails to get shared preference " + e);
343                 }
344             }
345             sp.apply();
346         } catch (Exception e) {
347             // We have to guard ourselves against any weird behavior of the
348             // legacy provider by trying to catch everything
349             loge("Failed migration from legacy provider: " + e);
350         }
351     }
352 
353     /**
354      * Handle Service Category Program Data message.
355      * TODO: Send Service Category Program Results response message to sender
356      *
357      * @param programDataList
358      */
359     @VisibleForTesting
handleCdmaSmsCbProgramData(ArrayList<CdmaSmsCbProgramData> programDataList)360     public void handleCdmaSmsCbProgramData(ArrayList<CdmaSmsCbProgramData> programDataList) {
361         for (CdmaSmsCbProgramData programData : programDataList) {
362             switch (programData.getOperation()) {
363                 case CdmaSmsCbProgramData.OPERATION_ADD_CATEGORY:
364                     tryCdmaSetCategory(mContext, programData.getCategory(), true);
365                     break;
366 
367                 case CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY:
368                     tryCdmaSetCategory(mContext, programData.getCategory(), false);
369                     break;
370 
371                 case CdmaSmsCbProgramData.OPERATION_CLEAR_CATEGORIES:
372                     tryCdmaSetCategory(mContext,
373                             CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT, false);
374                     tryCdmaSetCategory(mContext,
375                             CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT, false);
376                     tryCdmaSetCategory(mContext,
377                             CdmaSmsCbProgramData.CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY, false);
378                     tryCdmaSetCategory(mContext,
379                             CdmaSmsCbProgramData.CATEGORY_CMAS_TEST_MESSAGE, false);
380                     break;
381 
382                 default:
383                     loge("Ignoring unknown SCPD operation " + programData.getOperation());
384             }
385         }
386     }
387 
388     /**
389      * set CDMA category in shared preferences
390      * @param context
391      * @param category CDMA category
392      * @param enable true for add category, false otherwise
393      */
394     @VisibleForTesting
tryCdmaSetCategory(Context context, int category, boolean enable)395     public void tryCdmaSetCategory(Context context, int category, boolean enable) {
396         SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
397 
398         switch (category) {
399             case CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT:
400                 sharedPrefs.edit().putBoolean(
401                         CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, enable)
402                         .apply();
403                 break;
404 
405             case CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT:
406                 sharedPrefs.edit().putBoolean(
407                         CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, enable)
408                         .apply();
409                 break;
410 
411             case CdmaSmsCbProgramData.CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY:
412                 sharedPrefs.edit().putBoolean(
413                         CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, enable).apply();
414                 break;
415 
416             case CdmaSmsCbProgramData.CATEGORY_CMAS_TEST_MESSAGE:
417                 sharedPrefs.edit().putBoolean(
418                         CellBroadcastSettings.KEY_ENABLE_TEST_ALERTS, enable).apply();
419                 break;
420 
421             default:
422                 Log.w(TAG, "Ignoring SCPD command to " + (enable ? "enable" : "disable")
423                         + " alerts in category " + category);
424         }
425     }
426 
427     /**
428      * This method's purpose if to enable unit testing
429      * @return if the mContext user is a system user
430      */
431     @VisibleForTesting
isSystemUser()432     public boolean isSystemUser() {
433         return isSystemUser(mContext);
434     }
435 
436     /**
437      * This method's purpose if to enable unit testing
438      */
439     @VisibleForTesting
startConfigService()440     public void startConfigService() {
441         startConfigService(mContext);
442     }
443 
444     /**
445      * Check if user from context is system user
446      * @param context
447      * @return whether the user is system user
448      */
isSystemUser(Context context)449     private static boolean isSystemUser(Context context) {
450         UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
451         return userManager.isSystemUser();
452     }
453 
454     /**
455      * Tell {@link CellBroadcastConfigService} to enable the CB channels.
456      * @param context the broadcast receiver context
457      */
startConfigService(Context context)458     static void startConfigService(Context context) {
459         if (isSystemUser(context)) {
460             Intent serviceIntent = new Intent(CellBroadcastConfigService.ACTION_ENABLE_CHANNELS,
461                     null, context, CellBroadcastConfigService.class);
462             Log.d(TAG, "Start Cell Broadcast configuration.");
463             context.startService(serviceIntent);
464         } else {
465             Log.e(TAG, "startConfigService: Not system user.");
466         }
467     }
468 
469     /**
470      * Enable Launcher.
471      */
472     @VisibleForTesting
enableLauncher()473     public void enableLauncher() {
474         boolean enable = getResourcesMethod().getBoolean(R.bool.show_message_history_in_launcher);
475         final PackageManager pm = mContext.getPackageManager();
476         // This alias presents the target activity, CellBroadcastListActivity, as a independent
477         // entity with its own intent filter for android.intent.category.LAUNCHER.
478         // This alias will be enabled/disabled at run-time based on resource overlay. Once enabled,
479         // it will appear in the Launcher as a top-level application
480         String aliasLauncherActivity = null;
481         try {
482             PackageInfo p = pm.getPackageInfo(mContext.getPackageName(),
483                 PackageManager.GET_ACTIVITIES | PackageManager.MATCH_DISABLED_COMPONENTS);
484             if (p != null) {
485                 for (ActivityInfo activityInfo : p.activities) {
486                     String targetActivity = activityInfo.targetActivity;
487                     if (CellBroadcastListActivity.class.getName().equals(targetActivity)) {
488                         aliasLauncherActivity = activityInfo.name;
489                         break;
490                     }
491                 }
492             }
493         } catch (PackageManager.NameNotFoundException e) {
494             Log.e(TAG, e.toString());
495         }
496         if (TextUtils.isEmpty(aliasLauncherActivity)) {
497             Log.e(TAG, "cannot find launcher activity");
498             return;
499         }
500 
501         if (enable) {
502             Log.d(TAG, "enable launcher activity: " + aliasLauncherActivity);
503             pm.setComponentEnabledSetting(
504                 new ComponentName(mContext, aliasLauncherActivity),
505                 PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
506         } else {
507             Log.d(TAG, "disable launcher activity: " + aliasLauncherActivity);
508             pm.setComponentEnabledSetting(
509                 new ComponentName(mContext, aliasLauncherActivity),
510                 PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
511         }
512     }
513 
log(String msg)514     private static void log(String msg) {
515         Log.d(TAG, msg);
516     }
517 
loge(String msg)518     private static void loge(String msg) {
519         Log.e(TAG, msg);
520     }
521 }
522