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