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

Commit 9c962b03 authored by Zaiyue Xue's avatar Zaiyue Xue
Browse files

Support accessibility for battery chart (1)

Remove the logic of disabling clickable when accessability is on in battery chartview.

Bug: 242989585
Test: manual
Change-Id: I92ce0ff5aac5220d686d600dbdf1d5738fe2c385
Merged-In: I92ce0ff5aac5220d686d600dbdf1d5738fe2c385
parent 97985b62
Loading
Loading
Loading
Loading
+12 −141
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ import static com.android.settings.Utils.formatPercentage;
import static java.lang.Math.round;
import static java.util.Objects.requireNonNull;

import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
@@ -29,13 +28,11 @@ import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
import android.widget.TextView;

import androidx.annotation.NonNull;
@@ -43,20 +40,15 @@ import androidx.annotation.VisibleForTesting;
import androidx.appcompat.widget.AppCompatImageView;

import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.Utils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

/** A widget component to draw chart graph. */
public class BatteryChartView extends AppCompatImageView implements View.OnClickListener,
        AccessibilityManager.AccessibilityStateChangeListener {
public class BatteryChartView extends AppCompatImageView implements View.OnClickListener {
    private static final String TAG = "BatteryChartView";
    private static final List<String> ACCESSIBILITY_SERVICE_NAMES =
            Arrays.asList("SwitchAccessService", "TalkBackService", "JustSpeakService");

    private static final int DIVIDER_COLOR = Color.parseColor("#CDCCC5");
    private static final long UPDATE_STATE_DELAYED_TIME = 500L;
@@ -67,48 +59,31 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
        void onSelect(int trapezoidIndex);
    }

    private BatteryChartViewModel mViewModel;
    private final String[] mPercentages = getPercentages();
    private final Rect mIndent = new Rect();
    private final Rect[] mPercentageBounds = new Rect[]{new Rect(), new Rect(), new Rect()};
    private final List<Rect> mAxisLabelsBounds = new ArrayList<>();

    private BatteryChartViewModel mViewModel;
    private int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
    private int mDividerWidth;
    private int mDividerHeight;
    private float mTrapezoidVOffset;
    private float mTrapezoidHOffset;
    private boolean mIsSlotsClickabled;
    private String[] mPercentages = getPercentages();

    @VisibleForTesting
    int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;

    // Colors for drawing the trapezoid shape and dividers.
    private int mTrapezoidColor;
    private int mTrapezoidSolidColor;
    private int mTrapezoidHoverColor;
    // For drawing the percentage information.
    private int mTextPadding;
    private final Rect mIndent = new Rect();
    private final Rect[] mPercentageBounds =
            new Rect[]{new Rect(), new Rect(), new Rect()};
    // For drawing the axis label information.
    private final List<Rect> mAxisLabelsBounds = new ArrayList<>();


    @VisibleForTesting
    Handler mHandler = new Handler();
    @VisibleForTesting
    final Runnable mUpdateClickableStateRun = () -> updateClickableState();

    private Paint mTextPaint;
    private Paint mDividerPaint;
    private Paint mTrapezoidPaint;
    private Paint mTextPaint;
    private BatteryChartView.OnSelectListener mOnSelectListener;

    @VisibleForTesting
    Paint mTrapezoidCurvePaint = null;
    @VisibleForTesting
    TrapezoidSlot[] mTrapezoidSlots;
    // Records the location to calculate selected index.
    @VisibleForTesting
    float mTouchUpEventX = Float.MIN_VALUE;
    private BatteryChartView.OnSelectListener mOnSelectListener;

    public BatteryChartView(Context context) {
        super(context, null);
@@ -261,66 +236,6 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
        view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        updateClickableState();
        mContext.getSystemService(AccessibilityManager.class)
                .addAccessibilityStateChangeListener(/*listener=*/ this);
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mContext.getSystemService(AccessibilityManager.class)
                .removeAccessibilityStateChangeListener(/*listener=*/ this);
        mHandler.removeCallbacks(mUpdateClickableStateRun);
    }

    @Override
    public void onAccessibilityStateChanged(boolean enabled) {
        Log.d(TAG, "onAccessibilityStateChanged:" + enabled);
        mHandler.removeCallbacks(mUpdateClickableStateRun);
        // We should delay it a while since accessibility manager will spend
        // some times to bind with new enabled accessibility services.
        mHandler.postDelayed(
                mUpdateClickableStateRun, UPDATE_STATE_DELAYED_TIME);
    }

    private void updateClickableState() {
        final Context context = mContext;
        mIsSlotsClickabled =
                FeatureFactory.getFactory(context)
                        .getPowerUsageFeatureProvider(context)
                        .isChartGraphSlotsEnabled(context)
                        && !isAccessibilityEnabled(context);
        Log.d(TAG, "isChartGraphSlotsEnabled:" + mIsSlotsClickabled);
        setClickable(isClickable());
        // Initializes the trapezoid curve paint for non-clickable case.
        if (!mIsSlotsClickabled && mTrapezoidCurvePaint == null) {
            mTrapezoidCurvePaint = new Paint();
            mTrapezoidCurvePaint.setAntiAlias(true);
            mTrapezoidCurvePaint.setColor(mTrapezoidSolidColor);
            mTrapezoidCurvePaint.setStyle(Paint.Style.STROKE);
            mTrapezoidCurvePaint.setStrokeWidth(mDividerWidth * 2);
        } else if (mIsSlotsClickabled) {
            mTrapezoidCurvePaint = null;
            // Sets view model again to force update the click state.
            setViewModel(mViewModel);
        }
        invalidate();
    }

    @Override
    public void setClickable(boolean clickable) {
        super.setClickable(mIsSlotsClickabled && clickable);
    }

    @VisibleForTesting
    void setClickableForce(boolean clickable) {
        super.setClickable(clickable);
    }

    private void initializeTrapezoidSlots(int count) {
        mTrapezoidSlots = new TrapezoidSlot[count];
        for (int index = 0; index < mTrapezoidSlots.length; index++) {
@@ -545,19 +460,14 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
        for (int index = 0; index < mTrapezoidSlots.length; index++) {
            // Not draws the trapezoid for corner or not initialization cases.
            if (!isValidToDraw(mViewModel, index)) {
                if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) {
                    canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint);
                    trapezoidCurvePath = null;
                }
                continue;
            }
            // Configures the trapezoid paint color.
            final int trapezoidColor = mIsSlotsClickabled && (mViewModel.selectedIndex() == index
            final int trapezoidColor = (mViewModel.selectedIndex() == index
                    || mViewModel.selectedIndex() == BatteryChartViewModel.SELECTED_INDEX_ALL)
                    ? mTrapezoidSolidColor : mTrapezoidColor;
            final boolean isHoverState =
                    mIsSlotsClickabled && mHoveredIndex == index
                            && isValidToDraw(mViewModel, mHoveredIndex);
            final boolean isHoverState = mHoveredIndex == index && isValidToDraw(mViewModel,
                    mHoveredIndex);
            mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);

            final float leftTop = round(
@@ -574,22 +484,6 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
            trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
            // Draws the trapezoid shape into canvas.
            canvas.drawPath(trapezoidPath, mTrapezoidPaint);

            // Generates path for non-clickable trapezoid curve.
            if (mTrapezoidCurvePaint != null) {
                if (trapezoidCurvePath == null) {
                    trapezoidCurvePath = new Path();
                    trapezoidCurvePath.moveTo(mTrapezoidSlots[index].mLeft, leftTop);
                } else {
                    trapezoidCurvePath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
                }
                trapezoidCurvePath.lineTo(mTrapezoidSlots[index].mRight, rightTop);
            }
        }
        // Draws the trapezoid curve for non-clickable case.
        if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) {
            canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint);
            trapezoidCurvePath = null;
        }
    }

@@ -645,29 +539,6 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
                formatPercentage(/*percentage=*/ 0, /*round=*/ true)};
    }

    @VisibleForTesting
    static boolean isAccessibilityEnabled(Context context) {
        final AccessibilityManager accessibilityManager =
                context.getSystemService(AccessibilityManager.class);
        if (!accessibilityManager.isEnabled()) {
            return false;
        }
        final List<AccessibilityServiceInfo> serviceInfoList =
                accessibilityManager.getEnabledAccessibilityServiceList(
                        AccessibilityServiceInfo.FEEDBACK_SPOKEN
                                | AccessibilityServiceInfo.FEEDBACK_GENERIC);
        for (AccessibilityServiceInfo info : serviceInfoList) {
            for (String serviceName : ACCESSIBILITY_SERVICE_NAMES) {
                final String serviceId = info.getId();
                if (serviceId != null && serviceId.contains(serviceName)) {
                    Log.d(TAG, "acccessibilityEnabled:" + serviceId);
                    return true;
                }
            }
        }
        return false;
    }

    // A container class for each trapezoid left and right location.
    @VisibleForTesting
    static final class TrapezoidSlot {
+0 −152
Original line number Diff line number Diff line
@@ -17,17 +17,11 @@ package com.android.settings.fuelgauge.batteryusage;

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

import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.Context;
import android.os.LocaleList;
import android.view.View;
import android.view.accessibility.AccessibilityManager;

import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
import com.android.settings.testutils.FakeFeatureFactory;
@@ -40,8 +34,6 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

@@ -53,10 +45,6 @@ public final class BatteryChartViewTest {
    private FakeFeatureFactory mFeatureFactory;
    private PowerUsageFeatureProvider mPowerUsageFeatureProvider;

    @Mock
    private AccessibilityServiceInfo mMockAccessibilityServiceInfo;
    @Mock
    private AccessibilityManager mMockAccessibilityManager;
    @Mock
    private View mMockView;

@@ -69,34 +57,6 @@ public final class BatteryChartViewTest {
        mContext.getResources().getConfiguration().setLocales(
                new LocaleList(new Locale("en_US")));
        mBatteryChartView = new BatteryChartView(mContext);
        doReturn(mMockAccessibilityManager).when(mContext)
                .getSystemService(AccessibilityManager.class);
        doReturn("TalkBackService").when(mMockAccessibilityServiceInfo).getId();
        doReturn(Arrays.asList(mMockAccessibilityServiceInfo))
                .when(mMockAccessibilityManager)
                .getEnabledAccessibilityServiceList(anyInt());
    }

    @Test
    public void isAccessibilityEnabled_disable_returnFalse() {
        doReturn(false).when(mMockAccessibilityManager).isEnabled();
        assertThat(BatteryChartView.isAccessibilityEnabled(mContext)).isFalse();
    }

    @Test
    public void isAccessibilityEnabled_emptyInfo_returnFalse() {
        doReturn(true).when(mMockAccessibilityManager).isEnabled();
        doReturn(new ArrayList<AccessibilityServiceInfo>())
                .when(mMockAccessibilityManager)
                .getEnabledAccessibilityServiceList(anyInt());

        assertThat(BatteryChartView.isAccessibilityEnabled(mContext)).isFalse();
    }

    @Test
    public void isAccessibilityEnabled_validServiceId_returnTrue() {
        doReturn(true).when(mMockAccessibilityManager).isEnabled();
        assertThat(BatteryChartView.isAccessibilityEnabled(mContext)).isTrue();
    }

    @Test
@@ -130,116 +90,4 @@ public final class BatteryChartViewTest {
        mBatteryChartView.onClick(mMockView);
        assertThat(selectedIndex[0]).isEqualTo(BatteryChartViewModel.SELECTED_INDEX_ALL);
    }

    @Test
    public void clickable_isChartGraphSlotsEnabledIsFalse_notClickable() {
        mBatteryChartView.setClickableForce(true);
        when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext))
                .thenReturn(false);

        mBatteryChartView.onAttachedToWindow();

        assertThat(mBatteryChartView.isClickable()).isFalse();
        assertThat(mBatteryChartView.mTrapezoidCurvePaint).isNotNull();
    }

    @Test
    public void clickable_accessibilityIsDisabled_clickable() {
        mBatteryChartView.setClickableForce(true);
        when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext))
                .thenReturn(true);
        doReturn(false).when(mMockAccessibilityManager).isEnabled();

        mBatteryChartView.onAttachedToWindow();

        assertThat(mBatteryChartView.isClickable()).isTrue();
        assertThat(mBatteryChartView.mTrapezoidCurvePaint).isNull();
    }

    @Test
    public void clickable_accessibilityIsEnabledWithoutValidId_clickable() {
        mBatteryChartView.setClickableForce(true);
        when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext))
                .thenReturn(true);
        doReturn(true).when(mMockAccessibilityManager).isEnabled();
        doReturn(new ArrayList<AccessibilityServiceInfo>())
                .when(mMockAccessibilityManager)
                .getEnabledAccessibilityServiceList(anyInt());

        mBatteryChartView.onAttachedToWindow();

        assertThat(mBatteryChartView.isClickable()).isTrue();
        assertThat(mBatteryChartView.mTrapezoidCurvePaint).isNull();
    }

    @Test
    public void clickable_accessibilityIsEnabledWithValidId_notClickable() {
        mBatteryChartView.setClickableForce(true);
        when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext))
                .thenReturn(true);
        doReturn(true).when(mMockAccessibilityManager).isEnabled();

        mBatteryChartView.onAttachedToWindow();

        assertThat(mBatteryChartView.isClickable()).isFalse();
        assertThat(mBatteryChartView.mTrapezoidCurvePaint).isNotNull();
    }

    @Test
    public void clickable_restoreFromNonClickableState() {
        final List<Integer> levels = new ArrayList<Integer>();
        final List<String> texts = new ArrayList<String>();
        for (int index = 0; index < 13; index++) {
            levels.add(index + 1);
            texts.add("");
        }
        mBatteryChartView.setViewModel(new BatteryChartViewModel(levels, texts,
                BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS));
        mBatteryChartView.setClickableForce(true);
        when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext))
                .thenReturn(true);
        doReturn(true).when(mMockAccessibilityManager).isEnabled();
        mBatteryChartView.onAttachedToWindow();
        // Ensures the testing environment is correct.
        assertThat(mBatteryChartView.isClickable()).isFalse();
        // Turns off accessibility service.
        doReturn(false).when(mMockAccessibilityManager).isEnabled();

        mBatteryChartView.onAttachedToWindow();

        assertThat(mBatteryChartView.isClickable()).isTrue();
    }

    @Test
    public void onAttachedToWindow_addAccessibilityStateChangeListener() {
        mBatteryChartView.onAttachedToWindow();
        verify(mMockAccessibilityManager)
                .addAccessibilityStateChangeListener(mBatteryChartView);
    }

    @Test
    public void onDetachedFromWindow_removeAccessibilityStateChangeListener() {
        mBatteryChartView.onAttachedToWindow();
        mBatteryChartView.mHandler.postDelayed(
                mBatteryChartView.mUpdateClickableStateRun, 1000);

        mBatteryChartView.onDetachedFromWindow();

        verify(mMockAccessibilityManager)
                .removeAccessibilityStateChangeListener(mBatteryChartView);
        assertThat(mBatteryChartView.mHandler.hasCallbacks(
                mBatteryChartView.mUpdateClickableStateRun))
                .isFalse();
    }

    @Test
    public void onAccessibilityStateChanged_postUpdateStateRunnable() {
        mBatteryChartView.mHandler = spy(mBatteryChartView.mHandler);
        mBatteryChartView.onAccessibilityStateChanged(/*enabled=*/ true);

        verify(mBatteryChartView.mHandler)
                .removeCallbacks(mBatteryChartView.mUpdateClickableStateRun);
        verify(mBatteryChartView.mHandler)
                .postDelayed(mBatteryChartView.mUpdateClickableStateRun, 500L);
    }
}