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
This commit is contained in:
Zaiyue Xue
2022-08-22 14:09:24 +08:00
parent 97985b6219
commit 9c962b03e9
2 changed files with 12 additions and 293 deletions

View File

@@ -20,7 +20,6 @@ import static com.android.settings.Utils.formatPercentage;
import static java.lang.Math.round; import static java.lang.Math.round;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Canvas; import android.graphics.Canvas;
@@ -29,13 +28,11 @@ import android.graphics.CornerPathEffect;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Path; import android.graphics.Path;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Handler;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.view.HapticFeedbackConstants; import android.view.HapticFeedbackConstants;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.accessibility.AccessibilityManager;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -43,20 +40,15 @@ import androidx.annotation.VisibleForTesting;
import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatImageView;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.Utils; import com.android.settingslib.Utils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
/** A widget component to draw chart graph. */ /** A widget component to draw chart graph. */
public class BatteryChartView extends AppCompatImageView implements View.OnClickListener, public class BatteryChartView extends AppCompatImageView implements View.OnClickListener {
AccessibilityManager.AccessibilityStateChangeListener {
private static final String TAG = "BatteryChartView"; 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 int DIVIDER_COLOR = Color.parseColor("#CDCCC5");
private static final long UPDATE_STATE_DELAYED_TIME = 500L; 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); 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 mDividerWidth;
private int mDividerHeight; private int mDividerHeight;
private float mTrapezoidVOffset; private float mTrapezoidVOffset;
private float mTrapezoidHOffset; 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 mTrapezoidColor;
private int mTrapezoidSolidColor; private int mTrapezoidSolidColor;
private int mTrapezoidHoverColor; private int mTrapezoidHoverColor;
// For drawing the percentage information.
private int mTextPadding; 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 mDividerPaint;
private Paint mTrapezoidPaint; private Paint mTrapezoidPaint;
private Paint mTextPaint;
private BatteryChartView.OnSelectListener mOnSelectListener;
@VisibleForTesting
Paint mTrapezoidCurvePaint = null;
@VisibleForTesting @VisibleForTesting
TrapezoidSlot[] mTrapezoidSlots; TrapezoidSlot[] mTrapezoidSlots;
// Records the location to calculate selected index. // Records the location to calculate selected index.
@VisibleForTesting @VisibleForTesting
float mTouchUpEventX = Float.MIN_VALUE; float mTouchUpEventX = Float.MIN_VALUE;
private BatteryChartView.OnSelectListener mOnSelectListener;
public BatteryChartView(Context context) { public BatteryChartView(Context context) {
super(context, null); super(context, null);
@@ -261,66 +236,6 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK); 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) { private void initializeTrapezoidSlots(int count) {
mTrapezoidSlots = new TrapezoidSlot[count]; mTrapezoidSlots = new TrapezoidSlot[count];
for (int index = 0; index < mTrapezoidSlots.length; index++) { 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++) { for (int index = 0; index < mTrapezoidSlots.length; index++) {
// Not draws the trapezoid for corner or not initialization cases. // Not draws the trapezoid for corner or not initialization cases.
if (!isValidToDraw(mViewModel, index)) { if (!isValidToDraw(mViewModel, index)) {
if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) {
canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint);
trapezoidCurvePath = null;
}
continue; continue;
} }
// Configures the trapezoid paint color. // Configures the trapezoid paint color.
final int trapezoidColor = mIsSlotsClickabled && (mViewModel.selectedIndex() == index final int trapezoidColor = (mViewModel.selectedIndex() == index
|| mViewModel.selectedIndex() == BatteryChartViewModel.SELECTED_INDEX_ALL) || mViewModel.selectedIndex() == BatteryChartViewModel.SELECTED_INDEX_ALL)
? mTrapezoidSolidColor : mTrapezoidColor; ? mTrapezoidSolidColor : mTrapezoidColor;
final boolean isHoverState = final boolean isHoverState = mHoveredIndex == index && isValidToDraw(mViewModel,
mIsSlotsClickabled && mHoveredIndex == index mHoveredIndex);
&& isValidToDraw(mViewModel, mHoveredIndex);
mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor); mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);
final float leftTop = round( final float leftTop = round(
@@ -574,22 +484,6 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop); trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
// Draws the trapezoid shape into canvas. // Draws the trapezoid shape into canvas.
canvas.drawPath(trapezoidPath, mTrapezoidPaint); 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)}; 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. // A container class for each trapezoid left and right location.
@VisibleForTesting @VisibleForTesting
static final class TrapezoidSlot { static final class TrapezoidSlot {

View File

@@ -17,17 +17,11 @@ package com.android.settings.fuelgauge.batteryusage;
import static com.google.common.truth.Truth.assertThat; 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.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.Context; import android.content.Context;
import android.os.LocaleList; import android.os.LocaleList;
import android.view.View; import android.view.View;
import android.view.accessibility.AccessibilityManager;
import com.android.settings.fuelgauge.PowerUsageFeatureProvider; import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.FakeFeatureFactory;
@@ -40,8 +34,6 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@@ -53,10 +45,6 @@ public final class BatteryChartViewTest {
private FakeFeatureFactory mFeatureFactory; private FakeFeatureFactory mFeatureFactory;
private PowerUsageFeatureProvider mPowerUsageFeatureProvider; private PowerUsageFeatureProvider mPowerUsageFeatureProvider;
@Mock
private AccessibilityServiceInfo mMockAccessibilityServiceInfo;
@Mock
private AccessibilityManager mMockAccessibilityManager;
@Mock @Mock
private View mMockView; private View mMockView;
@@ -69,34 +57,6 @@ public final class BatteryChartViewTest {
mContext.getResources().getConfiguration().setLocales( mContext.getResources().getConfiguration().setLocales(
new LocaleList(new Locale("en_US"))); new LocaleList(new Locale("en_US")));
mBatteryChartView = new BatteryChartView(mContext); 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 @Test
@@ -130,116 +90,4 @@ public final class BatteryChartViewTest {
mBatteryChartView.onClick(mMockView); mBatteryChartView.onClick(mMockView);
assertThat(selectedIndex[0]).isEqualTo(BatteryChartViewModel.SELECTED_INDEX_ALL); 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);
}
} }