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

Commit 9feafd75 authored by Angela Wang's avatar Angela Wang
Browse files

Refactor: extract common methods to ExpandableUi interface

Prepare the common expandable interface for new feature - separated
preset control

Flag: EXEMPT simple refactor
Bug: 417628947
Test: AmbientVolumeLayoutTest
Test: AmbientVolumeUiControllerTest
Change-Id: Icd6d801fdccbafc43a96ccfdd0cd55fa80e16814
parent 7d0205b0
Loading
Loading
Loading
Loading
+14 −57
Original line number Diff line number Diff line
@@ -16,37 +16,24 @@

package com.android.settingslib.bluetooth.hearingdevices.ui;

import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT;
import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT;

import android.bluetooth.AudioInputControl;
import android.bluetooth.BluetoothDevice;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.List;
import java.util.Map;
import java.util.Set;

/** Interface for the ambient volume UI. */
public interface AmbientVolumeUi {
public interface AmbientVolumeUi extends ExpandableControlUi {

    /** Interface definition for a callback to be invoked when event happens in AmbientVolumeUi. */
    interface AmbientVolumeUiListener {
        /** Called when the expand icon is clicked. */
        void onExpandIconClick();

    interface AmbientVolumeUiListener extends ExpandableControlUiListener {
        /** Called when the ambient volume icon is clicked. */
        void onAmbientVolumeIconClick();

        /** Called when the slider of the specified side is changed. */
        void onSliderValueChange(int side, int value);
    };

    /** The rotation degree of the expand icon when the UI is in collapsed mode. */
    float ROTATION_COLLAPSED = 0f;
    /** The rotation degree of the expand icon when the UI is in expanded mode. */
    float ROTATION_EXPANDED = 180f;
    }

    /**
     * The default ambient volume level for hearing device ambient volume icon
@@ -78,34 +65,6 @@ public interface AmbientVolumeUi {
     */
    int AMBIENT_VOLUME_LEVEL_MAX = 24;

    /**
     * Ths side identifier for slider in collapsed mode which can unified control the ambient
     * volume of all devices in the same set.
     */
    int SIDE_UNIFIED = 999;

    /** All valid side of the sliders in the UI. */
    List<Integer> VALID_SIDES = List.of(SIDE_UNIFIED, SIDE_LEFT, SIDE_RIGHT);

    /** Sets if the UI is visible. */
    void setVisible(boolean visible);

    /**
     * Sets if the UI is expandable between expanded and collapsed mode.
     *
     * <p> If the UI is not expandable, it implies the UI will always stay in collapsed mode
     */
    void setControlExpandable(boolean expandable);

    /** @return if the UI is expandable. */
    boolean isControlExpandable();

    /** Sets if the UI is in expanded mode. */
    void setControlExpanded(boolean expanded);

    /** @return if the UI is in expanded mode. */
    boolean isControlExpanded();

    /** @return if the UI is capable to mute the ambient of remote device. */
    boolean isMutable();

@@ -113,22 +72,23 @@ public interface AmbientVolumeUi {
    boolean isMuted();

    /**
     * Sets listener on the UI.
     *
     * Sets the listener to be invoked when events happen in this UI.
     * @see AmbientVolumeUiListener
     */
    void setListener(@Nullable AmbientVolumeUiListener listener);

    /**
     * Sets up sliders in the UI.
     * Sets up sliders in the UI based on the provided sides.
     *
     * <p> For each side of device, the UI should hava a corresponding slider to control it's
     * ambient volume.
     * <p> For all devices in the same set, the UI should have a slider to control all devices'
     * ambient volume at once.
     * @param sideToDeviceMap the side and device mapping of all devices in the same set
     * <p>A slider will be created for each unique side identifier in the {@code sides} set.
     * Additionally, a unified slider ({@link ExpandableControlUi#SIDE_UNIFIED}) is always created
     * to control all sides together when the UI is in a collapsed state.
     *
     * @param sides A set of integers representing the device sides for which individual
     *              sliders should be created (e.g., {@code HearingAidInfo.DeviceSide.SIDE_LEFT},
     *              {@code HearingAidInfo.DeviceSide.SIDE_RIGHT}).
     */
    void setupSliders(@NonNull Map<Integer, BluetoothDevice> sideToDeviceMap);
    void setupSliders(@NonNull Set<Integer> sides);

    /**
     * Sets if the slider is enabled.
@@ -170,7 +130,4 @@ public interface AmbientVolumeUi {
     * @return the mute state, see {@link AudioInputControl.Mute}
     */
    int getSliderMuteState(int side);

    /** Updates the UI according to current state. */
    void updateLayout();
}
+4 −8
Original line number Diff line number Diff line
@@ -25,8 +25,8 @@ import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_I
import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT;
import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT;
import static com.android.settingslib.bluetooth.hearingdevices.metrics.HearingDeviceLocalDataManager.Data.INVALID_VOLUME;
import static com.android.settingslib.bluetooth.hearingdevices.ui.AmbientVolumeUi.SIDE_UNIFIED;
import static com.android.settingslib.bluetooth.hearingdevices.ui.AmbientVolumeUi.VALID_SIDES;
import static com.android.settingslib.bluetooth.hearingdevices.ui.ExpandableControlUi.SIDE_UNIFIED;
import static com.android.settingslib.bluetooth.hearingdevices.ui.ExpandableControlUi.VALID_SIDES;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
@@ -44,7 +44,6 @@ import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothEventManager;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.bluetooth.VolumeControlProfile;
import com.android.settingslib.bluetooth.hearingdevices.AmbientVolumeController;
import com.android.settingslib.bluetooth.hearingdevices.metrics.HearingDeviceLocalDataManager;
@@ -66,7 +65,6 @@ public class AmbientVolumeUiController implements
    private static final String TAG = "AmbientVolumeUiController";

    private final Context mContext;
    private final LocalBluetoothProfileManager mProfileManager;
    private final BluetoothEventManager mEventManager;
    private final AmbientVolumeUi mAmbientLayout;
    private final AmbientVolumeController mVolumeController;
@@ -83,11 +81,10 @@ public class AmbientVolumeUiController implements
            @NonNull LocalBluetoothManager bluetoothManager,
            @NonNull AmbientVolumeUi ambientLayout) {
        mContext = context;
        mProfileManager = bluetoothManager.getProfileManager();
        mEventManager = bluetoothManager.getEventManager();
        mAmbientLayout = ambientLayout;
        mAmbientLayout.setListener(this);
        mVolumeController = new AmbientVolumeController(mProfileManager, this);
        mVolumeController = new AmbientVolumeController(bluetoothManager.getProfileManager(), this);
        mLocalDataManager = new HearingDeviceLocalDataManager(context);
        mLocalDataManager.setOnDeviceLocalDataChangeListener(this,
                ThreadUtils.getBackgroundExecutor());
@@ -100,7 +97,6 @@ public class AmbientVolumeUiController implements
            @NonNull AmbientVolumeController volumeController,
            @NonNull HearingDeviceLocalDataManager localDataManager) {
        mContext = context;
        mProfileManager = bluetoothManager.getProfileManager();
        mEventManager = bluetoothManager.getEventManager();
        mAmbientLayout = ambientLayout;
        mVolumeController = volumeController;
@@ -353,7 +349,7 @@ public class AmbientVolumeUiController implements
        }

        mAmbientLayout.setControlExpandable(mSideToDeviceMap.size() >  1);
        mAmbientLayout.setupSliders(mSideToDeviceMap);
        mAmbientLayout.setupSliders(mSideToDeviceMap.keySet());
        if (mStarted) {
            refresh();
        }
+81 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.hearingdevices.ui

import com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT
import com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT
import com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_MONO
import com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT

/**
 * Defines the basic behavior and state for an expandable UI component.
 *
 * <p>An expandable UI typically consists of a header with a collapse/expand icon and multiple child
 * views. The UI's mode (expanded or collapsed) determines which specific set of child views is
 * visible.
 */
interface ExpandableControlUi {

    /** Interface definition for a callback to be invoked when event happens in ExpandableUi. */
    interface ExpandableControlUiListener {
        /** Called when the expand icon is clicked. */
        fun onExpandIconClick()
    }

    /** Sets if the UI is visible. */
    fun setVisible(visible: Boolean)

    /**
     * Sets if the UI is expandable between expanded and collapsed mode.
     *
     * A p> If the UI is not expandable, it implies the UI will always stay in collapsed mode
     */
    fun setControlExpandable(expandable: Boolean)

    /** @return if the UI is expandable. */
    fun isControlExpandable(): Boolean

    /** Sets if the UI is in expanded mode. */
    fun setControlExpanded(expanded: Boolean)

    /** @return if the UI is in expanded mode. */
    fun isControlExpanded(): Boolean

    companion object {
        /** The rotation degree of the expand icon when the UI is collapsed. */
        @JvmField val ROTATION_COLLAPSED = 0f
        /** The rotation degree of the expand icon when the UI is expanded. */
        @JvmField val ROTATION_EXPANDED = 180f

        /**
         * A special side identifier that represents unified control.
         *
         * A p>This identifier is used by the view in collapsed mode to allow a single control
         * to apply settings to all devices in the same set simultaneously.
         */
        @JvmField val SIDE_UNIFIED = 999

        /** All valid, controllable sides for the hearing device in the UI. */
        @JvmField val VALID_SIDES = listOf(
            SIDE_UNIFIED,
            SIDE_LEFT,
            SIDE_RIGHT,
            SIDE_MONO,
            SIDE_LEFT_AND_RIGHT
        )
    }
}
 No newline at end of file
+9 −17
Original line number Diff line number Diff line
@@ -84,7 +84,7 @@ public class AmbientVolumeLayoutTest extends SysuiTestCase {
        mLayout.setControlExpandable(true);

        prepareDevices();
        mLayout.setupSliders(mSideToDeviceMap);
        mLayout.setupSliders(mSideToDeviceMap.keySet());
        mLayout.getSliders().forEach((side, slider) -> {
            slider.setMin(0);
            slider.setMax(4);
@@ -110,6 +110,9 @@ public class AmbientVolumeLayoutTest extends SysuiTestCase {

    @Test
    public void setControlExpandable_notExpandable_expandIconGone() {
        // Change the state from its default (false) to true. This ensures that the subsequent call
        // to setControlExpandable(false) will trigger the update logic.
        mLayout.setControlExpandable(true);
        mLayout.setControlExpandable(false);

        assertThat(mExpandIcon.getVisibility()).isEqualTo(View.GONE);
@@ -120,29 +123,18 @@ public class AmbientVolumeLayoutTest extends SysuiTestCase {
        mLayout.setControlExpanded(true);

        assertControlUiCorrect();
    }

    @Test
    public void setControlExpanded_notExpanded_assertControlUiCorrect() {
        mLayout.setControlExpanded(false);

        assertControlUiCorrect();
    }

    @Test
    public void updateLayout_expanded_volumeIconIsCorrect() {
        mLayout.setControlExpanded(true);
        mLayout.updateLayout();

        int expectedLevel = calculateVolumeLevel(TEST_LEFT_VOLUME_LEVEL, TEST_RIGHT_VOLUME_LEVEL);
        assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(expectedLevel);
    }

    @Test
    public void updateLayout_notExpanded_volumeIconIsCorrect() {
    public void setControlExpanded_notExpanded_assertControlUiCorrect() {
        // Change the state from its default (false) to true. This ensures that the subsequent call
        // to setControlExpanded(false) will trigger the update logic.
        mLayout.setControlExpanded(true);
        mLayout.setControlExpanded(false);
        mLayout.updateLayout();

        assertControlUiCorrect();
        int expectedLevel = calculateVolumeLevel(TEST_UNIFIED_VOLUME_LEVEL,
                TEST_UNIFIED_VOLUME_LEVEL);
        assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(expectedLevel);
+4 −5
Original line number Diff line number Diff line
@@ -23,7 +23,6 @@ import static android.bluetooth.AudioInputControl.MUTE_NOT_MUTED;
import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT;
import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT;

import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.util.ArrayMap;
import android.util.AttributeSet;
@@ -42,6 +41,7 @@ import com.google.common.collect.HashBiMap;
import com.google.common.primitives.Ints;

import java.util.Map;
import java.util.Set;

/**
 * A view of ambient volume controls.
@@ -231,8 +231,8 @@ public class AmbientVolumeLayout extends LinearLayout implements AmbientVolumeUi
    }

    @Override
    public void setupSliders(@NonNull Map<Integer, BluetoothDevice> sideToDeviceMap) {
        sideToDeviceMap.forEach((side, device) -> createSlider(side));
    public void setupSliders(@NonNull Set<Integer> sides) {
        sides.forEach(this::createSlider);
        createSlider(SIDE_UNIFIED);

        LinearLayout controlContainer = requireViewById(R.id.ambient_control_container);
@@ -278,8 +278,7 @@ public class AmbientVolumeLayout extends LinearLayout implements AmbientVolumeUi
        }
    }

    @Override
    public void updateLayout() {
    private void updateLayout() {
        mSideToSliderMap.forEach((side, slider) -> {
            if (side == SIDE_UNIFIED) {
                slider.setVisibility(mExpanded ? GONE : VISIBLE);