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

Commit 711ffac4 authored by Angela Wang's avatar Angela Wang
Browse files

Update categorization logic when updating history

Device categorization currently classifies devices based on Asha/HapClient(hearing devices) and A2dp/Headset(hearable devices). LE Audio profile should also be taken into consideration to ensure accurate user segmentation.

Flag: EXEMPT log only
Bug: 407477051
Test: atest HearingAidStatsLogUtilsTest
Change-Id: Ie4e73433d52645d624b7b77f2520990b20b603ef
parent 220d8441
Loading
Loading
Loading
Loading
+55 −30
Original line number Diff line number Diff line
@@ -45,16 +45,25 @@ 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 =

    private static final String BT_HEARING_DEVICES_PAIRED_HISTORY =
            "bt_hearing_aids_paired_history";
    private static final String BT_HEARING_DEVICES_CONNECTED_HISTORY =
            "bt_hearing_aids_connected_history";
    // The values here actually represent Bluetooth hearable devices, but were mistyped
    // as hearing devices in the string value previously. Keep the string values to ensure record
    // persistence.
    private static final String BT_HEARABLE_DEVICES_PAIRED_HISTORY =
            "bt_hearing_devices_paired_history";
    private static final String BT_HEARABLE_DEVICES_CONNECTED_HISTORY =
            "bt_hearing_devices_connected_history";

    private static final String HISTORY_RECORD_DELIMITER = ",";
    static final String CATEGORY_HEARING_AIDS = "A11yHearingAidsUser";
    static final String CATEGORY_NEW_HEARING_AIDS = "A11yNewHearingAidsUser";

    static final String CATEGORY_HEARING_DEVICES = "A11yHearingAidsUser";
    static final String CATEGORY_NEW_HEARING_DEVICES = "A11yNewHearingAidsUser";
    // Previously, 'hearable device' was incorrectly entered as 'hearing device'. These string
    // values are maintained to ensure records persistence.
    static final String CATEGORY_HEARABLE_DEVICES = "A11yHearingDevicesUser";
    static final String CATEGORY_NEW_HEARABLE_DEVICES = "A11yNewHearingDevicesUser";

@@ -69,14 +78,14 @@ public final class HearingAidStatsLogUtils {
    @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,
            HistoryType.TYPE_HEARABLE_DEVICES_PAIRED,
            HistoryType.TYPE_HEARABLE_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 = 0;
        int TYPE_HEARING_DEVICES_CONNECTED = 1;
        int TYPE_HEARABLE_DEVICES_PAIRED = 2;
        int TYPE_HEARABLE_DEVICES_CONNECTED = 3;
    }
@@ -137,27 +146,25 @@ public final class HearingAidStatsLogUtils {
            LocalBluetoothProfile profile, int profileState) {

        if (isJustBonded(cachedDevice.getAddress())) {
            // Saves bonded timestamp as the source for judging whether to display
            // the survey
            if (cachedDevice.getProfiles().stream().anyMatch(
                    p -> (p instanceof HearingAidProfile || p instanceof HapClientProfile))) {
            // Saves bonded timestamp as the source for judging whether to display the survey
            if (isHearingDevice(cachedDevice)) {
                HearingAidStatsLogUtils.addCurrentTimeToHistory(context,
                        HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_AIDS_PAIRED);
            } else if (cachedDevice.getProfiles().stream().anyMatch(
                    p -> (p instanceof A2dpSinkProfile || p instanceof HeadsetProfile))) {
                        HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_PAIRED);
            } else if (isHearableDevice(cachedDevice)) {
                HearingAidStatsLogUtils.addCurrentTimeToHistory(context,
                        HearingAidStatsLogUtils.HistoryType.TYPE_HEARABLE_DEVICES_PAIRED);
            }
            removeFromJustBonded(cachedDevice.getAddress());
        }

        // Saves connected timestamp as the source for judging whether to display
        // the survey
        if (profileState == BluetoothProfile.STATE_CONNECTED) {
            if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) {
            // Saves connected timestamp as the source for judging whether to display the survey
            if (isHearingProfile(profile)) {
                HearingAidStatsLogUtils.addCurrentTimeToHistory(context,
                        HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_AIDS_CONNECTED);
            } else if (profile instanceof A2dpSinkProfile || profile instanceof HeadsetProfile) {
                        HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_CONNECTED);
            } else if (isHearableProfile(profile) && !isHearingDevice(cachedDevice)) {
                // Hearing devices may contain some hearable profiles as well, make sure to exclude
                // these cases when update hearable connected history.
                HearingAidStatsLogUtils.addCurrentTimeToHistory(context,
                        HearingAidStatsLogUtils.HistoryType.TYPE_HEARABLE_DEVICES_CONNECTED);
            }
@@ -166,8 +173,8 @@ public final class HearingAidStatsLogUtils {

    /**
     * Returns the user category if the user is already categorized. Otherwise, checks the
     * history and sees if the user is categorized as one of {@link #CATEGORY_HEARING_AIDS},
     * {@link #CATEGORY_NEW_HEARING_AIDS}, {@link #CATEGORY_HEARABLE_DEVICES}, and
     * history and sees if the user is categorized as one of {@link #CATEGORY_HEARING_DEVICES},
     * {@link #CATEGORY_NEW_HEARING_DEVICES}, {@link #CATEGORY_HEARABLE_DEVICES}, and
     * {@link #CATEGORY_NEW_HEARABLE_DEVICES}.
     *
     * @param context the request context
@@ -175,19 +182,19 @@ public final class HearingAidStatsLogUtils {
     */
    public static synchronized String getUserCategory(Context context) {
        LinkedList<Long> hearingAidsConnectedHistory = getHistory(context,
                HistoryType.TYPE_HEARING_AIDS_CONNECTED);
                HistoryType.TYPE_HEARING_DEVICES_CONNECTED);
        if (hearingAidsConnectedHistory != null
                && hearingAidsConnectedHistory.size() >= VALID_CONNECTED_EVENT_COUNT) {
            LinkedList<Long> hearingAidsPairedHistory = getHistory(context,
                    HistoryType.TYPE_HEARING_AIDS_PAIRED);
                    HistoryType.TYPE_HEARING_DEVICES_PAIRED);
            // Since paired history will be cleared after 30 days. If there's any record within 30
            // days, the user will be categorized as CATEGORY_NEW_HEARING_AIDS. Otherwise, the user
            // will be categorized as CATEGORY_HEARING_AIDS.
            if (hearingAidsPairedHistory != null
                    && hearingAidsPairedHistory.size() >= VALID_PAIRED_EVENT_COUNT) {
                return CATEGORY_NEW_HEARING_AIDS;
                return CATEGORY_NEW_HEARING_DEVICES;
            } else {
                return CATEGORY_HEARING_AIDS;
                return CATEGORY_HEARING_DEVICES;
            }
        }

@@ -271,13 +278,13 @@ public final class HearingAidStatsLogUtils {
    @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)
        if (BT_HEARING_DEVICES_PAIRED_HISTORY.equals(spName)
                || BT_HEARABLE_DEVICES_PAIRED_HISTORY.equals(spName)) {
            LinkedList<Long> history = convertToHistoryList(
                    getSharedPreferences(context).getString(spName, ""));
            removeRecordsBeforeDay(history, PAIRED_HISTORY_EXPIRED_DAY);
            return history;
        } else if (BT_HEARING_AIDS_CONNECTED_HISTORY.equals(spName)
        } else if (BT_HEARING_DEVICES_CONNECTED_HISTORY.equals(spName)
                || BT_HEARABLE_DEVICES_CONNECTED_HISTORY.equals(spName)) {
            LinkedList<Long> history = convertToHistoryList(
                    getSharedPreferences(context).getString(spName, ""));
@@ -333,6 +340,23 @@ public final class HearingAidStatsLogUtils {
        return Math.abs(ChronoUnit.DAYS.between(date1, date2));
    }

    private static boolean isHearingDevice(CachedBluetoothDevice device) {
        return device.getProfiles().stream().anyMatch(HearingAidStatsLogUtils::isHearingProfile);
    }

    private static boolean isHearingProfile(LocalBluetoothProfile profile) {
        return profile instanceof HearingAidProfile || profile instanceof HapClientProfile;
    }

    private static boolean isHearableDevice(CachedBluetoothDevice device) {
        return device.getProfiles().stream().anyMatch(HearingAidStatsLogUtils::isHearableProfile);
    }

    private static boolean isHearableProfile(LocalBluetoothProfile profile) {
        return profile instanceof A2dpProfile || profile instanceof HeadsetProfile
                || profile instanceof LeAudioProfile;
    }

    private static SharedPreferences getSharedPreferences(Context context) {
        return context.getSharedPreferences(ACCESSIBILITY_PREFERENCE, Context.MODE_PRIVATE);
    }
@@ -341,13 +365,14 @@ public final class HearingAidStatsLogUtils {
    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);
                HistoryType.TYPE_HEARING_DEVICES_PAIRED, BT_HEARING_DEVICES_PAIRED_HISTORY);
        HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
                HistoryType.TYPE_HEARING_AIDS_CONNECTED, BT_HEARING_AIDS_CONNECTED_HISTORY);
                HistoryType.TYPE_HEARING_DEVICES_CONNECTED, BT_HEARING_DEVICES_CONNECTED_HISTORY);
        HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
                HistoryType.TYPE_HEARABLE_DEVICES_PAIRED, BT_HEARABLE_DEVICES_PAIRED_HISTORY);
        HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
                HistoryType.TYPE_HEARABLE_DEVICES_CONNECTED, BT_HEARABLE_DEVICES_CONNECTED_HISTORY);
    }

    private HearingAidStatsLogUtils() {}
}
+81 −13
Original line number Diff line number Diff line
@@ -21,8 +21,11 @@ import static com.android.settingslib.bluetooth.HearingAidStatsLogUtils.PAIRED_H

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.bluetooth.BluetoothProfile;
import android.content.Context;

import androidx.test.core.app.ApplicationProvider;
@@ -42,6 +45,7 @@ import java.time.LocalDate;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@RunWith(RobolectricTestRunner.class)
@@ -49,7 +53,7 @@ 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;
            HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_CONNECTED;

    @Rule
    public final MockitoRule mockito = MockitoJUnit.rule();
@@ -128,20 +132,20 @@ public class HearingAidStatsLogUtilsTest {
    }

    @Test
    public void getUserCategory_hearingAidsUser() {
        prepareHearingAidsUserHistory();
    public void getUserCategory_hearingDevicesUser() {
        prepareHearingDevicesUserHistory();

        assertThat(HearingAidStatsLogUtils.getUserCategory(mContext)).isEqualTo(
                HearingAidStatsLogUtils.CATEGORY_HEARING_AIDS);
                HearingAidStatsLogUtils.CATEGORY_HEARING_DEVICES);
    }

    @Test
    public void getUserCategory_newHearingAidsUser() {
        prepareHearingAidsUserHistory();
    public void getUserCategory_newHearingDevicesUser() {
        prepareHearingDevicesUserHistory();
        prepareNewUserHistory();

        assertThat(HearingAidStatsLogUtils.getUserCategory(mContext)).isEqualTo(
                HearingAidStatsLogUtils.CATEGORY_NEW_HEARING_AIDS);
                HearingAidStatsLogUtils.CATEGORY_NEW_HEARING_DEVICES);
    }

    @Test
@@ -162,12 +166,56 @@ public class HearingAidStatsLogUtilsTest {
    }

    @Test
    public void getUserCategory_bothHearingAidsAndHearableDevicesUser_returnHearingAidsUser() {
        prepareHearingAidsUserHistory();
    public void getUserCategory_bothHearingAndHearableDevicesUser_returnHearingDevicesUser() {
        prepareHearingDevicesUserHistory();
        prepareHearableDevicesUserHistory();

        assertThat(HearingAidStatsLogUtils.getUserCategory(mContext)).isEqualTo(
                HearingAidStatsLogUtils.CATEGORY_HEARING_AIDS);
                HearingAidStatsLogUtils.CATEGORY_HEARING_DEVICES);
    }

    @Test
    public void updateHistoryIfNeeded_ashaHearingDevice_ashaConnected_historyCorrect() {
        prepareAshaHearingDevice();

        HearingAidProfile ashaProfile = mock(HearingAidProfile.class);
        HearingAidStatsLogUtils.updateHistoryIfNeeded(mContext, mCachedBluetoothDevice, ashaProfile,
                BluetoothProfile.STATE_CONNECTED);

        assertHistorySize(HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_CONNECTED, 1);
    }

    @Test
    public void updateHistoryIfNeeded_leAudioHearingDevice_hapClientConnected_historyCorrect() {
        prepareLeAudioHearingDevice();

        HapClientProfile hapClientProfile = mock(HapClientProfile.class);
        HearingAidStatsLogUtils.updateHistoryIfNeeded(mContext, mCachedBluetoothDevice,
                hapClientProfile, BluetoothProfile.STATE_CONNECTED);

        assertHistorySize(HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_CONNECTED, 1);
    }

    @Test
    public void updateHistoryIfNeeded_leAudioHearingDevice_leAudioConnected_historyCorrect() {
        prepareLeAudioHearingDevice();

        LeAudioProfile leAudioProfile = mock(LeAudioProfile.class);
        HearingAidStatsLogUtils.updateHistoryIfNeeded(mContext, mCachedBluetoothDevice,
                leAudioProfile, BluetoothProfile.STATE_CONNECTED);

        assertHistorySize(HearingAidStatsLogUtils.HistoryType.TYPE_HEARABLE_DEVICES_CONNECTED, 0);
    }

    @Test
    public void updateHistoryIfNeeded_hearableDevice_leAudioConnected_historyCorrect() {
        prepareHearableDevice();

        LeAudioProfile leAudioProfile = mock(LeAudioProfile.class);
        HearingAidStatsLogUtils.updateHistoryIfNeeded(mContext, mCachedBluetoothDevice,
                leAudioProfile, BluetoothProfile.STATE_CONNECTED);

        assertHistorySize(HearingAidStatsLogUtils.HistoryType.TYPE_HEARABLE_DEVICES_CONNECTED, 1);
    }

    private long convertToStartOfDayTime(long timestamp) {
@@ -176,12 +224,12 @@ public class HearingAidStatsLogUtilsTest {
        return date.atStartOfDay(zoneId).toInstant().toEpochMilli();
    }

    private void prepareHearingAidsUserHistory() {
    private void prepareHearingDevicesUserHistory() {
        final long todayStartOfDay = convertToStartOfDayTime(System.currentTimeMillis());
        for (int i = CONNECTED_HISTORY_EXPIRED_DAY - 1; i >= 0; i--) {
            final long data = todayStartOfDay - TimeUnit.DAYS.toMillis(i);
            HearingAidStatsLogUtils.addToHistory(mContext,
                    HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_AIDS_CONNECTED, data);
                    HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_CONNECTED, data);
        }
    }

@@ -198,8 +246,28 @@ public class HearingAidStatsLogUtilsTest {
        final long todayStartOfDay = convertToStartOfDayTime(System.currentTimeMillis());
        final long data = todayStartOfDay - TimeUnit.DAYS.toMillis(PAIRED_HISTORY_EXPIRED_DAY - 1);
        HearingAidStatsLogUtils.addToHistory(mContext,
                HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_AIDS_PAIRED, data);
                HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_PAIRED, data);
        HearingAidStatsLogUtils.addToHistory(mContext,
                HearingAidStatsLogUtils.HistoryType.TYPE_HEARABLE_DEVICES_PAIRED, data);
    }

    private void prepareAshaHearingDevice() {
        doReturn(List.of(mock(HearingAidProfile.class))).when(mCachedBluetoothDevice).getProfiles();
    }

    private void prepareLeAudioHearingDevice() {
        doReturn(List.of(mock(HapClientProfile.class), mock(LeAudioProfile.class))).when(
                mCachedBluetoothDevice).getProfiles();
    }

    private void prepareHearableDevice() {
        doReturn(List.of(mock(A2dpProfile.class), mock(HeadsetProfile.class),
                mock(LeAudioProfile.class))).when(mCachedBluetoothDevice).getProfiles();
    }

    private void assertHistorySize(int type, int size) {
        LinkedList<Long> history = HearingAidStatsLogUtils.getHistory(mContext, type);
        assertThat(history).isNotNull();
        assertThat(history.size()).isEqualTo(size);
    }
}