1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.cellbroadcastreceiver; 18 19 import static android.content.Context.MODE_PRIVATE; 20 import static android.telephony.SmsCbMessage.MESSAGE_FORMAT_3GPP; 21 22 import static com.android.cellbroadcastservice.CellBroadcastMetrics.FILTER_CDMA; 23 import static com.android.cellbroadcastservice.CellBroadcastMetrics.FILTER_GSM; 24 25 import android.content.Context; 26 import android.content.SharedPreferences; 27 import android.telephony.SmsCbMessage; 28 import android.util.Log; 29 import android.util.Pair; 30 31 import com.android.cellbroadcastservice.CellBroadcastModuleStatsLog; 32 import com.android.internal.annotations.VisibleForTesting; 33 34 import com.google.protobuf.InvalidProtocolBufferException; 35 36 import java.io.IOException; 37 import java.util.HashSet; 38 import java.util.Objects; 39 import java.util.StringJoiner; 40 41 /** 42 * CellBroadcastReceiverMetrics 43 * Logging featureUpdated when alert message is received or channel range is updated 44 * Logging onConfigUpdated when channel range is updated 45 */ 46 public class CellBroadcastReceiverMetrics { 47 48 private static final String TAG = "CellbroadcastReceiverMetrics"; 49 private static final boolean VDBG = false; 50 51 // Key to access the shared preference of cellbroadcast channel range information for metric. 52 public static final String CBR_CONFIG_UPDATED = "CellBroadcastConfigUpdated"; 53 // Key to access the shared preference of cellbroadcast receiver feature for metric. 54 private static final String CBR_METRIC_PREF = "CellBroadcastReceiverMetricSharedPref"; 55 private static final String CHANNEL_DELIMITER = ","; 56 private static final String RANGE_DELIMITER = "-"; 57 58 private static CellBroadcastReceiverMetrics sCbrMetrics; 59 60 HashSet<Pair<Integer, Integer>> mConfigUpdatedCachedChannelSet; 61 62 private FeatureMetrics mFeatureMetrics; 63 private FeatureMetrics mFeatureMetricsSharedPreferences; 64 65 /** 66 * Get instance of CellBroadcastReceiverMetrics. 67 */ 68 @VisibleForTesting getInstance()69 public static CellBroadcastReceiverMetrics getInstance() { 70 if (sCbrMetrics == null) { 71 sCbrMetrics = new CellBroadcastReceiverMetrics(); 72 } 73 return sCbrMetrics; 74 } 75 76 /** 77 * set cached feature metrics for current status 78 */ 79 @VisibleForTesting setFeatureMetrics( FeatureMetrics featureMetrics)80 public void setFeatureMetrics( 81 FeatureMetrics featureMetrics) { 82 mFeatureMetrics = featureMetrics; 83 } 84 85 /** 86 * get cached feature metrics for shared preferences 87 */ 88 @VisibleForTesting getFeatureMetricsSharedPreferences()89 public FeatureMetrics getFeatureMetricsSharedPreferences() { 90 return mFeatureMetricsSharedPreferences; 91 } 92 93 /** 94 * Set featureMetricsSharedPreferences 95 * 96 * @param featureMetricsSharedPreferences : Cbr features information 97 */ 98 @VisibleForTesting setFeatureMetricsSharedPreferences( FeatureMetrics featureMetricsSharedPreferences)99 public void setFeatureMetricsSharedPreferences( 100 FeatureMetrics featureMetricsSharedPreferences) { 101 mFeatureMetricsSharedPreferences = featureMetricsSharedPreferences; 102 } 103 104 /** 105 * Get current configuration channel set status 106 */ 107 @VisibleForTesting getCachedChannelSet()108 public HashSet<Pair<Integer, Integer>> getCachedChannelSet() { 109 return mConfigUpdatedCachedChannelSet; 110 } 111 112 /** 113 * CellbroadcastReceiverMetrics 114 * Logging featureUpdated as needed when alert message is received or channel range is updated 115 */ 116 public class FeatureMetrics implements Cloneable { 117 public static final String ALERT_IN_CALL = "enable_alert_handling_during_call"; 118 public static final String OVERRIDE_DND = "override_dnd"; 119 public static final String ROAMING_SUPPORT = "cmas_roaming_network_strings"; 120 public static final String STORE_SMS = "enable_write_alerts_to_sms_inbox"; 121 public static final String TEST_MODE = "testing_mode"; 122 public static final String TTS_MODE = "enable_alert_speech_default"; 123 public static final String TEST_MODE_ON_USER_BUILD = "allow_testing_mode_on_user_build"; 124 125 private boolean mAlertDuringCall; 126 private HashSet<Pair<Integer, Integer>> mDnDChannelSet; 127 private boolean mRoamingSupport; 128 private boolean mStoreSms; 129 private boolean mTestMode; 130 private boolean mEnableAlertSpeech; 131 private boolean mTestModeOnUserBuild; 132 133 private Context mContext; 134 FeatureMetrics(Context context)135 FeatureMetrics(Context context) { 136 mContext = context; 137 SharedPreferences sp = mContext.getSharedPreferences(CBR_METRIC_PREF, MODE_PRIVATE); 138 mAlertDuringCall = sp.getBoolean(ALERT_IN_CALL, false); 139 String strOverrideDnD = sp.getString(OVERRIDE_DND, String.valueOf(0)); 140 mDnDChannelSet = getChannelSetFromString(strOverrideDnD); 141 mRoamingSupport = sp.getBoolean(ROAMING_SUPPORT, false); 142 mStoreSms = sp.getBoolean(STORE_SMS, false); 143 mTestMode = sp.getBoolean(TEST_MODE, false); 144 mEnableAlertSpeech = sp.getBoolean(TTS_MODE, true); 145 mTestModeOnUserBuild = sp.getBoolean(TEST_MODE_ON_USER_BUILD, true); 146 } 147 148 @Override hashCode()149 public int hashCode() { 150 return Objects.hash(mDnDChannelSet, mAlertDuringCall, mRoamingSupport, mStoreSms, 151 mTestMode, mEnableAlertSpeech, mTestModeOnUserBuild); 152 } 153 154 @Override equals(Object object)155 public boolean equals(Object object) { 156 if (object instanceof FeatureMetrics) { 157 FeatureMetrics features = (FeatureMetrics) object; 158 return (this.mAlertDuringCall == features.mAlertDuringCall 159 && this.mDnDChannelSet.equals(features.mDnDChannelSet) 160 && this.mRoamingSupport == features.mRoamingSupport 161 && this.mStoreSms == features.mStoreSms 162 && this.mTestMode == features.mTestMode 163 && this.mEnableAlertSpeech == features.mEnableAlertSpeech 164 && this.mTestModeOnUserBuild == features.mTestModeOnUserBuild); 165 } 166 return false; 167 } 168 169 @Override clone()170 public Object clone() throws CloneNotSupportedException { 171 FeatureMetrics copy = (FeatureMetrics) super.clone(); 172 copy.mDnDChannelSet = new HashSet<>(); 173 copy.mDnDChannelSet.addAll(this.mDnDChannelSet); 174 return copy; 175 } 176 177 /** 178 * Get current status whether alert during call is enabled 179 */ 180 @VisibleForTesting isAlertDuringCall()181 public boolean isAlertDuringCall() { 182 return mAlertDuringCall; 183 } 184 185 /** 186 * Get current do not disturb channels set 187 */ 188 @VisibleForTesting getDnDChannelSet()189 public HashSet<Pair<Integer, Integer>> getDnDChannelSet() { 190 return mDnDChannelSet; 191 } 192 193 /** 194 * Get whether currently roaming supported 195 */ 196 @VisibleForTesting isRoamingSupport()197 public boolean isRoamingSupport() { 198 return mRoamingSupport; 199 } 200 201 /** 202 * Get whether alert messages are saved inbox 203 */ 204 @VisibleForTesting isStoreSms()205 public boolean isStoreSms() { 206 return mStoreSms; 207 } 208 209 /** 210 * Get whether test mode is enabled 211 */ 212 @VisibleForTesting isTestMode()213 public boolean isTestMode() { 214 return mTestMode; 215 } 216 217 /** 218 * Get whether alert message support text to speech 219 */ 220 @VisibleForTesting isEnableAlertSpeech()221 public boolean isEnableAlertSpeech() { 222 return mEnableAlertSpeech; 223 } 224 225 /** 226 * Get whether test mode is not supporting in user build status 227 */ 228 @VisibleForTesting isTestModeOnUserBuild()229 public boolean isTestModeOnUserBuild() { 230 return mTestModeOnUserBuild; 231 } 232 233 /** 234 * Set alert during call 235 * 236 * @param current : current status of alert during call 237 */ 238 @VisibleForTesting onChangedAlertDuringCall(boolean current)239 public void onChangedAlertDuringCall(boolean current) { 240 mAlertDuringCall = current; 241 } 242 243 /** 244 * Set alert during call 245 * 246 * @param channelManager : channel manager to get channel range supporting override dnd 247 * @param overAllDnD : whether override dnd is fully supported or not 248 */ 249 @VisibleForTesting onChangedOverrideDnD( CellBroadcastChannelManager channelManager, boolean overAllDnD)250 public void onChangedOverrideDnD( 251 CellBroadcastChannelManager channelManager, boolean overAllDnD) { 252 mDnDChannelSet.clear(); 253 if (overAllDnD) { 254 mDnDChannelSet.add(new Pair(Integer.MAX_VALUE, Integer.MAX_VALUE)); 255 } else { 256 channelManager.getAllCellBroadcastChannelRanges().forEach(r -> { 257 if (r.mOverrideDnd) { 258 mDnDChannelSet.add(new Pair(r.mStartId, r.mEndId)); 259 } 260 }); 261 if (mDnDChannelSet.size() == 0) { 262 mDnDChannelSet.add(new Pair(0, 0)); 263 } 264 } 265 } 266 267 /** 268 * Set roaming support 269 * 270 * @param current : current status of roaming support 271 */ 272 @VisibleForTesting onChangedRoamingSupport(boolean current)273 public void onChangedRoamingSupport(boolean current) { 274 mRoamingSupport = current; 275 } 276 277 /** 278 * Set current status of storing alert message inbox 279 * 280 * @param current : current status value of storing inbox 281 */ 282 @VisibleForTesting onChangedStoreSms(boolean current)283 public void onChangedStoreSms(boolean current) { 284 mStoreSms = current; 285 } 286 287 /** 288 * Set current status of test-mode 289 * 290 * @param current : current status value of test-mode 291 */ 292 @VisibleForTesting onChangedTestMode(boolean current)293 public void onChangedTestMode(boolean current) { 294 mTestMode = current; 295 } 296 297 /** 298 * Set whether text to speech is supported for alert message 299 * 300 * @param current : current status tts 301 */ 302 @VisibleForTesting onChangedEnableAlertSpeech(boolean current)303 public void onChangedEnableAlertSpeech(boolean current) { 304 mEnableAlertSpeech = current; 305 } 306 307 /** 308 * Set whether test mode on user build is supported 309 * 310 * @param current : current status of test mode on user build 311 */ onChangedTestModeOnUserBuild(boolean current)312 public void onChangedTestModeOnUserBuild(boolean current) { 313 mTestModeOnUserBuild = current; 314 } 315 316 /** 317 * Calling check-in method for CB_SERVICE_FEATURE 318 */ 319 @VisibleForTesting logFeatureChanged()320 public void logFeatureChanged() { 321 try { 322 CellBroadcastModuleStatsLog.write( 323 CellBroadcastModuleStatsLog.CB_RECEIVER_FEATURE_CHANGED, 324 mAlertDuringCall, 325 convertToProtoBuffer(mDnDChannelSet), 326 mRoamingSupport, 327 mStoreSms, 328 mTestMode, 329 mEnableAlertSpeech, 330 mTestModeOnUserBuild); 331 } catch (IOException e) { 332 Log.e(TAG, "IOException while encoding array byte from channel set" + e); 333 } 334 if (VDBG) Log.d(TAG, this.toString()); 335 } 336 337 /** 338 * Update preferences for receiver feature metrics 339 */ updateSharedPreferences()340 public void updateSharedPreferences() { 341 SharedPreferences sp = 342 mContext.getSharedPreferences(CBR_METRIC_PREF, MODE_PRIVATE); 343 SharedPreferences.Editor editor = sp.edit(); 344 editor.putBoolean(ALERT_IN_CALL, mAlertDuringCall); 345 editor.putString(OVERRIDE_DND, getStringFromChannelSet(mDnDChannelSet)); 346 editor.putBoolean(ROAMING_SUPPORT, mRoamingSupport); 347 editor.putBoolean(STORE_SMS, mStoreSms); 348 editor.putBoolean(TEST_MODE, mTestMode); 349 editor.putBoolean(TTS_MODE, mEnableAlertSpeech); 350 editor.putBoolean(TEST_MODE_ON_USER_BUILD, mTestModeOnUserBuild); 351 editor.apply(); 352 } 353 354 @Override toString()355 public String toString() { 356 return "CellBroadcast_Receiver_Feature : " 357 + "mAlertDuringCall = " + mAlertDuringCall + " | " 358 + "mOverrideDnD = " + getStringFromChannelSet(mDnDChannelSet) + " | " 359 + "mRoamingSupport = " + mRoamingSupport + " | " 360 + "mStoreSms = " + mStoreSms + " | " 361 + "mTestMode = " + mTestMode + " | " 362 + "mEnableAlertSpeech = " + mEnableAlertSpeech + " | " 363 + "mTestModeOnUserBuild = " + mTestModeOnUserBuild; 364 } 365 } 366 367 /** 368 * Get current feature metrics 369 * 370 * @param context : Context 371 */ 372 @VisibleForTesting getFeatureMetrics(Context context)373 public FeatureMetrics getFeatureMetrics(Context context) { 374 if (mFeatureMetrics == null) { 375 mFeatureMetrics = new FeatureMetrics(context); 376 mFeatureMetricsSharedPreferences = new FeatureMetrics(context); 377 } 378 return mFeatureMetrics; 379 } 380 381 382 /** 383 * Convert ChannelSet to ProtoBuffer 384 * 385 * @param rangeList : channel range set 386 */ 387 @VisibleForTesting convertToProtoBuffer(HashSet<Pair<Integer, Integer>> rangeList)388 public byte[] convertToProtoBuffer(HashSet<Pair<Integer, Integer>> rangeList) 389 throws IOException { 390 Cellbroadcastmetric.CellBroadcastChannelRangesProto.Builder rangeListBuilder = 391 Cellbroadcastmetric.CellBroadcastChannelRangesProto.newBuilder(); 392 rangeList.stream().sorted((o1, o2) -> o1.first == o2.first ? o1.second - o2.second 393 : o1.first - o2.first).forEach(pair -> { 394 Cellbroadcastmetric.CellBroadcastChannelRangeProto.Builder rangeBuilder = 395 Cellbroadcastmetric.CellBroadcastChannelRangeProto.newBuilder(); 396 rangeBuilder.setStart(pair.first); 397 rangeBuilder.setEnd(pair.second); 398 rangeListBuilder.addChannelRanges(rangeBuilder); 399 if (VDBG) { 400 Log.d(TAG, "[first] : " + pair.first + " [second] : " + pair.second); 401 } 402 }); 403 return rangeListBuilder.build().toByteArray(); 404 } 405 406 /** 407 * Convert ProtoBuffer to ChannelSet 408 * 409 * @param arrayByte : channel range set encoded arrayByte 410 */ 411 @VisibleForTesting getDataFromProtoArrayByte(byte[] arrayByte)412 public HashSet<Pair<Integer, Integer>> getDataFromProtoArrayByte(byte[] arrayByte) 413 throws InvalidProtocolBufferException { 414 HashSet<Pair<Integer, Integer>> convertResult = new HashSet<>(); 415 416 Cellbroadcastmetric.CellBroadcastChannelRangesProto channelRangesProto = 417 Cellbroadcastmetric.CellBroadcastChannelRangesProto 418 .parser().parseFrom(arrayByte); 419 420 for (Cellbroadcastmetric.CellBroadcastChannelRangeProto range : 421 channelRangesProto.getChannelRangesList()) { 422 convertResult.add(new Pair(range.getStart(), range.getEnd())); 423 } 424 425 return convertResult; 426 } 427 428 /** 429 * When feature changed and net alert message received then check-in logging 430 * 431 * @param context : Context 432 */ 433 @VisibleForTesting logFeatureChangedAsNeeded(Context context)434 public void logFeatureChangedAsNeeded(Context context) { 435 if (!getFeatureMetrics(context).equals(mFeatureMetricsSharedPreferences)) { 436 mFeatureMetrics.logFeatureChanged(); 437 mFeatureMetrics.updateSharedPreferences(); 438 try { 439 mFeatureMetricsSharedPreferences = (FeatureMetrics) mFeatureMetrics.clone(); 440 } catch (CloneNotSupportedException e) { 441 Log.e(TAG, "CloneNotSupportedException error" + e); 442 } 443 } 444 } 445 446 /** 447 * Convert ChannelSet to String 448 * 449 * @param curChRangeSet : channel range set 450 */ 451 @VisibleForTesting getStringFromChannelSet( HashSet<Pair<Integer, Integer>> curChRangeSet)452 public String getStringFromChannelSet( 453 HashSet<Pair<Integer, Integer>> curChRangeSet) { 454 StringJoiner strChannelList = new StringJoiner(CHANNEL_DELIMITER); 455 curChRangeSet.forEach(pair -> strChannelList.add( 456 pair.first.equals(pair.second) 457 ? String.valueOf(pair.first) : pair.first + RANGE_DELIMITER + pair.second)); 458 return strChannelList.toString(); 459 } 460 461 /** 462 * Convert String to ChannelSet 463 * 464 * @param strChannelRange : channel range string 465 */ getChannelSetFromString(String strChannelRange)466 public HashSet<Pair<Integer, Integer>> getChannelSetFromString(String strChannelRange) { 467 String[] arrStringChannelRange = strChannelRange.split(CHANNEL_DELIMITER); 468 HashSet<Pair<Integer, Integer>> channelSet = new HashSet<>(); 469 try { 470 for (String chRange : arrStringChannelRange) { 471 if (chRange.contains(RANGE_DELIMITER)) { 472 String[] range = chRange.split(RANGE_DELIMITER); 473 channelSet.add( 474 new Pair(Integer.parseInt(range[0].trim()), 475 Integer.parseInt(range[1].trim()))); 476 } else { 477 channelSet.add(new Pair(Integer.parseInt(chRange.trim()), 478 Integer.parseInt(chRange.trim()))); 479 } 480 } 481 } catch (NumberFormatException e) { 482 Log.e(TAG, "NumberFormatException error" + e); 483 } 484 return channelSet; 485 } 486 487 /** 488 * Create a new onConfigUpdated 489 * 490 * @param context : Context 491 * @param roamingMccMnc : country and operator information 492 * @param curChRangeSet : channel range list information 493 */ onConfigUpdated(Context context, String roamingMccMnc, HashSet<Pair<Integer, Integer>> curChRangeSet)494 public void onConfigUpdated(Context context, String roamingMccMnc, 495 HashSet<Pair<Integer, Integer>> curChRangeSet) { 496 497 if (curChRangeSet == null) return; 498 499 SharedPreferences sp = context.getSharedPreferences(CBR_METRIC_PREF, MODE_PRIVATE); 500 501 if (mConfigUpdatedCachedChannelSet == null) { 502 mConfigUpdatedCachedChannelSet = getChannelSetFromString( 503 sp.getString(CBR_CONFIG_UPDATED, String.valueOf(0))); 504 } 505 506 if (!curChRangeSet.equals(mConfigUpdatedCachedChannelSet)) { 507 logFeatureChangedAsNeeded(context); 508 try { 509 byte[] byteArrayChannelRange = convertToProtoBuffer(curChRangeSet); 510 if (byteArrayChannelRange != null) { 511 CellBroadcastModuleStatsLog.write( 512 CellBroadcastModuleStatsLog.CB_CONFIG_UPDATED, 513 roamingMccMnc, byteArrayChannelRange); 514 515 String stringConfigurationUpdated = getStringFromChannelSet(curChRangeSet); 516 SharedPreferences.Editor editor = sp.edit(); 517 editor.putString(CBR_CONFIG_UPDATED, stringConfigurationUpdated); 518 editor.apply(); 519 520 mConfigUpdatedCachedChannelSet.clear(); 521 mConfigUpdatedCachedChannelSet.addAll(curChRangeSet); 522 523 if (VDBG) { 524 Log.d(TAG, "onConfigUpdated : roamingMccMnc is = " + roamingMccMnc 525 + " | channelRange is = " + stringConfigurationUpdated); 526 } 527 } 528 } catch (RuntimeException | IOException e) { 529 Log.e(TAG, "Exception error occur " + e.getMessage()); 530 } 531 } 532 } 533 534 /** 535 * Create a new logMessageReported 536 * 537 * @param context : Context 538 * @param type : radio type 539 * @param source : layer of reported message 540 * @param serialNo : unique identifier of message 541 * @param msgId : service_category of message 542 */ logMessageReported(Context context, int type, int source, int serialNo, int msgId)543 void logMessageReported(Context context, int type, int source, int serialNo, int msgId) { 544 if (VDBG) { 545 Log.d(TAG, 546 "logMessageReported : " + type + " " + source + " " + serialNo + " " 547 + msgId); 548 } 549 CellBroadcastModuleStatsLog.write(CellBroadcastModuleStatsLog.CB_MESSAGE_REPORTED, 550 type, source, serialNo, msgId); 551 } 552 553 /** 554 * Create a new logMessageFiltered 555 * 556 * @param filterType : reason type of filtered 557 * @param msg : sms cell broadcast message information 558 */ logMessageFiltered(int filterType, SmsCbMessage msg)559 void logMessageFiltered(int filterType, SmsCbMessage msg) { 560 int ratType = msg.getMessageFormat() == MESSAGE_FORMAT_3GPP ? FILTER_GSM : FILTER_CDMA; 561 if (VDBG) { 562 Log.d(TAG, "logMessageFiltered : " + ratType + " " + filterType + " " 563 + msg.getSerialNumber() + " " + msg.getServiceCategory()); 564 } 565 CellBroadcastModuleStatsLog.write(CellBroadcastModuleStatsLog.CB_MESSAGE_FILTERED, 566 ratType, filterType, msg.getSerialNumber(), msg.getServiceCategory()); 567 } 568 569 /** 570 * Create a new logModuleError 571 * 572 * @param source : where this log happened 573 * @param errorType : type of error 574 */ logModuleError(int source, int errorType)575 void logModuleError(int source, int errorType) { 576 if (VDBG) { 577 Log.d(TAG, "logModuleError : " + source + " " + errorType); 578 } 579 CellBroadcastModuleStatsLog.write(CellBroadcastModuleStatsLog.CB_MODULE_ERROR_REPORTED, 580 source, errorType); 581 } 582 } 583