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