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