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