diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java index fc6daf70c8b..40e3167c68b 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java @@ -28,14 +28,21 @@ import android.graphics.CornerPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; +import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.appcompat.widget.AppCompatImageView; @@ -77,6 +84,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick private Paint mDividerPaint; private Paint mTrapezoidPaint; private Paint mTextPaint; + private AccessibilityNodeProvider mAccessibilityNodeProvider; private BatteryChartView.OnSelectListener mOnSelectListener; @VisibleForTesting @@ -200,10 +208,23 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick if (mHoveredIndex != trapezoidIndex) { mHoveredIndex = trapezoidIndex; invalidate(); + sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); } - break; + // Ignore the super.onHoverEvent() because the hovered trapezoid has already been + // sent here. + return true; + case MotionEvent.ACTION_HOVER_EXIT: + if (mHoveredIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID) { + sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset + invalidate(); + } + // Ignore the super.onHoverEvent() because the hovered trapezoid has already been + // sent here. + return true; + default: + return super.onTouchEvent(event); } - return super.onHoverEvent(event); } @Override @@ -221,21 +242,53 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick Log.w(TAG, "invalid motion event for onClick() callback"); return; } - final int trapezoidIndex = getTrapezoidIndex(mTouchUpEventX); + onTrapezoidClicked(view, getTrapezoidIndex(mTouchUpEventX)); + } + + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + if (mViewModel == null) { + return super.getAccessibilityNodeProvider(); + } + if (mAccessibilityNodeProvider == null) { + mAccessibilityNodeProvider = new BatteryChartAccessibilityNodeProvider(); + } + return mAccessibilityNodeProvider; + } + + private void onTrapezoidClicked(View view, int index) { // Ignores the click event if the level is zero. - if (trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID - || !isValidToDraw(mViewModel, trapezoidIndex)) { + if (!isValidToDraw(mViewModel, index)) { return; } if (mOnSelectListener != null) { // Selects all if users click the same trapezoid item two times. mOnSelectListener.onSelect( - trapezoidIndex == mViewModel.selectedIndex() - ? BatteryChartViewModel.SELECTED_INDEX_ALL : trapezoidIndex); + index == mViewModel.selectedIndex() + ? BatteryChartViewModel.SELECTED_INDEX_ALL : index); } view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK); } + private boolean sendAccessibilityEvent(int virtualDescendantId, int eventType) { + ViewParent parent = getParent(); + if (parent == null || !AccessibilityManager.getInstance(mContext).isEnabled()) { + return false; + } + AccessibilityEvent accessibilityEvent = new AccessibilityEvent(eventType); + accessibilityEvent.setSource(this, virtualDescendantId); + accessibilityEvent.setEnabled(true); + accessibilityEvent.setClassName(getAccessibilityClassName()); + accessibilityEvent.setPackageName(getContext().getPackageName()); + return parent.requestSendAccessibilityEvent(this, accessibilityEvent); + } + + private void sendAccessibilityEventForHover(int eventType) { + if (isTrapezoidIndexValid(mViewModel, mHoveredIndex)) { + sendAccessibilityEvent(mHoveredIndex, eventType); + } + } + private void initializeTrapezoidSlots(int count) { mTrapezoidSlots = new TrapezoidSlot[count]; for (int index = 0; index < mTrapezoidSlots.length; index++) { @@ -515,10 +568,15 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick && viewModel.levels().get(trapezoidIndex + 1) != null; } - private static boolean isValidToDraw(BatteryChartViewModel viewModel, int trapezoidIndex) { + private static boolean isTrapezoidIndexValid( + @NonNull BatteryChartViewModel viewModel, int trapezoidIndex) { return viewModel != null && trapezoidIndex >= 0 - && trapezoidIndex < viewModel.size() - 1 + && trapezoidIndex < viewModel.size() - 1; + } + + private static boolean isValidToDraw(BatteryChartViewModel viewModel, int trapezoidIndex) { + return isTrapezoidIndexValid(viewModel, trapezoidIndex) && isTrapezoidValid(viewModel, trapezoidIndex); } @@ -539,6 +597,63 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick formatPercentage(/*percentage=*/ 0, /*round=*/ true)}; } + private class BatteryChartAccessibilityNodeProvider extends AccessibilityNodeProvider { + @Override + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) { + final AccessibilityNodeInfo hostInfo = + new AccessibilityNodeInfo(BatteryChartView.this); + for (int index = 0; index < mViewModel.size() - 1; index++) { + hostInfo.addChild(BatteryChartView.this, index); + } + return hostInfo; + } + final int index = virtualViewId; + if (!isTrapezoidIndexValid(mViewModel, index)) { + Log.w(TAG, "Invalid virtual view id:" + index); + return null; + } + final AccessibilityNodeInfo childInfo = + new AccessibilityNodeInfo(BatteryChartView.this, index); + onInitializeAccessibilityNodeInfo(childInfo); + childInfo.setClickable(isValidToDraw(mViewModel, index)); + childInfo.setText(mViewModel.texts().get(index)); + childInfo.setContentDescription(mViewModel.texts().get(index)); + + final Rect bounds = new Rect(); + getBoundsOnScreen(bounds, true); + final int hostLeft = bounds.left; + bounds.left = round(hostLeft + mTrapezoidSlots[index].mLeft); + bounds.right = round(hostLeft + mTrapezoidSlots[index].mRight); + childInfo.setBoundsInScreen(bounds); + return childInfo; + } + + @Override + public boolean performAction(int virtualViewId, int action, + @Nullable Bundle arguments) { + if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) { + return performAccessibilityAction(action, arguments); + } + switch (action) { + case AccessibilityNodeInfo.ACTION_CLICK: + onTrapezoidClicked(BatteryChartView.this, virtualViewId); + return true; + + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: + return sendAccessibilityEvent(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + return sendAccessibilityEvent(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + + default: + return performAccessibilityAction(action, arguments); + } + } + } + // A container class for each trapezoid left and right location. @VisibleForTesting static final class TrapezoidSlot {