1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.developeroptions.accessibility;
18 
19 import android.content.Context;
20 import android.content.res.ColorStateList;
21 import android.content.res.Resources;
22 import android.graphics.Canvas;
23 import android.graphics.Color;
24 import android.graphics.Paint;
25 import android.graphics.Rect;
26 import android.os.UserHandle;
27 import android.provider.Settings;
28 import android.util.AttributeSet;
29 import android.widget.SeekBar;
30 
31 import androidx.annotation.VisibleForTesting;
32 
33 import com.android.car.developeroptions.R;
34 
35 /**
36  * A custom seekbar for the balance setting.
37  *
38  * Adds a center line indicator between left and right, which snaps to if close.
39  * Updates Settings.System for balance on progress changed.
40  */
41 public class BalanceSeekBar extends SeekBar {
42     private final Context mContext;
43     private final Object mListenerLock = new Object();
44     private OnSeekBarChangeListener mOnSeekBarChangeListener;
45     private final OnSeekBarChangeListener mProxySeekBarListener = new OnSeekBarChangeListener() {
46         @Override
47         public void onStopTrackingTouch(SeekBar seekBar) {
48             synchronized (mListenerLock) {
49                 if (mOnSeekBarChangeListener != null) {
50                     mOnSeekBarChangeListener.onStopTrackingTouch(seekBar);
51                 }
52             }
53         }
54 
55         @Override
56         public void onStartTrackingTouch(SeekBar seekBar) {
57             synchronized (mListenerLock) {
58                 if (mOnSeekBarChangeListener != null) {
59                     mOnSeekBarChangeListener.onStartTrackingTouch(seekBar);
60                 }
61             }
62         }
63 
64         @Override
65         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
66             if (fromUser) {
67                 // Snap to centre when within the specified threshold
68                 if (progress != mCenter
69                         && progress > mCenter - mSnapThreshold
70                         && progress < mCenter + mSnapThreshold) {
71                     progress = mCenter;
72                     seekBar.setProgress(progress); // direct update (fromUser becomes false)
73                 }
74                 final float balance = (progress - mCenter) * 0.01f;
75                 Settings.System.putFloatForUser(mContext.getContentResolver(),
76                         Settings.System.MASTER_BALANCE, balance, UserHandle.USER_CURRENT);
77             }
78             // If fromUser is false, the call is a set from the framework on creation or on
79             // internal update. The progress may be zero, ignore (don't change system settings).
80 
81             // after adjusting the seekbar, notify downstream listener.
82             // note that progress may have been adjusted in the code above to mCenter.
83             synchronized (mListenerLock) {
84                 if (mOnSeekBarChangeListener != null) {
85                     mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser);
86                 }
87             }
88         }
89     };
90 
91     // Percentage of max to be used as a snap to threshold
92     @VisibleForTesting
93     static final float SNAP_TO_PERCENTAGE = 0.03f;
94     private final Paint mCenterMarkerPaint;
95     private final Rect mCenterMarkerRect;
96     // changed in setMax()
97     private float mSnapThreshold;
98     private int mCenter;
99 
BalanceSeekBar(Context context, AttributeSet attrs)100     public BalanceSeekBar(Context context, AttributeSet attrs) {
101         this(context, attrs, com.android.internal.R.attr.seekBarStyle);
102     }
103 
BalanceSeekBar(Context context, AttributeSet attrs, int defStyleAttr)104     public BalanceSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
105         this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
106     }
107 
BalanceSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)108     public BalanceSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
109         super(context, attrs, defStyleAttr, defStyleRes);
110         mContext = context;
111         Resources res = getResources();
112         mCenterMarkerRect = new Rect(0 /* left */, 0 /* top */,
113                 res.getDimensionPixelSize(R.dimen.balance_seekbar_center_marker_width),
114                 res.getDimensionPixelSize(R.dimen.balance_seekbar_center_marker_height));
115         mCenterMarkerPaint = new Paint();
116         // TODO use a more suitable colour?
117         mCenterMarkerPaint.setColor(Color.BLACK);
118         mCenterMarkerPaint.setStyle(Paint.Style.FILL);
119         // Remove the progress colour
120         setProgressTintList(ColorStateList.valueOf(Color.TRANSPARENT));
121 
122         super.setOnSeekBarChangeListener(mProxySeekBarListener);
123     }
124 
125     @Override
setOnSeekBarChangeListener(OnSeekBarChangeListener listener)126     public void setOnSeekBarChangeListener(OnSeekBarChangeListener listener) {
127         synchronized (mListenerLock) {
128             mOnSeekBarChangeListener = listener;
129         }
130     }
131 
132     // Note: the superclass AbsSeekBar.setMax is synchronized.
133     @Override
setMax(int max)134     public synchronized void setMax(int max) {
135         super.setMax(max);
136         // update snap to threshold
137         mCenter = max / 2;
138         mSnapThreshold = max * SNAP_TO_PERCENTAGE;
139     }
140 
141     // Note: the superclass AbsSeekBar.onDraw is synchronized.
142     @Override
onDraw(Canvas canvas)143     protected synchronized void onDraw(Canvas canvas) {
144         // Draw a vertical line at 50% that represents centred balance
145         int seekBarCenter = (canvas.getHeight() - getPaddingBottom()) / 2;
146         canvas.save();
147         canvas.translate((canvas.getWidth() - mCenterMarkerRect.right) / 2,
148                 seekBarCenter - (mCenterMarkerRect.bottom / 2));
149         canvas.drawRect(mCenterMarkerRect, mCenterMarkerPaint);
150         canvas.restore();
151         super.onDraw(canvas);
152     }
153 
154     @VisibleForTesting
getProxySeekBarListener()155     OnSeekBarChangeListener getProxySeekBarListener() {
156         return mProxySeekBarListener;
157     }
158 }
159 
160