/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.cellbroadcastreceiver; import static android.content.Context.MODE_PRIVATE; import static android.telephony.SmsCbMessage.MESSAGE_FORMAT_3GPP; import static com.android.cellbroadcastservice.CellBroadcastMetrics.FILTER_CDMA; import static com.android.cellbroadcastservice.CellBroadcastMetrics.FILTER_GSM; import android.content.Context; import android.content.SharedPreferences; import android.telephony.SmsCbMessage; import android.util.Log; import android.util.Pair; import com.android.cellbroadcastservice.CellBroadcastModuleStatsLog; import com.android.internal.annotations.VisibleForTesting; import com.google.protobuf.InvalidProtocolBufferException; import java.io.IOException; import java.util.HashSet; import java.util.Objects; import java.util.StringJoiner; /** * CellBroadcastReceiverMetrics * Logging featureUpdated when alert message is received or channel range is updated * Logging onConfigUpdated when channel range is updated */ public class CellBroadcastReceiverMetrics { private static final String TAG = "CellbroadcastReceiverMetrics"; private static final boolean VDBG = false; // Key to access the shared preference of cellbroadcast channel range information for metric. public static final String CBR_CONFIG_UPDATED = "CellBroadcastConfigUpdated"; // Key to access the shared preference of cellbroadcast receiver feature for metric. private static final String CBR_METRIC_PREF = "CellBroadcastReceiverMetricSharedPref"; private static final String CHANNEL_DELIMITER = ","; private static final String RANGE_DELIMITER = "-"; private static CellBroadcastReceiverMetrics sCbrMetrics; HashSet> mConfigUpdatedCachedChannelSet; private FeatureMetrics mFeatureMetrics; private FeatureMetrics mFeatureMetricsSharedPreferences; /** * Get instance of CellBroadcastReceiverMetrics. */ public static CellBroadcastReceiverMetrics getInstance() { if (sCbrMetrics == null) { sCbrMetrics = new CellBroadcastReceiverMetrics(); } return sCbrMetrics; } /** * set cached feature metrics for current status */ @VisibleForTesting public void setFeatureMetrics( FeatureMetrics featureMetrics) { mFeatureMetrics = featureMetrics; } /** * get cached feature metrics for shared preferences */ @VisibleForTesting public FeatureMetrics getFeatureMetricsSharedPreferences() { return mFeatureMetricsSharedPreferences; } /** * Set featureMetricsSharedPreferences * * @param featureMetricsSharedPreferences : Cbr features information */ @VisibleForTesting public void setFeatureMetricsSharedPreferences( FeatureMetrics featureMetricsSharedPreferences) { mFeatureMetricsSharedPreferences = featureMetricsSharedPreferences; } /** * Get current configuration channel set status */ @VisibleForTesting public HashSet> getCachedChannelSet() { return mConfigUpdatedCachedChannelSet; } /** * CellbroadcastReceiverMetrics * Logging featureUpdated as needed when alert message is received or channel range is updated */ public class FeatureMetrics implements Cloneable { public static final String ALERT_IN_CALL = "enable_alert_handling_during_call"; public static final String OVERRIDE_DND = "override_dnd"; public static final String ROAMING_SUPPORT = "cmas_roaming_network_strings"; public static final String STORE_SMS = "enable_write_alerts_to_sms_inbox"; public static final String TEST_MODE = "testing_mode"; public static final String TTS_MODE = "enable_alert_speech_default"; public static final String TEST_MODE_ON_USER_BUILD = "allow_testing_mode_on_user_build"; private boolean mAlertDuringCall; private HashSet> mDnDChannelSet; private boolean mRoamingSupport; private boolean mStoreSms; private boolean mTestMode; private boolean mEnableAlertSpeech; private boolean mTestModeOnUserBuild; private Context mContext; FeatureMetrics(Context context) { mContext = context; SharedPreferences sp = mContext.getSharedPreferences(CBR_METRIC_PREF, MODE_PRIVATE); mAlertDuringCall = sp.getBoolean(ALERT_IN_CALL, false); String strOverrideDnD = sp.getString(OVERRIDE_DND, String.valueOf(0)); mDnDChannelSet = getChannelSetFromString(strOverrideDnD); mRoamingSupport = sp.getBoolean(ROAMING_SUPPORT, false); mStoreSms = sp.getBoolean(STORE_SMS, false); mTestMode = sp.getBoolean(TEST_MODE, false); mEnableAlertSpeech = sp.getBoolean(TTS_MODE, true); mTestModeOnUserBuild = sp.getBoolean(TEST_MODE_ON_USER_BUILD, true); } @Override public int hashCode() { return Objects.hash(mDnDChannelSet, mAlertDuringCall, mRoamingSupport, mStoreSms, mTestMode, mEnableAlertSpeech, mTestModeOnUserBuild); } @Override public boolean equals(Object object) { if (object instanceof FeatureMetrics) { FeatureMetrics features = (FeatureMetrics) object; return (this.mAlertDuringCall == features.mAlertDuringCall && this.mDnDChannelSet.equals(features.mDnDChannelSet) && this.mRoamingSupport == features.mRoamingSupport && this.mStoreSms == features.mStoreSms && this.mTestMode == features.mTestMode && this.mEnableAlertSpeech == features.mEnableAlertSpeech && this.mTestModeOnUserBuild == features.mTestModeOnUserBuild); } return false; } @Override public Object clone() throws CloneNotSupportedException { FeatureMetrics copy = (FeatureMetrics) super.clone(); copy.mDnDChannelSet = new HashSet<>(); copy.mDnDChannelSet.addAll(this.mDnDChannelSet); return copy; } /** * Get current status whether alert during call is enabled */ @VisibleForTesting public boolean isAlertDuringCall() { return mAlertDuringCall; } /** * Get current do not disturb channels set */ @VisibleForTesting public HashSet> getDnDChannelSet() { return mDnDChannelSet; } /** * Get whether currently roaming supported */ @VisibleForTesting public boolean isRoamingSupport() { return mRoamingSupport; } /** * Get whether alert messages are saved inbox */ @VisibleForTesting public boolean isStoreSms() { return mStoreSms; } /** * Get whether test mode is enabled */ @VisibleForTesting public boolean isTestMode() { return mTestMode; } /** * Get whether alert message support text to speech */ @VisibleForTesting public boolean isEnableAlertSpeech() { return mEnableAlertSpeech; } /** * Get whether test mode is not supporting in user build status */ @VisibleForTesting public boolean isTestModeOnUserBuild() { return mTestModeOnUserBuild; } /** * Set alert during call * * @param current : current status of alert during call */ @VisibleForTesting public void onChangedAlertDuringCall(boolean current) { mAlertDuringCall = current; } /** * Set alert during call * * @param channelManager : channel manager to get channel range supporting override dnd * @param overAllDnD : whether override dnd is fully supported or not */ @VisibleForTesting public void onChangedOverrideDnD( CellBroadcastChannelManager channelManager, boolean overAllDnD) { mDnDChannelSet.clear(); if (overAllDnD) { mDnDChannelSet.add(new Pair(Integer.MAX_VALUE, Integer.MAX_VALUE)); } else { channelManager.getAllCellBroadcastChannelRanges().forEach(r -> { if (r.mOverrideDnd) { mDnDChannelSet.add(new Pair(r.mStartId, r.mEndId)); } }); if (mDnDChannelSet.size() == 0) { mDnDChannelSet.add(new Pair(0, 0)); } } } /** * Set roaming support * * @param current : current status of roaming support */ @VisibleForTesting public void onChangedRoamingSupport(boolean current) { mRoamingSupport = current; } /** * Set current status of storing alert message inbox * * @param current : current status value of storing inbox */ @VisibleForTesting public void onChangedStoreSms(boolean current) { mStoreSms = current; } /** * Set current status of test-mode * * @param current : current status value of test-mode */ @VisibleForTesting public void onChangedTestMode(boolean current) { mTestMode = current; } /** * Set whether text to speech is supported for alert message * * @param current : current status tts */ @VisibleForTesting public void onChangedEnableAlertSpeech(boolean current) { mEnableAlertSpeech = current; } /** * Set whether test mode on user build is supported * * @param current : current status of test mode on user build */ public void onChangedTestModeOnUserBuild(boolean current) { mTestModeOnUserBuild = current; } /** * Calling check-in method for CB_SERVICE_FEATURE */ @VisibleForTesting public void logFeatureChanged() { try { CellBroadcastModuleStatsLog.write( CellBroadcastModuleStatsLog.CB_RECEIVER_FEATURE_CHANGED, mAlertDuringCall, convertToProtoBuffer(mDnDChannelSet), mRoamingSupport, mStoreSms, mTestMode, mEnableAlertSpeech, mTestModeOnUserBuild); } catch (IOException e) { Log.e(TAG, "IOException while encoding array byte from channel set" + e); } if (VDBG) Log.d(TAG, this.toString()); } /** * Update preferences for receiver feature metrics */ public void updateSharedPreferences() { SharedPreferences sp = mContext.getSharedPreferences(CBR_METRIC_PREF, MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putBoolean(ALERT_IN_CALL, mAlertDuringCall); editor.putString(OVERRIDE_DND, getStringFromChannelSet(mDnDChannelSet)); editor.putBoolean(ROAMING_SUPPORT, mRoamingSupport); editor.putBoolean(STORE_SMS, mStoreSms); editor.putBoolean(TEST_MODE, mTestMode); editor.putBoolean(TTS_MODE, mEnableAlertSpeech); editor.putBoolean(TEST_MODE_ON_USER_BUILD, mTestModeOnUserBuild); editor.apply(); } @Override public String toString() { return "CellBroadcast_Receiver_Feature : " + "mAlertDuringCall = " + mAlertDuringCall + " | " + "mOverrideDnD = " + getStringFromChannelSet(mDnDChannelSet) + " | " + "mRoamingSupport = " + mRoamingSupport + " | " + "mStoreSms = " + mStoreSms + " | " + "mTestMode = " + mTestMode + " | " + "mEnableAlertSpeech = " + mEnableAlertSpeech + " | " + "mTestModeOnUserBuild = " + mTestModeOnUserBuild; } } /** * Get current feature metrics * * @param context : Context */ @VisibleForTesting public FeatureMetrics getFeatureMetrics(Context context) { if (mFeatureMetrics == null) { mFeatureMetrics = new FeatureMetrics(context); mFeatureMetricsSharedPreferences = new FeatureMetrics(context); } return mFeatureMetrics; } /** * Convert ChannelSet to ProtoBuffer * * @param rangeList : channel range set */ @VisibleForTesting public byte[] convertToProtoBuffer(HashSet> rangeList) throws IOException { Cellbroadcastmetric.CellBroadcastChannelRangesProto.Builder rangeListBuilder = Cellbroadcastmetric.CellBroadcastChannelRangesProto.newBuilder(); rangeList.stream().sorted((o1, o2) -> Objects.equals(o1.first, o2.first) ? o1.second - o2.second : o1.first - o2.first).forEach(pair -> { Cellbroadcastmetric.CellBroadcastChannelRangeProto.Builder rangeBuilder = Cellbroadcastmetric.CellBroadcastChannelRangeProto.newBuilder(); rangeBuilder.setStart(pair.first); rangeBuilder.setEnd(pair.second); rangeListBuilder.addChannelRanges(rangeBuilder); if (VDBG) { Log.d(TAG, "[first] : " + pair.first + " [second] : " + pair.second); } }); return rangeListBuilder.build().toByteArray(); } /** * Convert ProtoBuffer to ChannelSet * * @param arrayByte : channel range set encoded arrayByte */ @VisibleForTesting public HashSet> getDataFromProtoArrayByte(byte[] arrayByte) throws InvalidProtocolBufferException { HashSet> convertResult = new HashSet<>(); Cellbroadcastmetric.CellBroadcastChannelRangesProto channelRangesProto = Cellbroadcastmetric.CellBroadcastChannelRangesProto .parser().parseFrom(arrayByte); for (Cellbroadcastmetric.CellBroadcastChannelRangeProto range : channelRangesProto.getChannelRangesList()) { convertResult.add(new Pair(range.getStart(), range.getEnd())); } return convertResult; } /** * When feature changed and net alert message received then check-in logging * * @param context : Context */ @VisibleForTesting public void logFeatureChangedAsNeeded(Context context) { if (!getFeatureMetrics(context).equals(mFeatureMetricsSharedPreferences)) { mFeatureMetrics.logFeatureChanged(); mFeatureMetrics.updateSharedPreferences(); try { mFeatureMetricsSharedPreferences = (FeatureMetrics) mFeatureMetrics.clone(); } catch (CloneNotSupportedException e) { Log.e(TAG, "CloneNotSupportedException error" + e); } } } /** * Convert ChannelSet to String * * @param curChRangeSet : channel range set */ @VisibleForTesting public String getStringFromChannelSet( HashSet> curChRangeSet) { StringJoiner strChannelList = new StringJoiner(CHANNEL_DELIMITER); curChRangeSet.forEach(pair -> strChannelList.add( pair.first.equals(pair.second) ? String.valueOf(pair.first) : pair.first + RANGE_DELIMITER + pair.second)); return strChannelList.toString(); } /** * Convert String to ChannelSet * * @param strChannelRange : channel range string */ public HashSet> getChannelSetFromString(String strChannelRange) { String[] arrStringChannelRange = strChannelRange.split(CHANNEL_DELIMITER); HashSet> channelSet = new HashSet<>(); try { for (String chRange : arrStringChannelRange) { if (chRange.contains(RANGE_DELIMITER)) { String[] range = chRange.split(RANGE_DELIMITER); channelSet.add( new Pair(Integer.parseInt(range[0].trim()), Integer.parseInt(range[1].trim()))); } else { channelSet.add(new Pair(Integer.parseInt(chRange.trim()), Integer.parseInt(chRange.trim()))); } } } catch (NumberFormatException e) { Log.e(TAG, "NumberFormatException error" + e); } return channelSet; } /** * Create a new onConfigUpdated * * @param context : Context * @param roamingMccMnc : country and operator information * @param curChRangeSet : channel range list information */ public void onConfigUpdated(Context context, String roamingMccMnc, HashSet> curChRangeSet) { if (curChRangeSet == null) return; SharedPreferences sp = context.getSharedPreferences(CBR_METRIC_PREF, MODE_PRIVATE); if (mConfigUpdatedCachedChannelSet == null) { mConfigUpdatedCachedChannelSet = getChannelSetFromString( sp.getString(CBR_CONFIG_UPDATED, String.valueOf(0))); } if (!curChRangeSet.equals(mConfigUpdatedCachedChannelSet)) { logFeatureChangedAsNeeded(context); try { byte[] byteArrayChannelRange = convertToProtoBuffer(curChRangeSet); if (byteArrayChannelRange != null) { CellBroadcastModuleStatsLog.write( CellBroadcastModuleStatsLog.CB_CONFIG_UPDATED, roamingMccMnc, byteArrayChannelRange); String stringConfigurationUpdated = getStringFromChannelSet(curChRangeSet); SharedPreferences.Editor editor = sp.edit(); editor.putString(CBR_CONFIG_UPDATED, stringConfigurationUpdated); editor.apply(); mConfigUpdatedCachedChannelSet.clear(); mConfigUpdatedCachedChannelSet.addAll(curChRangeSet); if (VDBG) { Log.d(TAG, "onConfigUpdated : roamingMccMnc is = " + roamingMccMnc + " | channelRange is = " + stringConfigurationUpdated); } } } catch (RuntimeException | IOException e) { Log.e(TAG, "Exception error occur " + e.getMessage()); } } } /** * Create a new logMessageReported * * @param context : Context * @param type : radio type * @param source : layer of reported message * @param serialNo : unique identifier of message * @param msgId : service_category of message */ void logMessageReported(Context context, int type, int source, int serialNo, int msgId) { if (VDBG) { Log.d(TAG, "logMessageReported : " + type + " " + source + " " + serialNo + " " + msgId); } CellBroadcastModuleStatsLog.write(CellBroadcastModuleStatsLog.CB_MESSAGE_REPORTED, type, source, serialNo, msgId); } /** * Create a new logMessageFiltered * * @param filterType : reason type of filtered * @param msg : sms cell broadcast message information */ void logMessageFiltered(int filterType, SmsCbMessage msg) { int ratType = msg.getMessageFormat() == MESSAGE_FORMAT_3GPP ? FILTER_GSM : FILTER_CDMA; if (VDBG) { Log.d(TAG, "logMessageFiltered : " + ratType + " " + filterType + " " + msg.getSerialNumber() + " " + msg.getServiceCategory()); } CellBroadcastModuleStatsLog.write(CellBroadcastModuleStatsLog.CB_MESSAGE_FILTERED, ratType, filterType, msg.getSerialNumber(), msg.getServiceCategory()); } /** * Create a new logModuleError * * @param source : where this log happened * @param errorType : type of error */ void logModuleError(int source, int errorType) { if (VDBG) { Log.d(TAG, "logModuleError : " + source + " " + errorType); } CellBroadcastModuleStatsLog.write(CellBroadcastModuleStatsLog.CB_MODULE_ERROR_REPORTED, source, errorType); } }