Support accessibility for battery chart (2)

Support virtual accessbility children in battery usage chart.

Please see the following screen record with sound:
VoiceAccess by name: https://drive.google.com/file/d/15pEEU0OJsyCRbqR4nkALlIgHue4KVEvL/view?usp=sharing&resourcekey=0-ed-u-IWSDlODiYyJIEmVng
VoiceAccess by number: https://drive.google.com/file/d/1mBNjbpPGsw4nYU_krG8283RVPaYGZMO3/view?usp=sharing&resourcekey=0-3aIhbcCzJuEpsbDkaPAcWg
SwitchAccess: https://drive.google.com/file/d/1rr8sHMGCbP0kglsp7rWwOVQV5kcgEZHa/view?usp=sharing&resourcekey=0-GW2525dHtzDWvzS2uhu8Yg
TalkBack: https://drive.google.com/file/d/1daxwHQE3BwySuSIptvO9wCJwnjVehsLE/view?usp=sharing&resourcekey=0-DWo0TuhAfz_9Qaf9_orIWA
MouseConnected: https://drive.google.com/file/d/1DzJq5tJsNneNsRbRIZptXfK1l_wR0Kdz/view?usp=sharing&resourcekey=0-npq7ekR1glpofEKMRcJzFQ

The following is the orignal broken behaviors:
Original VoiceAccess: https://drive.google.com/file/d/1FtQJoVVWnq2xZyUaxW5_h1o0y7jTm9zd/view?usp=sharing&resourcekey=0-BVfk0nzpC2RSx9vGKmfogQ
Original TalkBack: https://drive.google.com/file/d/1jMuDo8Lu0uGRSm3OWVBCbm7lXVJnpMn4/view?usp=sharing&resourcekey=0-ozUs4bN14fMPrbvHUtogpw

Bug: 242989585
Fix: 242989585
Test: manual
Change-Id: I18fe63f75c2438e80b244050608a7ccb2b52c37b
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java
index fc6daf7..40e3167 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.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 @@
     private Paint mDividerPaint;
     private Paint mTrapezoidPaint;
     private Paint mTextPaint;
+    private AccessibilityNodeProvider mAccessibilityNodeProvider;
     private BatteryChartView.OnSelectListener mOnSelectListener;
 
     @VisibleForTesting
@@ -200,10 +208,23 @@
                 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 @@
             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 @@
                 && 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 @@
                 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 {