/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
package com.android.settings.fuelgauge;

import static java.lang.Math.round;

import static com.android.settings.Utils.formatPercentage;

import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.os.Handler;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
import android.widget.TextView;

import androidx.appcompat.widget.AppCompatImageView;
import androidx.annotation.VisibleForTesting;

import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.Utils;

import java.time.Clock;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

/** A widget component to draw chart graph. */
public class BatteryChartView extends AppCompatImageView implements View.OnClickListener,
        AccessibilityManager.AccessibilityStateChangeListener {
    private static final String TAG = "BatteryChartView";
    private static final List<String> ACCESSIBILITY_SERVICE_NAMES =
        Arrays.asList("SwitchAccessService", "TalkBackService", "JustSpeakService");

    private static final int DEFAULT_TRAPEZOID_COUNT = 12;
    private static final int DEFAULT_TIMESTAMP_COUNT = 4;
    private static final int TIMESTAMP_GAPS_COUNT = DEFAULT_TIMESTAMP_COUNT - 1;
    private static final int DIVIDER_COLOR = Color.parseColor("#CDCCC5");
    private static final long UPDATE_STATE_DELAYED_TIME = 500L;

    /** Selects all trapezoid shapes. */
    public static final int SELECTED_INDEX_ALL = -1;
    public static final int SELECTED_INDEX_INVALID = -2;

    /** A callback listener for selected group index is updated. */
    public interface OnSelectListener {
        void onSelect(int trapezoidIndex);
    }

    private int mDividerWidth;
    private int mDividerHeight;
    private int mTrapezoidCount;
    private float mTrapezoidVOffset;
    private float mTrapezoidHOffset;
    private boolean mIsSlotsClickabled;
    private String[] mPercentages = getPercentages();

    @VisibleForTesting int mHoveredIndex = SELECTED_INDEX_INVALID;
    @VisibleForTesting int mSelectedIndex = SELECTED_INDEX_INVALID;
    @VisibleForTesting String[] mTimestamps;

    // Colors for drawing the trapezoid shape and dividers.
    private int mTrapezoidColor;
    private int mTrapezoidSolidColor;
    private int mTrapezoidHoverColor;
    // For drawing the percentage information.
    private int mTextPadding;
    private final Rect mIndent = new Rect();
    private final Rect[] mPercentageBounds =
        new Rect[] {new Rect(), new Rect(), new Rect()};
    // For drawing the timestamp information.
    private final Rect[] mTimestampsBounds =
        new Rect[] {new Rect(), new Rect(), new Rect(), new Rect()};

    @VisibleForTesting
    Handler mHandler = new Handler();
    @VisibleForTesting
    final Runnable mUpdateClickableStateRun = () -> updateClickableState();

    private int[] mLevels;
    private Paint mTextPaint;
    private Paint mDividerPaint;
    private Paint mTrapezoidPaint;

    @VisibleForTesting
    Paint mTrapezoidCurvePaint = null;
    private TrapezoidSlot[] mTrapezoidSlots;
    // Records the location to calculate selected index.
    private float mTouchUpEventX = Float.MIN_VALUE;
    private BatteryChartView.OnSelectListener mOnSelectListener;

    public BatteryChartView(Context context) {
        super(context, null);
    }

    public BatteryChartView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initializeColors(context);
        // Registers the click event listener.
        setOnClickListener(this);
        setSelectedIndex(SELECTED_INDEX_ALL);
        setTrapezoidCount(DEFAULT_TRAPEZOID_COUNT);
        setClickable(false);
        setLatestTimestamp(0);
    }

    /** Sets the total trapezoid count for drawing. */
    public void setTrapezoidCount(int trapezoidCount) {
        Log.i(TAG, "trapezoidCount:" + trapezoidCount);
        mTrapezoidCount = trapezoidCount;
        mTrapezoidSlots = new TrapezoidSlot[trapezoidCount];
        // Allocates the trapezoid slot array.
        for (int index = 0; index < trapezoidCount; index++) {
            mTrapezoidSlots[index] = new TrapezoidSlot();
        }
        invalidate();
    }

    /** Sets all levels value to draw the trapezoid shape */
    public void setLevels(int[] levels) {
        Log.d(TAG, "setLevels() " + (levels == null ? "null" : levels.length));
        if (levels == null) {
            mLevels = null;
            return;
        }
        // We should provide trapezoid count + 1 data to draw all trapezoids.
        mLevels = levels.length == mTrapezoidCount + 1 ? levels : null;
        setClickable(false);
        invalidate();
        if (mLevels == null) {
            return;
        }
        // Sets the chart is clickable if there is at least one valid item in it.
        for (int index = 0; index < mLevels.length - 1; index++) {
            if (mLevels[index] != 0 && mLevels[index + 1] != 0) {
                setClickable(true);
                break;
            }
        }
    }

    /** Sets the selected group index to draw highlight effect. */
    public void setSelectedIndex(int index) {
        if (mSelectedIndex != index) {
            mSelectedIndex = index;
            invalidate();
            // Callbacks to the listener if we have.
            if (mOnSelectListener != null) {
                mOnSelectListener.onSelect(mSelectedIndex);
            }
        }
    }

    /** Sets the callback to monitor the selected group index. */
    public void setOnSelectListener(BatteryChartView.OnSelectListener listener) {
        mOnSelectListener = listener;
    }

    /** Sets the companion {@link TextView} for percentage information. */
    public void setCompanionTextView(TextView textView) {
        if (textView != null) {
            // Pre-draws the view first to load style atttributions into paint.
            textView.draw(new Canvas());
            mTextPaint = textView.getPaint();
        } else {
            mTextPaint = null;
        }
        setVisibility(View.VISIBLE);
        requestLayout();
    }

    /** Sets the latest timestamp for drawing into x-axis information. */
    public void setLatestTimestamp(long latestTimestamp) {
        if (latestTimestamp == 0) {
            latestTimestamp = Clock.systemUTC().millis();
        }
        if (mTimestamps == null) {
            mTimestamps = new String[DEFAULT_TIMESTAMP_COUNT];
        }
        final long timeSlotOffset =
            DateUtils.HOUR_IN_MILLIS * (/*total 24 hours*/ 24 / TIMESTAMP_GAPS_COUNT);
        final boolean is24HourFormat = DateFormat.is24HourFormat(getContext());
        for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
            mTimestamps[index] =
                ConvertUtils.utcToLocalTimeHour(
                    getContext(),
                    latestTimestamp - (TIMESTAMP_GAPS_COUNT - index) * timeSlotOffset,
                    is24HourFormat);
        }
        requestLayout();
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // Measures text bounds and updates indent configuration.
        if (mTextPaint != null) {
            for (int index = 0; index < mPercentages.length; index++) {
                mTextPaint.getTextBounds(
                    mPercentages[index], 0, mPercentages[index].length(),
                    mPercentageBounds[index]);
            }
            // Updates the indent configurations.
            mIndent.top = mPercentageBounds[0].height();
            mIndent.right = mPercentageBounds[0].width() + mTextPadding;

            if (mTimestamps != null) {
                int maxHeight = 0;
                for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
                    mTextPaint.getTextBounds(
                        mTimestamps[index], 0, mTimestamps[index].length(),
                        mTimestampsBounds[index]);
                    maxHeight = Math.max(maxHeight, mTimestampsBounds[index].height());
                }
                mIndent.bottom = maxHeight + round(mTextPadding * 1.5f);
            }
            Log.d(TAG, "setIndent:" + mPercentageBounds[0]);
        } else {
            mIndent.set(0, 0, 0, 0);
        }
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        drawHorizontalDividers(canvas);
        drawVerticalDividers(canvas);
        drawTrapezoids(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Caches the location to calculate selected trapezoid index.
        final int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_UP:
                mTouchUpEventX = event.getX();
                break;
            case MotionEvent.ACTION_CANCEL:
                mTouchUpEventX = Float.MIN_VALUE; // reset
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean onHoverEvent(MotionEvent event) {
        final int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_HOVER_ENTER:
            case MotionEvent.ACTION_HOVER_MOVE:
                final int trapezoidIndex = getTrapezoidIndex(event.getX());
                if (mHoveredIndex != trapezoidIndex) {
                    mHoveredIndex = trapezoidIndex;
                    invalidate();
                }
                break;
        }
        return super.onHoverEvent(event);
    }

    @Override
    public void onHoverChanged(boolean hovered) {
        super.onHoverChanged(hovered);
        if (!hovered) {
            mHoveredIndex = SELECTED_INDEX_INVALID; // reset
            invalidate();
        }
    }

    @Override
    public void onClick(View view) {
        if (mTouchUpEventX == Float.MIN_VALUE) {
            Log.w(TAG, "invalid motion event for onClick() callback");
            return;
        }
        final int trapezoidIndex = getTrapezoidIndex(mTouchUpEventX);
        // Ignores the click event if the level is zero.
        if (trapezoidIndex == SELECTED_INDEX_INVALID
                || !isValidToDraw(trapezoidIndex)) {
            return;
        }
        // Selects all if users click the same trapezoid item two times.
        if (trapezoidIndex == mSelectedIndex) {
            setSelectedIndex(SELECTED_INDEX_ALL);
        } else {
            setSelectedIndex(trapezoidIndex);
        }
        view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        updateClickableState();
        mContext.getSystemService(AccessibilityManager.class)
            .addAccessibilityStateChangeListener(/*listener=*/ this);
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mContext.getSystemService(AccessibilityManager.class)
            .removeAccessibilityStateChangeListener(/*listener=*/ this);
        mHandler.removeCallbacks(mUpdateClickableStateRun);
    }

    @Override
    public void onAccessibilityStateChanged(boolean enabled) {
        Log.d(TAG, "onAccessibilityStateChanged:" + enabled);
        mHandler.removeCallbacks(mUpdateClickableStateRun);
        // We should delay it a while since accessibility manager will spend
        // some times to bind with new enabled accessibility services.
        mHandler.postDelayed(
            mUpdateClickableStateRun, UPDATE_STATE_DELAYED_TIME);
    }

    private void updateClickableState() {
        final Context context = mContext;
        mIsSlotsClickabled =
            FeatureFactory.getFactory(context)
                    .getPowerUsageFeatureProvider(context)
                    .isChartGraphSlotsEnabled(context)
            && !isAccessibilityEnabled(context);
        Log.d(TAG, "isChartGraphSlotsEnabled:" + mIsSlotsClickabled);
        setClickable(isClickable());
        // Initializes the trapezoid curve paint for non-clickable case.
        if (!mIsSlotsClickabled && mTrapezoidCurvePaint == null) {
            mTrapezoidCurvePaint = new Paint();
            mTrapezoidCurvePaint.setAntiAlias(true);
            mTrapezoidCurvePaint.setColor(mTrapezoidSolidColor);
            mTrapezoidCurvePaint.setStyle(Paint.Style.STROKE);
            mTrapezoidCurvePaint.setStrokeWidth(mDividerWidth * 2);
        } else if (mIsSlotsClickabled) {
            mTrapezoidCurvePaint = null;
            // Sets levels again to force update the click state.
            setLevels(mLevels);
        }
        invalidate();
    }

    @Override
    public void setClickable(boolean clickable) {
        super.setClickable(mIsSlotsClickabled && clickable);
    }

    @VisibleForTesting
    void setClickableForce(boolean clickable) {
        super.setClickable(clickable);
    }

    private void initializeColors(Context context) {
        setBackgroundColor(Color.TRANSPARENT);
        mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
        mTrapezoidColor = Utils.getDisabled(context, mTrapezoidSolidColor);
        mTrapezoidHoverColor = Utils.getColorAttrDefaultColor(context,
            com.android.internal.R.attr.colorAccentSecondaryVariant);
        // Initializes the divider line paint.
        final Resources resources = getContext().getResources();
        mDividerWidth = resources.getDimensionPixelSize(R.dimen.chartview_divider_width);
        mDividerHeight = resources.getDimensionPixelSize(R.dimen.chartview_divider_height);
        mDividerPaint = new Paint();
        mDividerPaint.setAntiAlias(true);
        mDividerPaint.setColor(DIVIDER_COLOR);
        mDividerPaint.setStyle(Paint.Style.STROKE);
        mDividerPaint.setStrokeWidth(mDividerWidth);
        Log.i(TAG, "mDividerWidth:" + mDividerWidth);
        Log.i(TAG, "mDividerHeight:" + mDividerHeight);
        // Initializes the trapezoid paint.
        mTrapezoidHOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_start);
        mTrapezoidVOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_bottom);
        mTrapezoidPaint = new Paint();
        mTrapezoidPaint.setAntiAlias(true);
        mTrapezoidPaint.setColor(mTrapezoidSolidColor);
        mTrapezoidPaint.setStyle(Paint.Style.FILL);
        mTrapezoidPaint.setPathEffect(
            new CornerPathEffect(
                resources.getDimensionPixelSize(R.dimen.chartview_trapezoid_radius)));
        // Initializes for drawing text information.
        mTextPadding = resources.getDimensionPixelSize(R.dimen.chartview_text_padding);
    }

    private void drawHorizontalDividers(Canvas canvas) {
        final int width = getWidth() - mIndent.right;
        final int height = getHeight() - mIndent.top - mIndent.bottom;
        // Draws the top divider line for 100% curve.
        float offsetY = mIndent.top + mDividerWidth * .5f;
        canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
        drawPercentage(canvas, /*index=*/ 0, offsetY);

        // Draws the center divider line for 50% curve.
        final float availableSpace =
            height - mDividerWidth * 2 - mTrapezoidVOffset - mDividerHeight;
        offsetY = mIndent.top + mDividerWidth + availableSpace * .5f;
        canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
        drawPercentage(canvas, /*index=*/ 1, offsetY);

        // Draws the bottom divider line for 0% curve.
        offsetY = mIndent.top + (height - mDividerHeight - mDividerWidth * .5f);
        canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
        drawPercentage(canvas, /*index=*/ 2, offsetY);
    }

    private void drawPercentage(Canvas canvas, int index, float offsetY) {
        if (mTextPaint != null) {
            canvas.drawText(
                mPercentages[index],
                getWidth() - mPercentageBounds[index].width() - mPercentageBounds[index].left,
                offsetY + mPercentageBounds[index].height() *.5f,
                mTextPaint);
        }
    }

    private void drawVerticalDividers(Canvas canvas) {
        final int width = getWidth() - mIndent.right;
        final int dividerCount = mTrapezoidCount + 1;
        final float dividerSpace = dividerCount * mDividerWidth;
        final float unitWidth = (width - dividerSpace) / (float) mTrapezoidCount;
        final float bottomY = getHeight() - mIndent.bottom;
        final float startY = bottomY - mDividerHeight;
        final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * .5f;
        // Draws each vertical dividers.
        float startX = mDividerWidth * .5f;
        for (int index = 0; index < dividerCount; index++) {
            canvas.drawLine(startX, startY, startX, bottomY, mDividerPaint);
            final float nextX = startX + mDividerWidth + unitWidth;
            // Updates the trapezoid slots for drawing.
            if (index < mTrapezoidSlots.length) {
                mTrapezoidSlots[index].mLeft = round(startX + trapezoidSlotOffset);
                mTrapezoidSlots[index].mRight = round(nextX - trapezoidSlotOffset);
            }
            startX = nextX;
        }
        // Draws the timestamp slot information.
        if (mTimestamps != null) {
            final float[] xOffsets = new float[DEFAULT_TIMESTAMP_COUNT];
            final float baselineX = mDividerWidth * .5f;
            final float offsetX = mDividerWidth + unitWidth;
            final int slotBarOffset = (/*total 12 bars*/ 12) / TIMESTAMP_GAPS_COUNT;
            for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
                xOffsets[index] = baselineX + index * offsetX * slotBarOffset;
            }
            drawTimestamp(canvas, xOffsets);
        }
    }

    private void drawTimestamp(Canvas canvas, float[] xOffsets) {
        // Draws the 1st timestamp info.
        canvas.drawText(
            mTimestamps[0],
            xOffsets[0] - mTimestampsBounds[0].left,
            getTimestampY(0), mTextPaint);
        final int latestIndex = DEFAULT_TIMESTAMP_COUNT - 1;
        // Draws the last timestamp info.
        canvas.drawText(
            mTimestamps[latestIndex],
            xOffsets[latestIndex] - mTimestampsBounds[latestIndex].width()
                    - mTimestampsBounds[latestIndex].left,
            getTimestampY(latestIndex), mTextPaint);
        // Draws the rest of timestamp info since it is located in the center.
        for (int index = 1; index <= DEFAULT_TIMESTAMP_COUNT - 2; index++) {
            canvas.drawText(
                mTimestamps[index],
                xOffsets[index] -
                    (mTimestampsBounds[index].width() - mTimestampsBounds[index].left) * .5f,
                getTimestampY(index), mTextPaint);

        }
    }

    private int getTimestampY(int index) {
        return getHeight() - mTimestampsBounds[index].height()
            + (mTimestampsBounds[index].height() + mTimestampsBounds[index].top)
            + round(mTextPadding * 1.5f);
    }

    private void drawTrapezoids(Canvas canvas) {
        // Ignores invalid trapezoid data.
        if (mLevels == null) {
            return;
        }
        final float trapezoidBottom =
            getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth
                - mTrapezoidVOffset;
        final float availableSpace = trapezoidBottom - mDividerWidth * .5f - mIndent.top;
        final float unitHeight = availableSpace / 100f;
        // Draws all trapezoid shapes into the canvas.
        final Path trapezoidPath = new Path();
        Path trapezoidCurvePath = null;
        for (int index = 0; index < mTrapezoidCount; index++) {
            // Not draws the trapezoid for corner or not initialization cases.
            if (!isValidToDraw(index)) {
                if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) {
                    canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint);
                    trapezoidCurvePath = null;
                }
                continue;
            }
            // Configures the trapezoid paint color.
            final int trapezoidColor =
                !mIsSlotsClickabled
                    ? mTrapezoidColor
                    : mSelectedIndex == index || mSelectedIndex == SELECTED_INDEX_ALL
                        ? mTrapezoidSolidColor : mTrapezoidColor;
            final boolean isHoverState =
                mIsSlotsClickabled && mHoveredIndex == index && isValidToDraw(mHoveredIndex);
            mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);

            final float leftTop = round(trapezoidBottom - mLevels[index] * unitHeight);
            final float rightTop = round(trapezoidBottom - mLevels[index + 1] * unitHeight);
            trapezoidPath.reset();
            trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
            trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
            trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, rightTop);
            trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, trapezoidBottom);
            // A tricky way to make the trapezoid shape drawing the rounded corner.
            trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
            trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
            // Draws the trapezoid shape into canvas.
            canvas.drawPath(trapezoidPath, mTrapezoidPaint);

            // Generates path for non-clickable trapezoid curve.
            if (mTrapezoidCurvePaint != null) {
                if (trapezoidCurvePath == null) {
                    trapezoidCurvePath= new Path();
                    trapezoidCurvePath.moveTo(mTrapezoidSlots[index].mLeft, leftTop);
                } else {
                    trapezoidCurvePath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
                }
                trapezoidCurvePath.lineTo(mTrapezoidSlots[index].mRight, rightTop);
            }
        }
        // Draws the trapezoid curve for non-clickable case.
        if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) {
            canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint);
            trapezoidCurvePath = null;
        }
    }

    // Searches the corresponding trapezoid index from x location.
    private int getTrapezoidIndex(float x) {
        for (int index = 0; index < mTrapezoidSlots.length; index++) {
            final TrapezoidSlot slot = mTrapezoidSlots[index];
            if (x >= slot.mLeft - mTrapezoidHOffset
                    && x <= slot.mRight + mTrapezoidHOffset) {
                return index;
            }
        }
        return SELECTED_INDEX_INVALID;
    }

    private boolean isValidToDraw(int trapezoidIndex) {
        return mLevels != null
                && trapezoidIndex >= 0
                && trapezoidIndex < mLevels.length - 1
                && mLevels[trapezoidIndex] != 0
                && mLevels[trapezoidIndex + 1] != 0;
    }

    private static String[] getPercentages() {
        return new String[] {
            formatPercentage(/*percentage=*/ 100, /*round=*/ true),
            formatPercentage(/*percentage=*/ 50, /*round=*/ true),
            formatPercentage(/*percentage=*/ 0, /*round=*/ true)};
    }

    @VisibleForTesting
    static boolean isAccessibilityEnabled(Context context) {
        final AccessibilityManager accessibilityManager =
            context.getSystemService(AccessibilityManager.class);
        if (!accessibilityManager.isEnabled()) {
            return false;
        }
        final List<AccessibilityServiceInfo> serviceInfoList =
            accessibilityManager.getEnabledAccessibilityServiceList(
                AccessibilityServiceInfo.FEEDBACK_SPOKEN
                    | AccessibilityServiceInfo.FEEDBACK_GENERIC);
        for (AccessibilityServiceInfo info : serviceInfoList) {
            for (String serviceName : ACCESSIBILITY_SERVICE_NAMES) {
                final String serviceId = info.getId();
                if (serviceId != null && serviceId.contains(serviceName)) {
                    Log.d(TAG, "acccessibilityEnabled:" + serviceId);
                    return true;
                }
            }
        }
        return false;
    }

    // A container class for each trapezoid left and right location.
    private static final class TrapezoidSlot {
        public float mLeft;
        public float mRight;

        @Override
        public String toString() {
            return String.format(Locale.US, "TrapezoidSlot[%f,%f]", mLeft, mRight);
        }
    }
}
