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