[TelephonyAnalytics_Implementation] Changes related to Call AnalyticsProvider
- This changeset uses data from CallAnalytics and implements
functionalities related to aggregation of data.
- Applies requisite business logic and passes
data to util class for appropriate db operation.
Bug: 290965632
Test: Device Test & atest
Change-Id: I916baba50590dbc8fabd3a56e2ab28effd3f3a21
diff --git a/src/java/com/android/internal/telephony/analytics/CallAnalyticsProvider.java b/src/java/com/android/internal/telephony/analytics/CallAnalyticsProvider.java
new file mode 100644
index 0000000..e0da0f1
--- /dev/null
+++ b/src/java/com/android/internal/telephony/analytics/CallAnalyticsProvider.java
@@ -0,0 +1,663 @@
+/*
+ * Copyright (C) 2023 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.internal.telephony.analytics;
+
+import static android.os.Build.VERSION.INCREMENTAL;
+
+import static com.android.internal.telephony.analytics.TelephonyAnalyticsDatabase.DATE_FORMAT;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.analytics.TelephonyAnalyticsDatabase.CallAnalyticsTable;
+import com.android.telephony.Rlog;
+
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashMap;
+
+/**
+ * Provider class for calls, receives the data from CallAnalytics and performs business logic on the
+ * received data. It uses util class for required db operation. This class implements the
+ * TelephonyAnalyticsProvider interface to provide aggregation functionality.
+ */
+public class CallAnalyticsProvider implements TelephonyAnalyticsProvider {
+ private static final String TAG = CallAnalyticsProvider.class.getSimpleName();
+ private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.00");
+ private TelephonyAnalyticsUtil mTelephonyAnalyticsUtil;
+ private String mDateOfDeletedRecordsCallTable;
+ private static final String CREATE_CALL_ANALYTICS_TABLE =
+ "CREATE TABLE IF NOT EXISTS "
+ + CallAnalyticsTable.TABLE_NAME
+ + "("
+ + CallAnalyticsTable._ID
+ + " INTEGER PRIMARY KEY,"
+ + CallAnalyticsTable.LOG_DATE
+ + " DATE ,"
+ + CallAnalyticsTable.CALL_STATUS
+ + " TEXT DEFAULT '',"
+ + CallAnalyticsTable.CALL_TYPE
+ + " TEXT DEFAULT '',"
+ + CallAnalyticsTable.RAT
+ + " TEXT DEFAULT '',"
+ + CallAnalyticsTable.SLOT_ID
+ + " INTEGER ,"
+ + CallAnalyticsTable.FAILURE_REASON
+ + " TEXT DEFAULT '',"
+ + CallAnalyticsTable.RELEASE_VERSION
+ + " TEXT DEFAULT '' , "
+ + CallAnalyticsTable.COUNT
+ + " INTEGER DEFAULT 1 "
+ + ");";
+
+ private static final String[] CALL_INSERTION_PROJECTION = {
+ CallAnalyticsTable._ID, CallAnalyticsTable.COUNT
+ };
+
+ private static final String CALL_SUCCESS_INSERTION_SELECTION =
+ CallAnalyticsTable.CALL_TYPE
+ + " = ? AND "
+ + CallAnalyticsTable.LOG_DATE
+ + " = ? AND "
+ + CallAnalyticsTable.CALL_STATUS
+ + " = ? AND "
+ + CallAnalyticsTable.SLOT_ID
+ + " = ? ";
+
+ private static final String CALL_FAILED_INSERTION_SELECTION =
+ CallAnalyticsTable.LOG_DATE
+ + " = ? AND "
+ + CallAnalyticsTable.CALL_STATUS
+ + " = ? AND "
+ + CallAnalyticsTable.CALL_TYPE
+ + " = ? AND "
+ + CallAnalyticsTable.SLOT_ID
+ + " = ? AND "
+ + CallAnalyticsTable.RAT
+ + " = ? AND "
+ + CallAnalyticsTable.FAILURE_REASON
+ + " = ? AND "
+ + CallAnalyticsTable.RELEASE_VERSION
+ + " = ? ";
+
+ private static final String CALL_OLD_DATA_DELETION_SELECTION =
+ CallAnalyticsTable.LOG_DATE + " < ? ";
+
+ private static final String CALL_OVERFLOW_DATA_DELETION_SELECTION =
+ CallAnalyticsTable._ID
+ + " IN "
+ + " ( SELECT "
+ + CallAnalyticsTable._ID
+ + " FROM "
+ + CallAnalyticsTable.TABLE_NAME
+ + " ORDER BY "
+ + CallAnalyticsTable.LOG_DATE
+ + " DESC LIMIT -1 OFFSET ? )";
+
+ private enum CallStatus {
+ SUCCESS("Success"),
+ FAILURE("Failure");
+ public String value;
+
+ CallStatus(String value) {
+ this.value = value;
+ }
+ }
+
+ private enum CallType {
+ NORMAL("Normal Call"),
+ SOS("SOS Call");
+ public String value;
+
+ CallType(String value) {
+ this.value = value;
+ }
+ }
+
+ private final int mSlotIndex;
+
+ /**
+ * Initializes the CallAnalyticsProvider object and creates a table in the DB to log the
+ * information related to Calls.
+ *
+ * @param telephonyAnalyticsUtil : Util Class object to support db operations
+ * @param slotIndex : Logical slot index.
+ */
+ public CallAnalyticsProvider(TelephonyAnalyticsUtil telephonyAnalyticsUtil, int slotIndex) {
+ mTelephonyAnalyticsUtil = telephonyAnalyticsUtil;
+ mSlotIndex = slotIndex;
+ mTelephonyAnalyticsUtil.createTable(CREATE_CALL_ANALYTICS_TABLE);
+ }
+
+ private ContentValues getContentValues(
+ String callType, String callStatus, int slotId, String rat, String failureReason) {
+ ContentValues values = new ContentValues();
+ String dateToday = DATE_FORMAT.format(Calendar.getInstance().toInstant());
+ values.put(CallAnalyticsTable.LOG_DATE, dateToday);
+ values.put(CallAnalyticsTable.CALL_TYPE, callType);
+ values.put(CallAnalyticsTable.CALL_STATUS, callStatus);
+ values.put(CallAnalyticsTable.SLOT_ID, slotId);
+ values.put(CallAnalyticsTable.RAT, rat);
+ values.put(CallAnalyticsTable.FAILURE_REASON, failureReason);
+ values.put(CallAnalyticsTable.RELEASE_VERSION, INCREMENTAL);
+ return values;
+ }
+
+ private String[] getSuccessfulCallSelectionArgs(ContentValues values) {
+ return new String[] {
+ values.getAsString(CallAnalyticsTable.CALL_TYPE),
+ values.getAsString(CallAnalyticsTable.LOG_DATE),
+ CallStatus.SUCCESS.value,
+ values.getAsString(CallAnalyticsTable.SLOT_ID)
+ };
+ }
+
+ private String[] getFailedCallSelectionArgs(ContentValues values) {
+
+ return new String[] {
+ values.getAsString(CallAnalyticsTable.LOG_DATE),
+ values.getAsString(CallAnalyticsTable.CALL_STATUS),
+ values.getAsString(CallAnalyticsTable.CALL_TYPE),
+ values.getAsString(CallAnalyticsTable.SLOT_ID),
+ values.getAsString(CallAnalyticsTable.RAT),
+ values.getAsString(CallAnalyticsTable.FAILURE_REASON),
+ values.getAsString(CallAnalyticsTable.RELEASE_VERSION)
+ };
+ }
+
+ /**
+ * Receives data, processes it and sends for insertion to db.
+ *
+ * @param callType : Type of the Call , i.e. Normal or Sos
+ * @param callStatus : Defines call was success or failure
+ * @param slotId : Logical Slot index derived from Phone object.
+ * @param rat : Radio Access Technology on which call ended.
+ * @param failureReason : Failure Reason of the call.
+ */
+ public void insertDataToDb(
+ String callType, String callStatus, int slotId, String rat, String failureReason) {
+ ContentValues values = getContentValues(callType, callStatus, slotId, rat, failureReason);
+ Cursor cursor = null;
+ try {
+ if (values.getAsString(CallAnalyticsTable.CALL_STATUS)
+ .equals(CallStatus.SUCCESS.value)) {
+ Rlog.d(TAG, "Insertion for Success Call");
+ String[] selectionArgs = getSuccessfulCallSelectionArgs(values);
+ cursor =
+ mTelephonyAnalyticsUtil.getCursor(
+ CallAnalyticsTable.TABLE_NAME,
+ CALL_INSERTION_PROJECTION,
+ CALL_SUCCESS_INSERTION_SELECTION,
+ selectionArgs,
+ null,
+ null,
+ null,
+ null);
+ } else {
+
+ String[] selectionArgs = getFailedCallSelectionArgs(values);
+ cursor =
+ mTelephonyAnalyticsUtil.getCursor(
+ CallAnalyticsTable.TABLE_NAME,
+ CALL_INSERTION_PROJECTION,
+ CALL_FAILED_INSERTION_SELECTION,
+ selectionArgs,
+ null,
+ null,
+ null,
+ null);
+ }
+ updateEntryIfExistsOrInsert(cursor, values);
+ deleteOldAndOverflowData();
+ } catch (Exception e) {
+ Rlog.e(TAG, "Error caught in insertDataToDb while insertion.");
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ private void updateEntryIfExistsOrInsert(Cursor cursor, ContentValues values) {
+ if (cursor != null && cursor.moveToFirst()) {
+ int idColumnIndex = cursor.getColumnIndex(CallAnalyticsTable._ID);
+ int countColumnIndex = cursor.getColumnIndex(CallAnalyticsTable.COUNT);
+ if (idColumnIndex != -1 && countColumnIndex != -1) {
+ int id = cursor.getInt(idColumnIndex);
+ int count = cursor.getInt(countColumnIndex);
+ int newCount = count + 1;
+
+ values.put(CallAnalyticsTable.COUNT, newCount);
+
+ String updateSelection = CallAnalyticsTable._ID + " = ? ";
+ String[] updateSelectionArgs = {String.valueOf(id)};
+ Rlog.d(TAG, "Updated Count = " + values.getAsString(CallAnalyticsTable.COUNT));
+
+ mTelephonyAnalyticsUtil.update(
+ CallAnalyticsTable.TABLE_NAME,
+ values,
+ updateSelection,
+ updateSelectionArgs);
+ }
+ } else {
+ Rlog.d(TAG, "Simple Insertion");
+ mTelephonyAnalyticsUtil.insert(CallAnalyticsTable.TABLE_NAME, values);
+ }
+ }
+
+ /** Gets the count stored in the cursor object. */
+ @VisibleForTesting
+ public long getCount(
+ String tableName,
+ String[] columns,
+ String selection,
+ String[] selectionArgs,
+ String groupBy,
+ String having,
+ String orderBy,
+ String limit) {
+ Cursor cursor = null;
+ long totalCount = 0;
+ try {
+ cursor =
+ mTelephonyAnalyticsUtil.getCursor(
+ CallAnalyticsTable.TABLE_NAME,
+ columns,
+ selection,
+ selectionArgs,
+ groupBy,
+ having,
+ orderBy,
+ limit);
+ totalCount = mTelephonyAnalyticsUtil.getCountFromCursor(cursor);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return totalCount;
+ }
+
+ private String[] getColumnSelectionAndArgs(String callType, String callStatus) {
+ String selection;
+ String[] selectionAndArgs;
+ if (callType != null) {
+ if (callStatus != null) {
+ selection =
+ CallAnalyticsTable.CALL_TYPE
+ + " = ? AND "
+ + CallAnalyticsTable.CALL_STATUS
+ + " = ? AND "
+ + CallAnalyticsTable.SLOT_ID
+ + " = ? ";
+ selectionAndArgs =
+ new String[] {
+ selection, callType, callStatus, Integer.toString(mSlotIndex)
+ };
+ } else {
+ selection =
+ CallAnalyticsTable.CALL_TYPE
+ + " = ? AND "
+ + CallAnalyticsTable.SLOT_ID
+ + " = ? ";
+ selectionAndArgs = new String[] {selection, callType, Integer.toString(mSlotIndex)};
+ }
+ } else {
+ if (callStatus != null) {
+ selection =
+ CallAnalyticsTable.CALL_STATUS
+ + " = ? AND "
+ + CallAnalyticsTable.SLOT_ID
+ + " = ? ";
+ selectionAndArgs =
+ new String[] {selection, callStatus, Integer.toString(mSlotIndex)};
+ } else {
+ selection = CallAnalyticsTable.SLOT_ID + " = ? ";
+ selectionAndArgs = new String[] {selection, Integer.toString(mSlotIndex)};
+ }
+ }
+ return selectionAndArgs;
+ }
+
+ private long countCallsOfTypeAndStatus(String callType, String callStatus) {
+ int selectionIndex = 0;
+ String[] columns = {"sum(" + CallAnalyticsTable.COUNT + ")"};
+ String[] selectionAndArgs = getColumnSelectionAndArgs(callType, callStatus);
+ String selection = selectionAndArgs[selectionIndex];
+ int selectionArgsStartIndex = 1, selectionArgsEndIndex = selectionAndArgs.length;
+ String[] selectionArgs =
+ Arrays.copyOfRange(
+ selectionAndArgs, selectionArgsStartIndex, selectionArgsEndIndex);
+ return getCount(
+ CallAnalyticsTable.TABLE_NAME,
+ columns,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null,
+ null);
+ }
+
+ private long countTotalCalls() {
+ return countCallsOfTypeAndStatus(null, null);
+ }
+
+ private long countFailedCalls() {
+ return countCallsOfTypeAndStatus(null, CallStatus.FAILURE.value);
+ }
+
+ private long countNormalCalls() {
+ return countCallsOfTypeAndStatus(CallType.NORMAL.value, null);
+ }
+
+ private long countFailedNormalCalls() {
+ return countCallsOfTypeAndStatus(CallType.NORMAL.value, CallStatus.FAILURE.value);
+ }
+
+ private long countSosCalls() {
+ return countCallsOfTypeAndStatus(CallType.SOS.value, null);
+ }
+
+ private long countFailedSosCalls() {
+ return countCallsOfTypeAndStatus(CallType.SOS.value, CallStatus.FAILURE.value);
+ }
+
+ private String getMaxFailureVersion() {
+ String[] columns = {CallAnalyticsTable.RELEASE_VERSION};
+ String selection =
+ CallAnalyticsTable.CALL_STATUS + " = ? AND " + CallAnalyticsTable.SLOT_ID + " = ? ";
+ String[] selectionArgs = {CallStatus.FAILURE.value, Integer.toString(mSlotIndex)};
+ String groupBy = CallAnalyticsTable.RELEASE_VERSION;
+ String orderBy = "SUM(" + CallAnalyticsTable.COUNT + ") DESC ";
+ String limit = "1";
+ Cursor cursor = null;
+ String version = "";
+ try {
+ cursor =
+ mTelephonyAnalyticsUtil.getCursor(
+ CallAnalyticsTable.TABLE_NAME,
+ columns,
+ selection,
+ selectionArgs,
+ groupBy,
+ null,
+ orderBy,
+ limit);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ int releaseVersionColumnIndex =
+ cursor.getColumnIndex(CallAnalyticsTable.RELEASE_VERSION);
+ if (releaseVersionColumnIndex != -1) {
+ version = cursor.getString(releaseVersionColumnIndex);
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return version;
+ }
+
+ private String getMaxFailureNetworkType() {
+ String[] columns = {CallAnalyticsTable.RAT};
+ String selection =
+ CallAnalyticsTable.CALL_STATUS + " = ? AND " + CallAnalyticsTable.SLOT_ID + " = ? ";
+ String[] selectionArgs = {CallStatus.FAILURE.value, Integer.toString(mSlotIndex)};
+ String groupBy = CallAnalyticsTable.RAT;
+ String orderBy = "SUM(" + CallAnalyticsTable.COUNT + ") DESC ";
+ String limit = "1";
+ Cursor cursor = null;
+ String networkType = "";
+
+ try {
+ cursor =
+ mTelephonyAnalyticsUtil.getCursor(
+ CallAnalyticsTable.TABLE_NAME,
+ columns,
+ selection,
+ selectionArgs,
+ groupBy,
+ null,
+ orderBy,
+ limit);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ int networkColumnIndex = cursor.getColumnIndex(CallAnalyticsTable.RAT);
+ networkType = cursor.getString(networkColumnIndex);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return networkType;
+ }
+
+ private HashMap<String, Integer> getFailureCountByRatForCallType(String callType) {
+ return getFailureCountByColumnForCallType(CallAnalyticsTable.RAT, callType);
+ }
+
+ private HashMap<String, Integer> getFailureCountByReasonForCallType(String callType) {
+ return getFailureCountByColumnForCallType(CallAnalyticsTable.FAILURE_REASON, callType);
+ }
+
+ private HashMap<String, Integer> getFailureCountByColumnForCallType(
+ String column, String callType) {
+ String[] columns = {column, "SUM(" + CallAnalyticsTable.COUNT + ") AS count"};
+ String selection =
+ CallAnalyticsTable.CALL_TYPE
+ + " = ? AND "
+ + CallAnalyticsTable.CALL_STATUS
+ + " = ? AND "
+ + CallAnalyticsTable.SLOT_ID
+ + " = ? ";
+ String[] selectionArgs = {callType, CallStatus.FAILURE.value, Integer.toString(mSlotIndex)};
+ String groupBy = column;
+ Cursor cursor = null;
+ HashMap<String, Integer> failureCountByReason = new HashMap<>();
+
+ try {
+ cursor =
+ mTelephonyAnalyticsUtil.getCursor(
+ CallAnalyticsTable.TABLE_NAME,
+ columns,
+ selection,
+ selectionArgs,
+ groupBy,
+ null,
+ null,
+ null);
+
+ if (cursor != null) {
+ int failureColumnIndex = cursor.getColumnIndex(column);
+ int failureCountColumnIndex = cursor.getColumnIndex("count");
+
+ if (failureCountColumnIndex != -1 && failureColumnIndex != -1) {
+ while (cursor.moveToNext()) {
+ String failureReason = cursor.getString(failureColumnIndex);
+ int failureCount = cursor.getInt(failureCountColumnIndex);
+ failureCountByReason.put(failureReason, failureCount);
+ }
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return failureCountByReason;
+ }
+
+ protected void deleteOldAndOverflowData() {
+ String dateToday = DATE_FORMAT.format(Calendar.getInstance().toInstant());
+ if (mDateOfDeletedRecordsCallTable == null
+ || !mDateOfDeletedRecordsCallTable.equals(dateToday)) {
+ mTelephonyAnalyticsUtil.deleteOverflowAndOldData(
+ CallAnalyticsTable.TABLE_NAME,
+ CALL_OVERFLOW_DATA_DELETION_SELECTION,
+ CALL_OLD_DATA_DELETION_SELECTION);
+ mDateOfDeletedRecordsCallTable = dateToday;
+ }
+ }
+
+ /** Setter function for mDateOfDeletedRecordsCallTable. */
+ public void setDateOfDeletedRecordsCallTable(String dateOfDeletedRecordsCallTable) {
+ mDateOfDeletedRecordsCallTable = dateOfDeletedRecordsCallTable;
+ }
+
+ private ArrayList<String> dumpInformationInList(
+ long totalCalls,
+ long failedCalls,
+ double percentageFailedCalls,
+ long normalCallsCount,
+ double percentageFailedNormalCalls,
+ long sosCallCount,
+ double percentageFailedSosCalls,
+ String maxFailureVersion,
+ String maxFailureNetworkType,
+ HashMap<String, Integer> failureCountByReasonNormalCall,
+ HashMap<String, Integer> failureCountByReasonSosCall,
+ HashMap<String, Integer> failureCountByRatNormalCall,
+ HashMap<String, Integer> failureCountByRatSosCall) {
+
+ ArrayList<String> aggregatedCallInformation = new ArrayList<>();
+ aggregatedCallInformation.add("Normal Call Stats");
+ aggregatedCallInformation.add("\tTotal Normal Calls = " + normalCallsCount);
+ aggregatedCallInformation.add(
+ "\tPercentage Failure of Normal Calls = "
+ + DECIMAL_FORMAT.format(percentageFailedNormalCalls)
+ + "%");
+ addFailureStatsFromHashMap(
+ failureCountByReasonNormalCall,
+ CallType.NORMAL.value,
+ normalCallsCount,
+ CallAnalyticsTable.FAILURE_REASON,
+ aggregatedCallInformation);
+ addFailureStatsFromHashMap(
+ failureCountByRatNormalCall,
+ CallType.NORMAL.value,
+ normalCallsCount,
+ CallAnalyticsTable.RAT,
+ aggregatedCallInformation);
+
+ if (sosCallCount > 0) {
+ aggregatedCallInformation.add("SOS Call Stats");
+ aggregatedCallInformation.add("\tTotal SOS Calls = " + sosCallCount);
+ aggregatedCallInformation.add(
+ "\tPercentage Failure of SOS Calls = "
+ + DECIMAL_FORMAT.format(percentageFailedSosCalls)
+ + "%");
+ addFailureStatsFromHashMap(
+ failureCountByReasonSosCall,
+ CallType.SOS.value,
+ sosCallCount,
+ CallAnalyticsTable.FAILURE_REASON,
+ aggregatedCallInformation);
+ addFailureStatsFromHashMap(
+ failureCountByRatSosCall,
+ CallType.SOS.value,
+ sosCallCount,
+ CallAnalyticsTable.RAT,
+ aggregatedCallInformation);
+ }
+ if (failedCalls != 0) {
+ aggregatedCallInformation.add(
+ "\tMax Call(Normal+SOS) Failures at Version : " + maxFailureVersion);
+ aggregatedCallInformation.add(
+ "\tMax Call(Normal+SOS) Failures at Network Type: " + maxFailureNetworkType);
+ }
+
+ return aggregatedCallInformation;
+ }
+
+ private void addFailureStatsFromHashMap(
+ HashMap<String, Integer> failureCountByColumn,
+ String callType,
+ long totalCallsOfCallType,
+ String column,
+ ArrayList<String> aggregatedCallInformation) {
+ failureCountByColumn.forEach(
+ (k, v) -> {
+ double percentageFail = (double) v / (double) totalCallsOfCallType * 100.0;
+ aggregatedCallInformation.add(
+ "\tNo. of "
+ + callType
+ + " failures at "
+ + column
+ + " : "
+ + k
+ + " = "
+ + v
+ + ", Percentage = "
+ + DECIMAL_FORMAT.format(percentageFail)
+ + "%");
+ });
+ }
+
+ /**
+ * Collects all information which is intended to be a part of the report by calling the required
+ * functions implemented in the class.
+ *
+ * @return List which contains all the Calls related information
+ */
+ public ArrayList<String> aggregate() {
+ long totalCalls = countTotalCalls();
+ long failedCalls = countFailedCalls();
+ double percentageFailedCalls = (double) failedCalls / (double) totalCalls * 100.0;
+ String maxFailuresVersion = getMaxFailureVersion();
+ String maxFailuresNetworkType = getMaxFailureNetworkType();
+ HashMap<String, Integer> failureCountByReasonNormalCall =
+ getFailureCountByReasonForCallType(CallType.NORMAL.value);
+ HashMap<String, Integer> failureCountByReasonSosCall =
+ getFailureCountByReasonForCallType(CallType.SOS.value);
+ HashMap<String, Integer> failureCountByRatNormalCall =
+ getFailureCountByRatForCallType(CallType.NORMAL.value);
+ HashMap<String, Integer> failureCountByRatSosCall =
+ getFailureCountByRatForCallType(CallType.SOS.value);
+ long normalCallsCount = countNormalCalls();
+ long normalCallFailureCount = countFailedNormalCalls();
+ double percentageFailureNormalCall =
+ (double) normalCallFailureCount / (double) normalCallsCount * 100.0;
+ long sosCallCount = countSosCalls();
+ long sosCallFailureCount = countFailedSosCalls();
+ double percentageFailureSosCall =
+ (double) sosCallFailureCount / (double) sosCallCount * 100.0;
+ ArrayList<String> aggregatedCallInformation =
+ dumpInformationInList(
+ totalCalls,
+ failedCalls,
+ percentageFailedCalls,
+ normalCallsCount,
+ percentageFailureNormalCall,
+ sosCallCount,
+ percentageFailureSosCall,
+ maxFailuresVersion,
+ maxFailuresNetworkType,
+ failureCountByReasonNormalCall,
+ failureCountByReasonSosCall,
+ failureCountByRatNormalCall,
+ failureCountByRatSosCall);
+ return aggregatedCallInformation;
+ }
+}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/analytics/CallAnalyticsProviderTest.java b/tests/telephonytests/src/com/android/internal/telephony/analytics/CallAnalyticsProviderTest.java
new file mode 100644
index 0000000..076ee06
--- /dev/null
+++ b/tests/telephonytests/src/com/android/internal/telephony/analytics/CallAnalyticsProviderTest.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2023 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.internal.telephony.analytics;
+
+import static android.os.Build.VERSION.INCREMENTAL;
+
+import static com.android.internal.telephony.analytics.TelephonyAnalyticsDatabase.CallAnalyticsTable;
+import static com.android.internal.telephony.analytics.TelephonyAnalyticsDatabase.DATE_FORMAT;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Calendar;
+
+public class CallAnalyticsProviderTest {
+
+ @Mock TelephonyAnalyticsUtil mTelephonyAnalyticsUtil;
+ @Mock Cursor mCursor;
+ private CallAnalyticsProvider mCallAnalyticsProvider;
+ private ContentValues mContentValues;
+
+ enum CallStatus {
+ SUCCESS("Success"),
+ FAILURE("Failure");
+ public String value;
+
+ CallStatus(String value) {
+ this.value = value;
+ }
+ }
+
+ enum CallType {
+ NORMAL("Normal Call"),
+ SOS("SOS Call");
+ public String value;
+
+ CallType(String value) {
+ this.value = value;
+ }
+ }
+
+ final String[] mCallInsertionProjection = {
+ TelephonyAnalyticsDatabase.CallAnalyticsTable._ID,
+ TelephonyAnalyticsDatabase.CallAnalyticsTable.COUNT
+ };
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ final String createCallAnalyticsTable =
+ "CREATE TABLE IF NOT EXISTS "
+ + TelephonyAnalyticsDatabase.CallAnalyticsTable.TABLE_NAME
+ + "("
+ + TelephonyAnalyticsDatabase.CallAnalyticsTable._ID
+ + " INTEGER PRIMARY KEY,"
+ + TelephonyAnalyticsDatabase.CallAnalyticsTable.LOG_DATE
+ + " DATE ,"
+ + TelephonyAnalyticsDatabase.CallAnalyticsTable.CALL_STATUS
+ + " TEXT DEFAULT '',"
+ + TelephonyAnalyticsDatabase.CallAnalyticsTable.CALL_TYPE
+ + " TEXT DEFAULT '',"
+ + TelephonyAnalyticsDatabase.CallAnalyticsTable.RAT
+ + " TEXT DEFAULT '',"
+ + TelephonyAnalyticsDatabase.CallAnalyticsTable.SLOT_ID
+ + " INTEGER ,"
+ + TelephonyAnalyticsDatabase.CallAnalyticsTable.FAILURE_REASON
+ + " TEXT DEFAULT '',"
+ + TelephonyAnalyticsDatabase.CallAnalyticsTable.RELEASE_VERSION
+ + " TEXT DEFAULT '' , "
+ + TelephonyAnalyticsDatabase.CallAnalyticsTable.COUNT
+ + " INTEGER DEFAULT 1 "
+ + ");";
+ mCallAnalyticsProvider = new CallAnalyticsProvider(mTelephonyAnalyticsUtil, 0);
+ verify(mTelephonyAnalyticsUtil).createTable(createCallAnalyticsTable);
+ }
+
+ @Test
+ public void testAggregate() {
+ String[] columns = {"sum(" + CallAnalyticsTable.COUNT + ")"};
+
+ when(mTelephonyAnalyticsUtil.getCursor(
+ eq(CallAnalyticsTable.TABLE_NAME),
+ any(String[].class),
+ anyString(),
+ any(String[].class),
+ isNull(),
+ isNull(),
+ isNull(),
+ isNull()))
+ .thenReturn(mCursor);
+ when(mTelephonyAnalyticsUtil.getCursor(
+ anyString(),
+ any(String[].class),
+ anyString(),
+ any(String[].class),
+ anyString(),
+ isNull(),
+ anyString(),
+ anyString()))
+ .thenReturn(mCursor);
+
+ when(mTelephonyAnalyticsUtil.getCountFromCursor(eq(mCursor)))
+ .thenReturn(
+ 100L /*totalCalls*/,
+ 50L /*totalFailedCalls*/,
+ 40L /*normalCalls*/,
+ 10L /*failedNormalCall*/,
+ 60L /*sosCalls*/,
+ 40L /*failedSosCall*/);
+ ArrayList<String> actual = mCallAnalyticsProvider.aggregate();
+ verify(mTelephonyAnalyticsUtil, times(6))
+ .getCursor(
+ eq(CallAnalyticsTable.TABLE_NAME),
+ any(String[].class),
+ anyString(),
+ any(String[].class),
+ isNull(),
+ isNull(),
+ isNull(),
+ isNull());
+ assertEquals("\tTotal Normal Calls = " + 40 /*normalCalls*/, actual.get(1));
+ assertEquals("\tPercentage Failure of Normal Calls = 25.00%", actual.get(2));
+ }
+
+ @Test
+ public void testGetMaxFailureVersion() {
+ String[] columns = {CallAnalyticsTable.RELEASE_VERSION};
+ String selection =
+ CallAnalyticsTable.CALL_STATUS + " = ? AND " + CallAnalyticsTable.SLOT_ID + " = ? ";
+ String[] selectionArgs = {"Failure", Integer.toString(0 /* slotIndex */)};
+ String groupBy = CallAnalyticsTable.RELEASE_VERSION;
+ String orderBy = "SUM(" + CallAnalyticsTable.COUNT + ") DESC ";
+ String limit = "1";
+ when(mTelephonyAnalyticsUtil.getCursor(
+ anyString(),
+ any(String[].class),
+ anyString(),
+ any(String[].class),
+ anyString(),
+ isNull(),
+ anyString(),
+ anyString()))
+ .thenReturn(mCursor);
+ when(mTelephonyAnalyticsUtil.getCountFromCursor(any(Cursor.class)))
+ .thenReturn(10L /* count */);
+ when(mTelephonyAnalyticsUtil.getCountFromCursor(isNull())).thenReturn(10L /* count */);
+ when(mCursor.moveToFirst()).thenReturn(true);
+ when(mCursor.getColumnIndex(CallAnalyticsTable.RELEASE_VERSION))
+ .thenReturn(0 /* releaseVersionColumnIndex */);
+ when(mCursor.getString(0)).thenReturn("1.1.1.1" /* version */);
+ ArrayList<String> actual = mCallAnalyticsProvider.aggregate();
+ verify(mTelephonyAnalyticsUtil)
+ .getCursor(
+ eq(CallAnalyticsTable.TABLE_NAME),
+ eq(columns),
+ eq(selection),
+ eq(selectionArgs),
+ eq(groupBy),
+ isNull(),
+ eq(orderBy),
+ eq(limit));
+ assertEquals(
+ actual.get(actual.size() - 2 /* array index for max failure at version info */),
+ "\tMax Call(Normal+SOS) Failures at Version : 1.1.1.1");
+ }
+
+ private ContentValues getContentValues(
+ String callType, String callStatus, int slotId, String rat, String failureReason) {
+ ContentValues values = new ContentValues();
+ String dateToday = DATE_FORMAT.format(Calendar.getInstance().toInstant());
+ values.put(CallAnalyticsTable.LOG_DATE, dateToday);
+ values.put(CallAnalyticsTable.CALL_TYPE, callType);
+ values.put(CallAnalyticsTable.CALL_STATUS, callStatus);
+ values.put(CallAnalyticsTable.SLOT_ID, slotId);
+ values.put(CallAnalyticsTable.RAT, rat);
+ values.put(CallAnalyticsTable.FAILURE_REASON, failureReason);
+ values.put(CallAnalyticsTable.RELEASE_VERSION, INCREMENTAL);
+ return values;
+ }
+
+ private void whenConditionForGetCursor() {
+ when(mTelephonyAnalyticsUtil.getCursor(
+ anyString(),
+ any(String[].class),
+ anyString(),
+ any(String[].class),
+ isNull(),
+ isNull(),
+ isNull(),
+ isNull()))
+ .thenReturn(mCursor);
+ }
+
+ private void verifyForGetCursor(
+ String[] callInsertionProjection,
+ String callSuccessInsertionSelection,
+ String[] selectionArgs) {
+
+ verify(mTelephonyAnalyticsUtil)
+ .getCursor(
+ eq(TelephonyAnalyticsDatabase.CallAnalyticsTable.TABLE_NAME),
+ eq(callInsertionProjection),
+ eq(callSuccessInsertionSelection),
+ eq(selectionArgs),
+ isNull(),
+ isNull(),
+ isNull(),
+ isNull());
+ }
+
+ @Test
+ public void testSuccessCall() {
+ int slotId = 0;
+ String callType = "Normal Call";
+ String callStatus = "Success";
+ String rat = "LTE";
+ String failureReason = "User Disconnects";
+ int count = 5;
+
+ final String callSuccessInsertionSelection =
+ TelephonyAnalyticsDatabase.CallAnalyticsTable.CALL_TYPE
+ + " = ? AND "
+ + TelephonyAnalyticsDatabase.CallAnalyticsTable.LOG_DATE
+ + " = ? AND "
+ + TelephonyAnalyticsDatabase.CallAnalyticsTable.CALL_STATUS
+ + " = ? AND "
+ + TelephonyAnalyticsDatabase.CallAnalyticsTable.SLOT_ID
+ + " = ? ";
+ ContentValues values = getContentValues(callType, callStatus, slotId, rat, failureReason);
+
+ String[] selectionArgs =
+ new String[] {
+ values.getAsString(TelephonyAnalyticsDatabase.CallAnalyticsTable.CALL_TYPE),
+ values.getAsString(TelephonyAnalyticsDatabase.CallAnalyticsTable.LOG_DATE),
+ callStatus,
+ values.getAsString(TelephonyAnalyticsDatabase.CallAnalyticsTable.SLOT_ID)
+ };
+ whenConditionForGetCursor();
+ mCallAnalyticsProvider.insertDataToDb(callType, callStatus, slotId, rat, failureReason);
+ verifyForGetCursor(mCallInsertionProjection, callSuccessInsertionSelection, selectionArgs);
+ }
+
+ @Test
+ public void testFailureCall() {
+ int slotId = 0;
+ String callType = "Normal Call";
+ String callStatus = "Failure";
+ String rat = "LTE";
+ String failureReason = "Network Detach";
+ int count = 5;
+ final String callFailedInsertionSelection =
+ CallAnalyticsTable.LOG_DATE
+ + " = ? AND "
+ + CallAnalyticsTable.CALL_STATUS
+ + " = ? AND "
+ + CallAnalyticsTable.CALL_TYPE
+ + " = ? AND "
+ + CallAnalyticsTable.SLOT_ID
+ + " = ? AND "
+ + CallAnalyticsTable.RAT
+ + " = ? AND "
+ + CallAnalyticsTable.FAILURE_REASON
+ + " = ? AND "
+ + CallAnalyticsTable.RELEASE_VERSION
+ + " = ? ";
+ ContentValues values = getContentValues(callType, callStatus, slotId, rat, failureReason);
+ String[] selectionArgs = {
+ values.getAsString(CallAnalyticsTable.LOG_DATE),
+ values.getAsString(CallAnalyticsTable.CALL_STATUS),
+ values.getAsString(CallAnalyticsTable.CALL_TYPE),
+ values.getAsString(CallAnalyticsTable.SLOT_ID),
+ values.getAsString(CallAnalyticsTable.RAT),
+ values.getAsString(CallAnalyticsTable.FAILURE_REASON),
+ values.getAsString(CallAnalyticsTable.RELEASE_VERSION)
+ };
+ whenConditionForGetCursor();
+ mCallAnalyticsProvider.insertDataToDb(callType, callStatus, slotId, rat, failureReason);
+ verifyForGetCursor(mCallInsertionProjection, callFailedInsertionSelection, selectionArgs);
+ }
+
+ public void setUpTestForUpdateEntryIfExistsOrInsert() throws NoSuchMethodException {
+ Method updateEntryIfExistsOrInsert =
+ CallAnalyticsProvider.class.getDeclaredMethod(
+ "updateEntryIfExistsOrInsert", Cursor.class, ContentValues.class);
+ updateEntryIfExistsOrInsert.setAccessible(true);
+ mContentValues = new ContentValues();
+ mContentValues.put(CallAnalyticsTable.CALL_STATUS, "Success");
+ }
+
+ @Test
+ public void testUpdateEntryIfExistsOrInsertWhenCursorNull()
+ throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ Method updateEntryIfExistsOrInsert =
+ CallAnalyticsProvider.class.getDeclaredMethod(
+ "updateEntryIfExistsOrInsert", Cursor.class, ContentValues.class);
+ updateEntryIfExistsOrInsert.setAccessible(true);
+ ContentValues values = new ContentValues();
+ values.put(CallAnalyticsTable.CALL_STATUS, "Success");
+ updateEntryIfExistsOrInsert.invoke(mCallAnalyticsProvider, null, values);
+ verify(mTelephonyAnalyticsUtil).insert(eq(CallAnalyticsTable.TABLE_NAME), eq(values));
+ }
+
+ @Test
+ public void testUpdateEntryIfExistsOrInsertWhenCursorInvalid()
+ throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ Method updateEntryIfExistsOrInsert =
+ CallAnalyticsProvider.class.getDeclaredMethod(
+ "updateEntryIfExistsOrInsert", Cursor.class, ContentValues.class);
+ updateEntryIfExistsOrInsert.setAccessible(true);
+ ContentValues values = new ContentValues();
+ values.put(CallAnalyticsTable.CALL_STATUS, "Success");
+ when(mCursor.moveToFirst()).thenReturn(false);
+ updateEntryIfExistsOrInsert.invoke(mCallAnalyticsProvider, mCursor, values);
+ verify(mTelephonyAnalyticsUtil).insert(eq(CallAnalyticsTable.TABLE_NAME), eq(values));
+ }
+
+ @Test
+ public void testUpdateEntryIfExistsOrInsertWhenCursorValidUpdateSuccess()
+ throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ Method updateEntryIfExistsOrInsert =
+ CallAnalyticsProvider.class.getDeclaredMethod(
+ "updateEntryIfExistsOrInsert", Cursor.class, ContentValues.class);
+ updateEntryIfExistsOrInsert.setAccessible(true);
+
+ ContentValues values = new ContentValues();
+ values.put(CallAnalyticsTable.CALL_STATUS, "Success");
+
+ when(mCursor.moveToFirst()).thenReturn(true);
+ when(mCursor.getColumnIndex(CallAnalyticsTable._ID)).thenReturn(0 /* idColumnIndex */);
+ when(mCursor.getColumnIndex(CallAnalyticsTable.COUNT)).thenReturn(1 /* countColumnIndex */);
+ when(mCursor.getInt(0 /* idColumnIndex */)).thenReturn(100 /* id */);
+ when(mCursor.getInt(1 /* countColumnIndex */)).thenReturn(5 /* count*/);
+
+ String updateSelection = CallAnalyticsTable._ID + " = ? ";
+ String[] updateSelectionArgs = {String.valueOf(100 /* id */)};
+
+ updateEntryIfExistsOrInsert.invoke(mCallAnalyticsProvider, mCursor, values);
+
+ values.put(CallAnalyticsTable.COUNT, 6 /* newCount */);
+ verify(mTelephonyAnalyticsUtil)
+ .update(
+ eq(CallAnalyticsTable.TABLE_NAME),
+ eq(values),
+ eq(updateSelection),
+ eq(updateSelectionArgs));
+ }
+
+ @Test
+ public void testUpdateEntryIfExistsOrInsertWhenUpdateFailedDueToInvalidIdIndex()
+ throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ Method updateEntryIfExistsOrInsert =
+ CallAnalyticsProvider.class.getDeclaredMethod(
+ "updateEntryIfExistsOrInsert", Cursor.class, ContentValues.class);
+ updateEntryIfExistsOrInsert.setAccessible(true);
+
+ ContentValues values = new ContentValues();
+ values.put(CallAnalyticsTable.CALL_STATUS, "Success");
+
+ when(mCursor.moveToFirst()).thenReturn(true);
+ when(mCursor.getColumnIndex(CallAnalyticsTable._ID)).thenReturn(-1 /* idColumnIndex */);
+ when(mCursor.getColumnIndex(CallAnalyticsTable.COUNT)).thenReturn(1 /* countColumnIndex */);
+ updateEntryIfExistsOrInsert.invoke(mCallAnalyticsProvider, mCursor, values);
+ verifyNoMoreInteractions(mTelephonyAnalyticsUtil);
+ }
+
+ @Test
+ public void testUpdateEntryIfExistsOrInsertWhenUpdateFailedDueToInvalidCountIndex()
+ throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ Method updateEntryIfExistsOrInsert =
+ CallAnalyticsProvider.class.getDeclaredMethod(
+ "updateEntryIfExistsOrInsert", Cursor.class, ContentValues.class);
+ updateEntryIfExistsOrInsert.setAccessible(true);
+
+ ContentValues values = new ContentValues();
+ values.put(CallAnalyticsTable.CALL_STATUS, "Success");
+
+ when(mCursor.moveToFirst()).thenReturn(true);
+ when(mCursor.getColumnIndex(CallAnalyticsTable._ID)).thenReturn(0 /* idColumnIndex */);
+ when(mCursor.getColumnIndex(CallAnalyticsTable.COUNT))
+ .thenReturn(-1 /* countColumnIndex */);
+ updateEntryIfExistsOrInsert.invoke(mCallAnalyticsProvider, mCursor, values);
+ verifyNoMoreInteractions(mTelephonyAnalyticsUtil);
+ }
+
+ @Test
+ public void testUpdateEntryIfExistsOrInsertWhenUpdateFailedDueToInvalidColumnIndex()
+ throws NoSuchMethodException {
+ Method updateEntryIfExistsOrInsert =
+ CallAnalyticsProvider.class.getDeclaredMethod(
+ "updateEntryIfExistsOrInsert", Cursor.class, ContentValues.class);
+ updateEntryIfExistsOrInsert.setAccessible(true);
+
+ ContentValues values = new ContentValues();
+ values.put(CallAnalyticsTable.CALL_STATUS, "Success");
+
+ when(mCursor.moveToFirst()).thenReturn(true);
+ when(mCursor.getColumnIndex(CallAnalyticsTable._ID)).thenReturn(-1 /* idColumnIndex */);
+ when(mCursor.getColumnIndex(CallAnalyticsTable.COUNT))
+ .thenReturn(-1 /* countColumnIndex */);
+ verifyNoMoreInteractions(mTelephonyAnalyticsUtil);
+ }
+
+ @After
+ public void tearDown() {
+ mCallAnalyticsProvider = null;
+ mContentValues = null;
+ mTelephonyAnalyticsUtil = null;
+ mCursor = null;
+ }
+}