blob: 3b84053b2fc8ac3df80978cb01d45575591a5103 [file] [log] [blame]
/*
* 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.settingslib.bluetooth;
import android.content.Context;
import android.content.SharedPreferences;
import android.icu.text.SimpleDateFormat;
import android.icu.util.TimeZone;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/** Utils class to report hearing aid metrics to statsd */
public final class HearingAidStatsLogUtils {
private static final String TAG = "HearingAidStatsLogUtils";
private static final boolean DEBUG = true;
private static final String ACCESSIBILITY_PREFERENCE = "accessibility_prefs";
private static final String BT_HEARING_AIDS_PAIRED_HISTORY = "bt_hearing_aids_paired_history";
private static final String BT_HEARING_AIDS_CONNECTED_HISTORY =
"bt_hearing_aids_connected_history";
private static final String BT_HEARING_DEVICES_PAIRED_HISTORY =
"bt_hearing_devices_paired_history";
private static final String BT_HEARING_DEVICES_CONNECTED_HISTORY =
"bt_hearing_devices_connected_history";
private static final String HISTORY_RECORD_DELIMITER = ",";
/**
* Type of different Bluetooth device events history related to hearing.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({
HistoryType.TYPE_UNKNOWN,
HistoryType.TYPE_HEARING_AIDS_PAIRED,
HistoryType.TYPE_HEARING_AIDS_CONNECTED,
HistoryType.TYPE_HEARING_DEVICES_PAIRED,
HistoryType.TYPE_HEARING_DEVICES_CONNECTED})
public @interface HistoryType {
int TYPE_UNKNOWN = -1;
int TYPE_HEARING_AIDS_PAIRED = 0;
int TYPE_HEARING_AIDS_CONNECTED = 1;
int TYPE_HEARING_DEVICES_PAIRED = 2;
int TYPE_HEARING_DEVICES_CONNECTED = 3;
}
private static final HashMap<String, Integer> sDeviceAddressToBondEntryMap = new HashMap<>();
private static final Set<String> sJustBondedDeviceAddressSet = new HashSet<>();
/**
* Sets the mapping from hearing aid device to the bond entry where this device starts it's
* bonding(connecting) process.
*
* @param bondEntry The entry page id where the bonding process starts
* @param device The bonding(connecting) hearing aid device
*/
public static void setBondEntryForDevice(int bondEntry, CachedBluetoothDevice device) {
sDeviceAddressToBondEntryMap.put(device.getAddress(), bondEntry);
}
/**
* Logs hearing aid device information to statsd, including device mode, device side, and entry
* page id where the binding(connecting) process starts.
*
* Only logs the info once after hearing aid is bonded(connected). Clears the map entry of this
* device when logging is completed.
*
* @param device The bonded(connected) hearing aid device
*/
public static void logHearingAidInfo(CachedBluetoothDevice device) {
final String deviceAddress = device.getAddress();
if (sDeviceAddressToBondEntryMap.containsKey(deviceAddress)) {
final int bondEntry = sDeviceAddressToBondEntryMap.getOrDefault(deviceAddress, -1);
final int deviceMode = device.getDeviceMode();
final int deviceSide = device.getDeviceSide();
FrameworkStatsLog.write(FrameworkStatsLog.HEARING_AID_INFO_REPORTED, deviceMode,
deviceSide, bondEntry);
sDeviceAddressToBondEntryMap.remove(deviceAddress);
} else {
Log.w(TAG, "The device address was not found. Hearing aid device info is not logged.");
}
}
@VisibleForTesting
static HashMap<String, Integer> getDeviceAddressToBondEntryMap() {
return sDeviceAddressToBondEntryMap;
}
/**
* Maintains a temporarily list of just bonded device address. After the device profiles are
* connected, {@link HearingAidStatsLogUtils#removeFromJustBonded} will be called to remove the
* address.
* @param address the device address
*/
public static void addToJustBonded(String address) {
sJustBondedDeviceAddressSet.add(address);
}
/**
* Removes the device address from the just bonded list.
* @param address the device address
*/
public static void removeFromJustBonded(String address) {
sJustBondedDeviceAddressSet.remove(address);
}
/**
* Checks whether the device address is in the just bonded list.
* @param address the device address
* @return true if the device address is in the just bonded list
*/
public static boolean isJustBonded(String address) {
return sJustBondedDeviceAddressSet.contains(address);
}
/**
* Clears all BT hearing devices related history stored in shared preference.
* @param context the request context
*/
public static synchronized void clearHistory(Context context) {
SharedPreferences.Editor editor = getSharedPreferences(context).edit();
editor.remove(BT_HEARING_AIDS_PAIRED_HISTORY)
.remove(BT_HEARING_AIDS_CONNECTED_HISTORY)
.remove(BT_HEARING_DEVICES_PAIRED_HISTORY)
.remove(BT_HEARING_DEVICES_CONNECTED_HISTORY)
.apply();
}
/**
* Adds current timestamp into BT hearing devices related history.
* @param context the request context
* @param type the type of history to store the data. See {@link HistoryType}.
*/
public static void addCurrentTimeToHistory(Context context, @HistoryType int type) {
addToHistory(context, type, System.currentTimeMillis());
}
static synchronized void addToHistory(Context context, @HistoryType int type,
long timestamp) {
LinkedList<Long> history = getHistory(context, type);
if (history == null) {
if (DEBUG) {
Log.w(TAG, "Couldn't find shared preference name matched type=" + type);
}
return;
}
if (history.peekLast() != null && isSameDay(history.peekLast(), timestamp)) {
if (DEBUG) {
Log.w(TAG, "Skip this record, it's same day record");
}
return;
}
history.add(timestamp);
SharedPreferences.Editor editor = getSharedPreferences(context).edit();
editor.putString(HISTORY_TYPE_TO_SP_NAME_MAPPING.get(type),
convertToHistoryString(history)).apply();
}
@Nullable
static synchronized LinkedList<Long> getHistory(Context context, @HistoryType int type) {
String spName = HISTORY_TYPE_TO_SP_NAME_MAPPING.get(type);
if (BT_HEARING_AIDS_PAIRED_HISTORY.equals(spName)
|| BT_HEARING_DEVICES_PAIRED_HISTORY.equals(spName)) {
LinkedList<Long> history = convertToHistoryList(
getSharedPreferences(context).getString(spName, ""));
removeRecordsBeforeDays(history, 30);
return history;
} else if (BT_HEARING_AIDS_CONNECTED_HISTORY.equals(spName)
|| BT_HEARING_DEVICES_CONNECTED_HISTORY.equals(spName)) {
LinkedList<Long> history = convertToHistoryList(
getSharedPreferences(context).getString(spName, ""));
removeRecordsBeforeDays(history, 7);
return history;
}
return null;
}
private static void removeRecordsBeforeDays(LinkedList<Long> history, int days) {
if (history == null) {
return;
}
Long currentTime = System.currentTimeMillis();
while (history.peekFirst() != null
&& currentTime - history.peekFirst() > TimeUnit.DAYS.toMillis(days)) {
history.poll();
}
}
private static String convertToHistoryString(LinkedList<Long> history) {
return history.stream().map(Object::toString).collect(
Collectors.joining(HISTORY_RECORD_DELIMITER));
}
private static LinkedList<Long> convertToHistoryList(String string) {
if (string == null || string.isEmpty()) {
return new LinkedList<>();
}
LinkedList<Long> ll = new LinkedList<>();
String[] elements = string.split(HISTORY_RECORD_DELIMITER);
for (String e: elements) {
if (e.isEmpty()) continue;
ll.offer(Long.parseLong(e));
}
return ll;
}
/**
* Check if two timestamps are in the same date according to current timezone. This function
* doesn't consider the original timezone when the timestamp is saved.
*
* @param t1 the first epoch timestamp
* @param t2 the second epoch timestamp
* @return {@code true} if two timestamps are on the same day
*/
private static boolean isSameDay(long t1, long t2) {
final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd", Locale.getDefault());
sdf.setTimeZone(TimeZone.getDefault());
String dateString1 = sdf.format(t1);
String dateString2 = sdf.format(t2);
return dateString1.equals(dateString2);
}
private static SharedPreferences getSharedPreferences(Context context) {
return context.getSharedPreferences(ACCESSIBILITY_PREFERENCE, Context.MODE_PRIVATE);
}
private static final HashMap<Integer, String> HISTORY_TYPE_TO_SP_NAME_MAPPING;
static {
HISTORY_TYPE_TO_SP_NAME_MAPPING = new HashMap<>();
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
HistoryType.TYPE_HEARING_AIDS_PAIRED, BT_HEARING_AIDS_PAIRED_HISTORY);
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
HistoryType.TYPE_HEARING_AIDS_CONNECTED, BT_HEARING_AIDS_CONNECTED_HISTORY);
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
HistoryType.TYPE_HEARING_DEVICES_PAIRED, BT_HEARING_DEVICES_PAIRED_HISTORY);
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
HistoryType.TYPE_HEARING_DEVICES_CONNECTED, BT_HEARING_DEVICES_CONNECTED_HISTORY);
}
private HearingAidStatsLogUtils() {}
}