Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8dafcfc0 authored by Angela Wang's avatar Angela Wang
Browse files

Handle BT hearing device event history for HaTS surveys

We'll run surveys on hearing adis and general hearing devices users to
know the satisfaction difference between different kinds of devices.
This can help us prioritize to improve the parts which are less satisfying.

Bug: 294627726
Test: atest HearingAidStatsLogUtilsTest
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:070e0055b2121f8cd0c42873b66b18fd778f7661)

Change-Id: I00280ddfcd11f3688c6c3e626cdae4914c5eb887
parent fda3324f
Loading
Loading
Loading
Loading
+165 −0
Original line number Diff line number Diff line
@@ -16,17 +16,59 @@

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.LinkedList;
import java.util.Locale;
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<>();

    /**
@@ -69,5 +111,128 @@ public final class HearingAidStatsLogUtils {
        return sDeviceAddressToBondEntryMap;
    }

    /**
     * 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() {}
}
+50 −0
Original line number Diff line number Diff line
@@ -20,6 +20,10 @@ import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.when;

import android.content.Context;

import androidx.test.core.app.ApplicationProvider;

import com.android.internal.util.FrameworkStatsLog;

import org.junit.Rule;
@@ -31,15 +35,21 @@ import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

@RunWith(RobolectricTestRunner.class)
public class HearingAidStatsLogUtilsTest {

    private static final String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1";
    private static final int TEST_HISTORY_TYPE =
            HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_AIDS_CONNECTED;

    @Rule
    public final MockitoRule mockito = MockitoJUnit.rule();

    private final Context mContext = ApplicationProvider.getApplicationContext();

    @Mock
    private CachedBluetoothDevice mCachedBluetoothDevice;

@@ -71,4 +81,44 @@ public class HearingAidStatsLogUtilsTest {
                HearingAidStatsLogUtils.getDeviceAddressToBondEntryMap();
        assertThat(map.containsKey(TEST_DEVICE_ADDRESS)).isFalse();
    }

    @Test
    public void addCurrentTimeToHistory_addNewData() {
        final long currentTime = System.currentTimeMillis();
        final long lastData = currentTime - TimeUnit.DAYS.toMillis(2);
        HearingAidStatsLogUtils.addToHistory(mContext, TEST_HISTORY_TYPE, lastData);

        HearingAidStatsLogUtils.addCurrentTimeToHistory(mContext, TEST_HISTORY_TYPE);

        LinkedList<Long> history = HearingAidStatsLogUtils.getHistory(mContext, TEST_HISTORY_TYPE);
        assertThat(history).isNotNull();
        assertThat(history.size()).isEqualTo(2);
    }
    @Test
    public void addCurrentTimeToHistory_skipSameDateData() {
        final long currentTime = System.currentTimeMillis();
        final long lastData = currentTime - 1;
        HearingAidStatsLogUtils.addToHistory(mContext, TEST_HISTORY_TYPE, lastData);

        HearingAidStatsLogUtils.addCurrentTimeToHistory(mContext, TEST_HISTORY_TYPE);

        LinkedList<Long> history = HearingAidStatsLogUtils.getHistory(mContext, TEST_HISTORY_TYPE);
        assertThat(history).isNotNull();
        assertThat(history.size()).isEqualTo(1);
        assertThat(history.getFirst()).isEqualTo(lastData);
    }

    @Test
    public void addCurrentTimeToHistory_cleanUpExpiredData() {
        final long currentTime = System.currentTimeMillis();
        final long expiredData = currentTime - TimeUnit.DAYS.toMillis(10);
        HearingAidStatsLogUtils.addToHistory(mContext, TEST_HISTORY_TYPE, expiredData);

        HearingAidStatsLogUtils.addCurrentTimeToHistory(mContext, TEST_HISTORY_TYPE);

        LinkedList<Long> history = HearingAidStatsLogUtils.getHistory(mContext, TEST_HISTORY_TYPE);
        assertThat(history).isNotNull();
        assertThat(history.size()).isEqualTo(1);
        assertThat(history.getFirst()).isNotEqualTo(expiredData);
    }
}