1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 package com.android.settings.fuelgauge;
15 
16 import static java.lang.Math.round;
17 
18 import static com.android.settings.Utils.formatPercentage;
19 
20 import android.accessibilityservice.AccessibilityServiceInfo;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.CornerPathEffect;
26 import android.graphics.Paint;
27 import android.graphics.Path;
28 import android.graphics.Rect;
29 import android.os.Handler;
30 import android.text.format.DateFormat;
31 import android.text.format.DateUtils;
32 import android.util.AttributeSet;
33 import android.util.Log;
34 import android.view.HapticFeedbackConstants;
35 import android.view.MotionEvent;
36 import android.view.View;
37 import android.view.accessibility.AccessibilityManager;
38 import android.widget.TextView;
39 
40 import androidx.appcompat.widget.AppCompatImageView;
41 import androidx.annotation.VisibleForTesting;
42 
43 import com.android.settings.R;
44 import com.android.settings.overlay.FeatureFactory;
45 import com.android.settingslib.Utils;
46 
47 import java.time.Clock;
48 import java.util.Arrays;
49 import java.util.List;
50 import java.util.Locale;
51 
52 /** A widget component to draw chart graph. */
53 public class BatteryChartView extends AppCompatImageView implements View.OnClickListener,
54         AccessibilityManager.AccessibilityStateChangeListener {
55     private static final String TAG = "BatteryChartView";
56     private static final List<String> ACCESSIBILITY_SERVICE_NAMES =
57         Arrays.asList("SwitchAccessService", "TalkBackService", "JustSpeakService");
58 
59     private static final int DEFAULT_TRAPEZOID_COUNT = 12;
60     private static final int DEFAULT_TIMESTAMP_COUNT = 4;
61     private static final int TIMESTAMP_GAPS_COUNT = DEFAULT_TIMESTAMP_COUNT - 1;
62     private static final int DIVIDER_COLOR = Color.parseColor("#CDCCC5");
63     private static final long UPDATE_STATE_DELAYED_TIME = 500L;
64 
65     /** Selects all trapezoid shapes. */
66     public static final int SELECTED_INDEX_ALL = -1;
67     public static final int SELECTED_INDEX_INVALID = -2;
68 
69     /** A callback listener for selected group index is updated. */
70     public interface OnSelectListener {
onSelect(int trapezoidIndex)71         void onSelect(int trapezoidIndex);
72     }
73 
74     private int mDividerWidth;
75     private int mDividerHeight;
76     private int mTrapezoidCount;
77     private float mTrapezoidVOffset;
78     private float mTrapezoidHOffset;
79     private boolean mIsSlotsClickabled;
80     private String[] mPercentages = getPercentages();
81 
82     @VisibleForTesting int mHoveredIndex = SELECTED_INDEX_INVALID;
83     @VisibleForTesting int mSelectedIndex = SELECTED_INDEX_INVALID;
84     @VisibleForTesting String[] mTimestamps;
85 
86     // Colors for drawing the trapezoid shape and dividers.
87     private int mTrapezoidColor;
88     private int mTrapezoidSolidColor;
89     private int mTrapezoidHoverColor;
90     // For drawing the percentage information.
91     private int mTextPadding;
92     private final Rect mIndent = new Rect();
93     private final Rect[] mPercentageBounds =
94         new Rect[] {new Rect(), new Rect(), new Rect()};
95     // For drawing the timestamp information.
96     private final Rect[] mTimestampsBounds =
97         new Rect[] {new Rect(), new Rect(), new Rect(), new Rect()};
98 
99     @VisibleForTesting
100     Handler mHandler = new Handler();
101     @VisibleForTesting
102     final Runnable mUpdateClickableStateRun = () -> updateClickableState();
103 
104     private int[] mLevels;
105     private Paint mTextPaint;
106     private Paint mDividerPaint;
107     private Paint mTrapezoidPaint;
108 
109     @VisibleForTesting
110     Paint mTrapezoidCurvePaint = null;
111     private TrapezoidSlot[] mTrapezoidSlots;
112     // Records the location to calculate selected index.
113     private float mTouchUpEventX = Float.MIN_VALUE;
114     private BatteryChartView.OnSelectListener mOnSelectListener;
115 
BatteryChartView(Context context)116     public BatteryChartView(Context context) {
117         super(context, null);
118     }
119 
BatteryChartView(Context context, AttributeSet attrs)120     public BatteryChartView(Context context, AttributeSet attrs) {
121         super(context, attrs);
122         initializeColors(context);
123         // Registers the click event listener.
124         setOnClickListener(this);
125         setSelectedIndex(SELECTED_INDEX_ALL);
126         setTrapezoidCount(DEFAULT_TRAPEZOID_COUNT);
127         setClickable(false);
128         setLatestTimestamp(0);
129     }
130 
131     /** Sets the total trapezoid count for drawing. */
setTrapezoidCount(int trapezoidCount)132     public void setTrapezoidCount(int trapezoidCount) {
133         Log.i(TAG, "trapezoidCount:" + trapezoidCount);
134         mTrapezoidCount = trapezoidCount;
135         mTrapezoidSlots = new TrapezoidSlot[trapezoidCount];
136         // Allocates the trapezoid slot array.
137         for (int index = 0; index < trapezoidCount; index++) {
138             mTrapezoidSlots[index] = new TrapezoidSlot();
139         }
140         invalidate();
141     }
142 
143     /** Sets all levels value to draw the trapezoid shape */
setLevels(int[] levels)144     public void setLevels(int[] levels) {
145         Log.d(TAG, "setLevels() " + (levels == null ? "null" : levels.length));
146         if (levels == null) {
147             mLevels = null;
148             return;
149         }
150         // We should provide trapezoid count + 1 data to draw all trapezoids.
151         mLevels = levels.length == mTrapezoidCount + 1 ? levels : null;
152         setClickable(false);
153         invalidate();
154         if (mLevels == null) {
155             return;
156         }
157         // Sets the chart is clickable if there is at least one valid item in it.
158         for (int index = 0; index < mLevels.length - 1; index++) {
159             if (mLevels[index] != 0 && mLevels[index + 1] != 0) {
160                 setClickable(true);
161                 break;
162             }
163         }
164     }
165 
166     /** Sets the selected group index to draw highlight effect. */
setSelectedIndex(int index)167     public void setSelectedIndex(int index) {
168         if (mSelectedIndex != index) {
169             mSelectedIndex = index;
170             invalidate();
171             // Callbacks to the listener if we have.
172             if (mOnSelectListener != null) {
173                 mOnSelectListener.onSelect(mSelectedIndex);
174             }
175         }
176     }
177 
178     /** Sets the callback to monitor the selected group index. */
setOnSelectListener(BatteryChartView.OnSelectListener listener)179     public void setOnSelectListener(BatteryChartView.OnSelectListener listener) {
180         mOnSelectListener = listener;
181     }
182 
183     /** Sets the companion {@link TextView} for percentage information. */
setCompanionTextView(TextView textView)184     public void setCompanionTextView(TextView textView) {
185         if (textView != null) {
186             // Pre-draws the view first to load style atttributions into paint.
187             textView.draw(new Canvas());
188             mTextPaint = textView.getPaint();
189         } else {
190             mTextPaint = null;
191         }
192         setVisibility(View.VISIBLE);
193         requestLayout();
194     }
195 
196     /** Sets the latest timestamp for drawing into x-axis information. */
setLatestTimestamp(long latestTimestamp)197     public void setLatestTimestamp(long latestTimestamp) {
198         if (latestTimestamp == 0) {
199             latestTimestamp = Clock.systemUTC().millis();
200         }
201         if (mTimestamps == null) {
202             mTimestamps = new String[DEFAULT_TIMESTAMP_COUNT];
203         }
204         final long timeSlotOffset =
205             DateUtils.HOUR_IN_MILLIS * (/*total 24 hours*/ 24 / TIMESTAMP_GAPS_COUNT);
206         final boolean is24HourFormat = DateFormat.is24HourFormat(getContext());
207         for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
208             mTimestamps[index] =
209                 ConvertUtils.utcToLocalTimeHour(
210                     getContext(),
211                     latestTimestamp - (TIMESTAMP_GAPS_COUNT - index) * timeSlotOffset,
212                     is24HourFormat);
213         }
214         requestLayout();
215     }
216 
217     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)218     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
219         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
220         // Measures text bounds and updates indent configuration.
221         if (mTextPaint != null) {
222             for (int index = 0; index < mPercentages.length; index++) {
223                 mTextPaint.getTextBounds(
224                     mPercentages[index], 0, mPercentages[index].length(),
225                     mPercentageBounds[index]);
226             }
227             // Updates the indent configurations.
228             mIndent.top = mPercentageBounds[0].height();
229             mIndent.right = mPercentageBounds[0].width() + mTextPadding;
230 
231             if (mTimestamps != null) {
232                 int maxHeight = 0;
233                 for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
234                     mTextPaint.getTextBounds(
235                         mTimestamps[index], 0, mTimestamps[index].length(),
236                         mTimestampsBounds[index]);
237                     maxHeight = Math.max(maxHeight, mTimestampsBounds[index].height());
238                 }
239                 mIndent.bottom = maxHeight + round(mTextPadding * 1.5f);
240             }
241             Log.d(TAG, "setIndent:" + mPercentageBounds[0]);
242         } else {
243             mIndent.set(0, 0, 0, 0);
244         }
245     }
246 
247     @Override
draw(Canvas canvas)248     public void draw(Canvas canvas) {
249         super.draw(canvas);
250         drawHorizontalDividers(canvas);
251         drawVerticalDividers(canvas);
252         drawTrapezoids(canvas);
253     }
254 
255     @Override
onTouchEvent(MotionEvent event)256     public boolean onTouchEvent(MotionEvent event) {
257         // Caches the location to calculate selected trapezoid index.
258         final int action = event.getAction();
259         switch (action) {
260             case MotionEvent.ACTION_UP:
261                 mTouchUpEventX = event.getX();
262                 break;
263             case MotionEvent.ACTION_CANCEL:
264                 mTouchUpEventX = Float.MIN_VALUE; // reset
265                 break;
266         }
267         return super.onTouchEvent(event);
268     }
269 
270     @Override
onHoverEvent(MotionEvent event)271     public boolean onHoverEvent(MotionEvent event) {
272         final int action = event.getAction();
273         switch (action) {
274             case MotionEvent.ACTION_HOVER_ENTER:
275             case MotionEvent.ACTION_HOVER_MOVE:
276                 final int trapezoidIndex = getTrapezoidIndex(event.getX());
277                 if (mHoveredIndex != trapezoidIndex) {
278                     mHoveredIndex = trapezoidIndex;
279                     invalidate();
280                 }
281                 break;
282         }
283         return super.onHoverEvent(event);
284     }
285 
286     @Override
onHoverChanged(boolean hovered)287     public void onHoverChanged(boolean hovered) {
288         super.onHoverChanged(hovered);
289         if (!hovered) {
290             mHoveredIndex = SELECTED_INDEX_INVALID; // reset
291             invalidate();
292         }
293     }
294 
295     @Override
onClick(View view)296     public void onClick(View view) {
297         if (mTouchUpEventX == Float.MIN_VALUE) {
298             Log.w(TAG, "invalid motion event for onClick() callback");
299             return;
300         }
301         final int trapezoidIndex = getTrapezoidIndex(mTouchUpEventX);
302         // Ignores the click event if the level is zero.
303         if (trapezoidIndex == SELECTED_INDEX_INVALID
304                 || !isValidToDraw(trapezoidIndex)) {
305             return;
306         }
307         // Selects all if users click the same trapezoid item two times.
308         if (trapezoidIndex == mSelectedIndex) {
309             setSelectedIndex(SELECTED_INDEX_ALL);
310         } else {
311             setSelectedIndex(trapezoidIndex);
312         }
313         view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
314     }
315 
316     @Override
onAttachedToWindow()317     public void onAttachedToWindow() {
318         super.onAttachedToWindow();
319         updateClickableState();
320         mContext.getSystemService(AccessibilityManager.class)
321             .addAccessibilityStateChangeListener(/*listener=*/ this);
322     }
323 
324     @Override
onDetachedFromWindow()325     public void onDetachedFromWindow() {
326         super.onDetachedFromWindow();
327         mContext.getSystemService(AccessibilityManager.class)
328             .removeAccessibilityStateChangeListener(/*listener=*/ this);
329         mHandler.removeCallbacks(mUpdateClickableStateRun);
330     }
331 
332     @Override
onAccessibilityStateChanged(boolean enabled)333     public void onAccessibilityStateChanged(boolean enabled) {
334         Log.d(TAG, "onAccessibilityStateChanged:" + enabled);
335         mHandler.removeCallbacks(mUpdateClickableStateRun);
336         // We should delay it a while since accessibility manager will spend
337         // some times to bind with new enabled accessibility services.
338         mHandler.postDelayed(
339             mUpdateClickableStateRun, UPDATE_STATE_DELAYED_TIME);
340     }
341 
updateClickableState()342     private void updateClickableState() {
343         final Context context = mContext;
344         mIsSlotsClickabled =
345             FeatureFactory.getFactory(context)
346                     .getPowerUsageFeatureProvider(context)
347                     .isChartGraphSlotsEnabled(context)
348             && !isAccessibilityEnabled(context);
349         Log.d(TAG, "isChartGraphSlotsEnabled:" + mIsSlotsClickabled);
350         setClickable(isClickable());
351         // Initializes the trapezoid curve paint for non-clickable case.
352         if (!mIsSlotsClickabled && mTrapezoidCurvePaint == null) {
353             mTrapezoidCurvePaint = new Paint();
354             mTrapezoidCurvePaint.setAntiAlias(true);
355             mTrapezoidCurvePaint.setColor(mTrapezoidSolidColor);
356             mTrapezoidCurvePaint.setStyle(Paint.Style.STROKE);
357             mTrapezoidCurvePaint.setStrokeWidth(mDividerWidth * 2);
358         } else if (mIsSlotsClickabled) {
359             mTrapezoidCurvePaint = null;
360             // Sets levels again to force update the click state.
361             setLevels(mLevels);
362         }
363         invalidate();
364     }
365 
366     @Override
setClickable(boolean clickable)367     public void setClickable(boolean clickable) {
368         super.setClickable(mIsSlotsClickabled && clickable);
369     }
370 
371     @VisibleForTesting
setClickableForce(boolean clickable)372     void setClickableForce(boolean clickable) {
373         super.setClickable(clickable);
374     }
375 
initializeColors(Context context)376     private void initializeColors(Context context) {
377         setBackgroundColor(Color.TRANSPARENT);
378         mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
379         mTrapezoidColor = Utils.getDisabled(context, mTrapezoidSolidColor);
380         mTrapezoidHoverColor = Utils.getColorAttrDefaultColor(context,
381             com.android.internal.R.attr.colorAccentSecondaryVariant);
382         // Initializes the divider line paint.
383         final Resources resources = getContext().getResources();
384         mDividerWidth = resources.getDimensionPixelSize(R.dimen.chartview_divider_width);
385         mDividerHeight = resources.getDimensionPixelSize(R.dimen.chartview_divider_height);
386         mDividerPaint = new Paint();
387         mDividerPaint.setAntiAlias(true);
388         mDividerPaint.setColor(DIVIDER_COLOR);
389         mDividerPaint.setStyle(Paint.Style.STROKE);
390         mDividerPaint.setStrokeWidth(mDividerWidth);
391         Log.i(TAG, "mDividerWidth:" + mDividerWidth);
392         Log.i(TAG, "mDividerHeight:" + mDividerHeight);
393         // Initializes the trapezoid paint.
394         mTrapezoidHOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_start);
395         mTrapezoidVOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_bottom);
396         mTrapezoidPaint = new Paint();
397         mTrapezoidPaint.setAntiAlias(true);
398         mTrapezoidPaint.setColor(mTrapezoidSolidColor);
399         mTrapezoidPaint.setStyle(Paint.Style.FILL);
400         mTrapezoidPaint.setPathEffect(
401             new CornerPathEffect(
402                 resources.getDimensionPixelSize(R.dimen.chartview_trapezoid_radius)));
403         // Initializes for drawing text information.
404         mTextPadding = resources.getDimensionPixelSize(R.dimen.chartview_text_padding);
405     }
406 
drawHorizontalDividers(Canvas canvas)407     private void drawHorizontalDividers(Canvas canvas) {
408         final int width = getWidth() - mIndent.right;
409         final int height = getHeight() - mIndent.top - mIndent.bottom;
410         // Draws the top divider line for 100% curve.
411         float offsetY = mIndent.top + mDividerWidth * .5f;
412         canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
413         drawPercentage(canvas, /*index=*/ 0, offsetY);
414 
415         // Draws the center divider line for 50% curve.
416         final float availableSpace =
417             height - mDividerWidth * 2 - mTrapezoidVOffset - mDividerHeight;
418         offsetY = mIndent.top + mDividerWidth + availableSpace * .5f;
419         canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
420         drawPercentage(canvas, /*index=*/ 1, offsetY);
421 
422         // Draws the bottom divider line for 0% curve.
423         offsetY = mIndent.top + (height - mDividerHeight - mDividerWidth * .5f);
424         canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
425         drawPercentage(canvas, /*index=*/ 2, offsetY);
426     }
427 
drawPercentage(Canvas canvas, int index, float offsetY)428     private void drawPercentage(Canvas canvas, int index, float offsetY) {
429         if (mTextPaint != null) {
430             canvas.drawText(
431                 mPercentages[index],
432                 getWidth() - mPercentageBounds[index].width() - mPercentageBounds[index].left,
433                 offsetY + mPercentageBounds[index].height() *.5f,
434                 mTextPaint);
435         }
436     }
437 
drawVerticalDividers(Canvas canvas)438     private void drawVerticalDividers(Canvas canvas) {
439         final int width = getWidth() - mIndent.right;
440         final int dividerCount = mTrapezoidCount + 1;
441         final float dividerSpace = dividerCount * mDividerWidth;
442         final float unitWidth = (width - dividerSpace) / (float) mTrapezoidCount;
443         final float bottomY = getHeight() - mIndent.bottom;
444         final float startY = bottomY - mDividerHeight;
445         final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * .5f;
446         // Draws each vertical dividers.
447         float startX = mDividerWidth * .5f;
448         for (int index = 0; index < dividerCount; index++) {
449             canvas.drawLine(startX, startY, startX, bottomY, mDividerPaint);
450             final float nextX = startX + mDividerWidth + unitWidth;
451             // Updates the trapezoid slots for drawing.
452             if (index < mTrapezoidSlots.length) {
453                 mTrapezoidSlots[index].mLeft = round(startX + trapezoidSlotOffset);
454                 mTrapezoidSlots[index].mRight = round(nextX - trapezoidSlotOffset);
455             }
456             startX = nextX;
457         }
458         // Draws the timestamp slot information.
459         if (mTimestamps != null) {
460             final float[] xOffsets = new float[DEFAULT_TIMESTAMP_COUNT];
461             final float baselineX = mDividerWidth * .5f;
462             final float offsetX = mDividerWidth + unitWidth;
463             final int slotBarOffset = (/*total 12 bars*/ 12) / TIMESTAMP_GAPS_COUNT;
464             for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
465                 xOffsets[index] = baselineX + index * offsetX * slotBarOffset;
466             }
467             drawTimestamp(canvas, xOffsets);
468         }
469     }
470 
drawTimestamp(Canvas canvas, float[] xOffsets)471     private void drawTimestamp(Canvas canvas, float[] xOffsets) {
472         // Draws the 1st timestamp info.
473         canvas.drawText(
474             mTimestamps[0],
475             xOffsets[0] - mTimestampsBounds[0].left,
476             getTimestampY(0), mTextPaint);
477         final int latestIndex = DEFAULT_TIMESTAMP_COUNT - 1;
478         // Draws the last timestamp info.
479         canvas.drawText(
480             mTimestamps[latestIndex],
481             xOffsets[latestIndex] - mTimestampsBounds[latestIndex].width()
482                     - mTimestampsBounds[latestIndex].left,
483             getTimestampY(latestIndex), mTextPaint);
484         // Draws the rest of timestamp info since it is located in the center.
485         for (int index = 1; index <= DEFAULT_TIMESTAMP_COUNT - 2; index++) {
486             canvas.drawText(
487                 mTimestamps[index],
488                 xOffsets[index] -
489                     (mTimestampsBounds[index].width() - mTimestampsBounds[index].left) * .5f,
490                 getTimestampY(index), mTextPaint);
491 
492         }
493     }
494 
getTimestampY(int index)495     private int getTimestampY(int index) {
496         return getHeight() - mTimestampsBounds[index].height()
497             + (mTimestampsBounds[index].height() + mTimestampsBounds[index].top)
498             + round(mTextPadding * 1.5f);
499     }
500 
drawTrapezoids(Canvas canvas)501     private void drawTrapezoids(Canvas canvas) {
502         // Ignores invalid trapezoid data.
503         if (mLevels == null) {
504             return;
505         }
506         final float trapezoidBottom =
507             getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth
508                 - mTrapezoidVOffset;
509         final float availableSpace = trapezoidBottom - mDividerWidth * .5f - mIndent.top;
510         final float unitHeight = availableSpace / 100f;
511         // Draws all trapezoid shapes into the canvas.
512         final Path trapezoidPath = new Path();
513         Path trapezoidCurvePath = null;
514         for (int index = 0; index < mTrapezoidCount; index++) {
515             // Not draws the trapezoid for corner or not initialization cases.
516             if (!isValidToDraw(index)) {
517                 if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) {
518                     canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint);
519                     trapezoidCurvePath = null;
520                 }
521                 continue;
522             }
523             // Configures the trapezoid paint color.
524             final int trapezoidColor =
525                 !mIsSlotsClickabled
526                     ? mTrapezoidColor
527                     : mSelectedIndex == index || mSelectedIndex == SELECTED_INDEX_ALL
528                         ? mTrapezoidSolidColor : mTrapezoidColor;
529             final boolean isHoverState =
530                 mIsSlotsClickabled && mHoveredIndex == index && isValidToDraw(mHoveredIndex);
531             mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);
532 
533             final float leftTop = round(trapezoidBottom - mLevels[index] * unitHeight);
534             final float rightTop = round(trapezoidBottom - mLevels[index + 1] * unitHeight);
535             trapezoidPath.reset();
536             trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
537             trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
538             trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, rightTop);
539             trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, trapezoidBottom);
540             // A tricky way to make the trapezoid shape drawing the rounded corner.
541             trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
542             trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
543             // Draws the trapezoid shape into canvas.
544             canvas.drawPath(trapezoidPath, mTrapezoidPaint);
545 
546             // Generates path for non-clickable trapezoid curve.
547             if (mTrapezoidCurvePaint != null) {
548                 if (trapezoidCurvePath == null) {
549                     trapezoidCurvePath= new Path();
550                     trapezoidCurvePath.moveTo(mTrapezoidSlots[index].mLeft, leftTop);
551                 } else {
552                     trapezoidCurvePath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
553                 }
554                 trapezoidCurvePath.lineTo(mTrapezoidSlots[index].mRight, rightTop);
555             }
556         }
557         // Draws the trapezoid curve for non-clickable case.
558         if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) {
559             canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint);
560             trapezoidCurvePath = null;
561         }
562     }
563 
564     // Searches the corresponding trapezoid index from x location.
getTrapezoidIndex(float x)565     private int getTrapezoidIndex(float x) {
566         for (int index = 0; index < mTrapezoidSlots.length; index++) {
567             final TrapezoidSlot slot = mTrapezoidSlots[index];
568             if (x >= slot.mLeft - mTrapezoidHOffset
569                     && x <= slot.mRight + mTrapezoidHOffset) {
570                 return index;
571             }
572         }
573         return SELECTED_INDEX_INVALID;
574     }
575 
isValidToDraw(int trapezoidIndex)576     private boolean isValidToDraw(int trapezoidIndex) {
577         return mLevels != null
578                 && trapezoidIndex >= 0
579                 && trapezoidIndex < mLevels.length - 1
580                 && mLevels[trapezoidIndex] != 0
581                 && mLevels[trapezoidIndex + 1] != 0;
582     }
583 
getPercentages()584     private static String[] getPercentages() {
585         return new String[] {
586             formatPercentage(/*percentage=*/ 100, /*round=*/ true),
587             formatPercentage(/*percentage=*/ 50, /*round=*/ true),
588             formatPercentage(/*percentage=*/ 0, /*round=*/ true)};
589     }
590 
591     @VisibleForTesting
isAccessibilityEnabled(Context context)592     static boolean isAccessibilityEnabled(Context context) {
593         final AccessibilityManager accessibilityManager =
594             context.getSystemService(AccessibilityManager.class);
595         if (!accessibilityManager.isEnabled()) {
596             return false;
597         }
598         final List<AccessibilityServiceInfo> serviceInfoList =
599             accessibilityManager.getEnabledAccessibilityServiceList(
600                 AccessibilityServiceInfo.FEEDBACK_SPOKEN
601                     | AccessibilityServiceInfo.FEEDBACK_GENERIC);
602         for (AccessibilityServiceInfo info : serviceInfoList) {
603             for (String serviceName : ACCESSIBILITY_SERVICE_NAMES) {
604                 final String serviceId = info.getId();
605                 if (serviceId != null && serviceId.contains(serviceName)) {
606                     Log.d(TAG, "acccessibilityEnabled:" + serviceId);
607                     return true;
608                 }
609             }
610         }
611         return false;
612     }
613 
614     // A container class for each trapezoid left and right location.
615     private static final class TrapezoidSlot {
616         public float mLeft;
617         public float mRight;
618 
619         @Override
toString()620         public String toString() {
621             return String.format(Locale.US, "TrapezoidSlot[%f,%f]", mLeft, mRight);
622         }
623     }
624 }
625