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      */
getInstance()68     public static CellBroadcastReceiverMetrics getInstance() {
69         if (sCbrMetrics == null) {
70             sCbrMetrics = new CellBroadcastReceiverMetrics();
71         }
72         return sCbrMetrics;
73     }
74 
75     /**
76      * set cached feature metrics for current status
77      */
78     @VisibleForTesting
setFeatureMetrics( FeatureMetrics featureMetrics)79     public void setFeatureMetrics(
80             FeatureMetrics featureMetrics) {
81         mFeatureMetrics = featureMetrics;
82     }
83 
84     /**
85      * get cached feature metrics for shared preferences
86      */
87     @VisibleForTesting
getFeatureMetricsSharedPreferences()88     public FeatureMetrics getFeatureMetricsSharedPreferences() {
89         return mFeatureMetricsSharedPreferences;
90     }
91 
92     /**
93      * Set featureMetricsSharedPreferences
94      *
95      * @param featureMetricsSharedPreferences : Cbr features information
96      */
97     @VisibleForTesting
setFeatureMetricsSharedPreferences( FeatureMetrics featureMetricsSharedPreferences)98     public void setFeatureMetricsSharedPreferences(
99             FeatureMetrics featureMetricsSharedPreferences) {
100         mFeatureMetricsSharedPreferences = featureMetricsSharedPreferences;
101     }
102 
103     /**
104      * Get current configuration channel set status
105      */
106     @VisibleForTesting
getCachedChannelSet()107     public HashSet<Pair<Integer, Integer>> getCachedChannelSet() {
108         return mConfigUpdatedCachedChannelSet;
109     }
110 
111     /**
112      * CellbroadcastReceiverMetrics
113      * Logging featureUpdated as needed when alert message is received or channel range is updated
114      */
115     public class FeatureMetrics implements Cloneable {
116         public static final String ALERT_IN_CALL = "enable_alert_handling_during_call";
117         public static final String OVERRIDE_DND = "override_dnd";
118         public static final String ROAMING_SUPPORT = "cmas_roaming_network_strings";
119         public static final String STORE_SMS = "enable_write_alerts_to_sms_inbox";
120         public static final String TEST_MODE = "testing_mode";
121         public static final String TTS_MODE = "enable_alert_speech_default";
122         public static final String TEST_MODE_ON_USER_BUILD = "allow_testing_mode_on_user_build";
123 
124         private boolean mAlertDuringCall;
125         private HashSet<Pair<Integer, Integer>> mDnDChannelSet;
126         private boolean mRoamingSupport;
127         private boolean mStoreSms;
128         private boolean mTestMode;
129         private boolean mEnableAlertSpeech;
130         private boolean mTestModeOnUserBuild;
131 
132         private Context mContext;
133 
FeatureMetrics(Context context)134         FeatureMetrics(Context context) {
135             mContext = context;
136             SharedPreferences sp = mContext.getSharedPreferences(CBR_METRIC_PREF, MODE_PRIVATE);
137             mAlertDuringCall = sp.getBoolean(ALERT_IN_CALL, false);
138             String strOverrideDnD = sp.getString(OVERRIDE_DND, String.valueOf(0));
139             mDnDChannelSet = getChannelSetFromString(strOverrideDnD);
140             mRoamingSupport = sp.getBoolean(ROAMING_SUPPORT, false);
141             mStoreSms = sp.getBoolean(STORE_SMS, false);
142             mTestMode = sp.getBoolean(TEST_MODE, false);
143             mEnableAlertSpeech = sp.getBoolean(TTS_MODE, true);
144             mTestModeOnUserBuild = sp.getBoolean(TEST_MODE_ON_USER_BUILD, true);
145         }
146 
147         @Override
hashCode()148         public int hashCode() {
149             return Objects.hash(mDnDChannelSet, mAlertDuringCall, mRoamingSupport, mStoreSms,
150                     mTestMode, mEnableAlertSpeech, mTestModeOnUserBuild);
151         }
152 
153         @Override
equals(Object object)154         public boolean equals(Object object) {
155             if (object instanceof FeatureMetrics) {
156                 FeatureMetrics features = (FeatureMetrics) object;
157                 return (this.mAlertDuringCall == features.mAlertDuringCall
158                         && this.mDnDChannelSet.equals(features.mDnDChannelSet)
159                         && this.mRoamingSupport == features.mRoamingSupport
160                         && this.mStoreSms == features.mStoreSms
161                         && this.mTestMode == features.mTestMode
162                         && this.mEnableAlertSpeech == features.mEnableAlertSpeech
163                         && this.mTestModeOnUserBuild == features.mTestModeOnUserBuild);
164             }
165             return false;
166         }
167 
168         @Override
clone()169         public Object clone() throws CloneNotSupportedException {
170             FeatureMetrics copy = (FeatureMetrics) super.clone();
171             copy.mDnDChannelSet = new HashSet<>();
172             copy.mDnDChannelSet.addAll(this.mDnDChannelSet);
173             return copy;
174         }
175 
176         /**
177          * Get current status whether alert during call is enabled
178          */
179         @VisibleForTesting
isAlertDuringCall()180         public boolean isAlertDuringCall() {
181             return mAlertDuringCall;
182         }
183 
184         /**
185          * Get current do not disturb channels set
186          */
187         @VisibleForTesting
getDnDChannelSet()188         public HashSet<Pair<Integer, Integer>> getDnDChannelSet() {
189             return mDnDChannelSet;
190         }
191 
192         /**
193          * Get whether currently roaming supported
194          */
195         @VisibleForTesting
isRoamingSupport()196         public boolean isRoamingSupport() {
197             return mRoamingSupport;
198         }
199 
200         /**
201          * Get whether alert messages are saved inbox
202          */
203         @VisibleForTesting
isStoreSms()204         public boolean isStoreSms() {
205             return mStoreSms;
206         }
207 
208         /**
209          * Get whether test mode is enabled
210          */
211         @VisibleForTesting
isTestMode()212         public boolean isTestMode() {
213             return mTestMode;
214         }
215 
216         /**
217          * Get whether alert message support text to speech
218          */
219         @VisibleForTesting
isEnableAlertSpeech()220         public boolean isEnableAlertSpeech() {
221             return mEnableAlertSpeech;
222         }
223 
224         /**
225          * Get whether test mode is not supporting in user build status
226          */
227         @VisibleForTesting
isTestModeOnUserBuild()228         public boolean isTestModeOnUserBuild() {
229             return mTestModeOnUserBuild;
230         }
231 
232         /**
233          * Set alert during call
234          *
235          * @param current : current status of alert during call
236          */
237         @VisibleForTesting
onChangedAlertDuringCall(boolean current)238         public void onChangedAlertDuringCall(boolean current) {
239             mAlertDuringCall = current;
240         }
241 
242         /**
243          * Set alert during call
244          *
245          * @param channelManager : channel manager to get channel range supporting override dnd
246          * @param overAllDnD     : whether override dnd is fully supported or not
247          */
248         @VisibleForTesting
onChangedOverrideDnD( CellBroadcastChannelManager channelManager, boolean overAllDnD)249         public void onChangedOverrideDnD(
250                 CellBroadcastChannelManager channelManager, boolean overAllDnD) {
251             mDnDChannelSet.clear();
252             if (overAllDnD) {
253                 mDnDChannelSet.add(new Pair(Integer.MAX_VALUE, Integer.MAX_VALUE));
254             } else {
255                 channelManager.getAllCellBroadcastChannelRanges().forEach(r -> {
256                     if (r.mOverrideDnd) {
257                         mDnDChannelSet.add(new Pair(r.mStartId, r.mEndId));
258                     }
259                 });
260                 if (mDnDChannelSet.size() == 0) {
261                     mDnDChannelSet.add(new Pair(0, 0));
262                 }
263             }
264         }
265 
266         /**
267          * Set roaming support
268          *
269          * @param current : current status of roaming support
270          */
271         @VisibleForTesting
onChangedRoamingSupport(boolean current)272         public void onChangedRoamingSupport(boolean current) {
273             mRoamingSupport = current;
274         }
275 
276         /**
277          * Set current status of storing alert message inbox
278          *
279          * @param current : current status value of storing inbox
280          */
281         @VisibleForTesting
onChangedStoreSms(boolean current)282         public void onChangedStoreSms(boolean current) {
283             mStoreSms = current;
284         }
285 
286         /**
287          * Set current status of test-mode
288          *
289          * @param current : current status value of test-mode
290          */
291         @VisibleForTesting
onChangedTestMode(boolean current)292         public void onChangedTestMode(boolean current) {
293             mTestMode = current;
294         }
295 
296         /**
297          * Set whether text to speech is supported for alert message
298          *
299          * @param current : current status tts
300          */
301         @VisibleForTesting
onChangedEnableAlertSpeech(boolean current)302         public void onChangedEnableAlertSpeech(boolean current) {
303             mEnableAlertSpeech = current;
304         }
305 
306         /**
307          * Set whether test mode on user build is supported
308          *
309          * @param current : current status of test mode on user build
310          */
onChangedTestModeOnUserBuild(boolean current)311         public void onChangedTestModeOnUserBuild(boolean current) {
312             mTestModeOnUserBuild = current;
313         }
314 
315         /**
316          * Calling check-in method for CB_SERVICE_FEATURE
317          */
318         @VisibleForTesting
logFeatureChanged()319         public void logFeatureChanged() {
320             try {
321                 CellBroadcastModuleStatsLog.write(
322                         CellBroadcastModuleStatsLog.CB_RECEIVER_FEATURE_CHANGED,
323                         mAlertDuringCall,
324                         convertToProtoBuffer(mDnDChannelSet),
325                         mRoamingSupport,
326                         mStoreSms,
327                         mTestMode,
328                         mEnableAlertSpeech,
329                         mTestModeOnUserBuild);
330             } catch (IOException e) {
331                 Log.e(TAG, "IOException while encoding array byte from channel set" + e);
332             }
333             if (VDBG) Log.d(TAG, this.toString());
334         }
335 
336         /**
337          * Update preferences for receiver feature metrics
338          */
updateSharedPreferences()339         public void updateSharedPreferences() {
340             SharedPreferences sp =
341                     mContext.getSharedPreferences(CBR_METRIC_PREF, MODE_PRIVATE);
342             SharedPreferences.Editor editor = sp.edit();
343             editor.putBoolean(ALERT_IN_CALL, mAlertDuringCall);
344             editor.putString(OVERRIDE_DND, getStringFromChannelSet(mDnDChannelSet));
345             editor.putBoolean(ROAMING_SUPPORT, mRoamingSupport);
346             editor.putBoolean(STORE_SMS, mStoreSms);
347             editor.putBoolean(TEST_MODE, mTestMode);
348             editor.putBoolean(TTS_MODE, mEnableAlertSpeech);
349             editor.putBoolean(TEST_MODE_ON_USER_BUILD, mTestModeOnUserBuild);
350             editor.apply();
351         }
352 
353         @Override
toString()354         public String toString() {
355             return "CellBroadcast_Receiver_Feature : "
356                     + "mAlertDuringCall = " + mAlertDuringCall + " | "
357                     + "mOverrideDnD = " + getStringFromChannelSet(mDnDChannelSet) + " | "
358                     + "mRoamingSupport = " + mRoamingSupport + " | "
359                     + "mStoreSms = " + mStoreSms + " | "
360                     + "mTestMode = " + mTestMode + " | "
361                     + "mEnableAlertSpeech = " + mEnableAlertSpeech + " | "
362                     + "mTestModeOnUserBuild = " + mTestModeOnUserBuild;
363         }
364     }
365 
366     /**
367      * Get current feature metrics
368      *
369      * @param context : Context
370      */
371     @VisibleForTesting
getFeatureMetrics(Context context)372     public FeatureMetrics getFeatureMetrics(Context context) {
373         if (mFeatureMetrics == null) {
374             mFeatureMetrics = new FeatureMetrics(context);
375             mFeatureMetricsSharedPreferences = new FeatureMetrics(context);
376         }
377         return mFeatureMetrics;
378     }
379 
380 
381     /**
382      * Convert ChannelSet to ProtoBuffer
383      *
384      * @param rangeList : channel range set
385      */
386     @VisibleForTesting
convertToProtoBuffer(HashSet<Pair<Integer, Integer>> rangeList)387     public byte[] convertToProtoBuffer(HashSet<Pair<Integer, Integer>> rangeList)
388             throws IOException {
389         Cellbroadcastmetric.CellBroadcastChannelRangesProto.Builder rangeListBuilder =
390                 Cellbroadcastmetric.CellBroadcastChannelRangesProto.newBuilder();
391         rangeList.stream().sorted((o1, o2) -> Objects.equals(o1.first, o2.first)
392                 ? 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