diff --git a/res/layout/preference_balance_slider.xml b/res/layout/preference_balance_slider.xml
new file mode 100644
index 00000000000..32010c36026
--- /dev/null
+++ b/res/layout/preference_balance_slider.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 0a35188287e..d42f0731d98 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -38,6 +38,9 @@
8dip
+ 14dp
+ 1dp
+
100sp
3dip
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ae0b8f0214b..b5592b4a451 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -4717,6 +4717,12 @@
Mono audio
Combine channels when playing audio
+
+ Audio balance
+
+ Left
+
+ Right
Default
diff --git a/res/xml/accessibility_settings.xml b/res/xml/accessibility_settings.xml
index cc07ce119e9..9cb73a2ea6b 100644
--- a/res/xml/accessibility_settings.xml
+++ b/res/xml/accessibility_settings.xml
@@ -130,6 +130,10 @@
android:summary="@string/accessibility_toggle_master_mono_summary"
android:persistent="false"/>
+
+
mCenter - mSnapThreshold
+ && progress < mCenter + mSnapThreshold) {
+ progress = mCenter;
+ seekBar.setProgress(progress); // direct update (fromUser becomes false)
+ }
+ final float balance = (progress - mCenter) * 0.01f;
+ Settings.System.putFloatForUser(mContext.getContentResolver(),
+ Settings.System.MASTER_BALANCE, balance, UserHandle.USER_CURRENT);
+ }
+ // If fromUser is false, the call is a set from the framework on creation or on
+ // internal update. The progress may be zero, ignore (don't change system settings).
+
+ // after adjusting the seekbar, notify downstream listener.
+ // note that progress may have been adjusted in the code above to mCenter.
+ synchronized(mListenerLock) {
+ if (mOnSeekBarChangeListener != null) {
+ mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser);
+ }
+ }
+ }
+ };
+
+ // Percentage of max to be used as a snap to threshold
+ private static final float SNAP_TO_PERCENTAGE = 0.03f;
+ private final Paint mCenterMarkerPaint;
+ private final Rect mCenterMarkerRect;
+ // changed in setMax()
+ private float mSnapThreshold;
+ private int mCenter;
+
+ public BalanceSeekBar(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.seekBarStyle);
+ }
+
+ public BalanceSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
+ }
+
+ public BalanceSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mContext = context;
+ Resources res = getResources();
+ mCenterMarkerRect = new Rect(0 /* left */, 0 /* top */,
+ res.getDimensionPixelSize(R.dimen.balance_seekbar_center_marker_width),
+ res.getDimensionPixelSize(R.dimen.balance_seekbar_center_marker_height));
+ mCenterMarkerPaint = new Paint();
+ // TODO use a more suitable colour?
+ mCenterMarkerPaint.setColor(Color.BLACK);
+ mCenterMarkerPaint.setStyle(Paint.Style.FILL);
+ // Remove the progress colour
+ setProgressTintList(ColorStateList.valueOf(Color.TRANSPARENT));
+
+ super.setOnSeekBarChangeListener(mProxySeekBarListener);
+ }
+
+ @Override
+ public void setOnSeekBarChangeListener(OnSeekBarChangeListener listener) {
+ synchronized(mListenerLock) {
+ mOnSeekBarChangeListener = listener;
+ }
+ }
+
+ // Note: the superclass AbsSeekBar.setMax is synchronized.
+ @Override
+ public synchronized void setMax(int max) {
+ super.setMax(max);
+ // update snap to threshold
+ mCenter = max / 2;
+ mSnapThreshold = max * SNAP_TO_PERCENTAGE;
+ }
+
+ // Note: the superclass AbsSeekBar.onDraw is synchronized.
+ @Override
+ protected synchronized void onDraw(Canvas canvas) {
+ // Draw a vertical line at 50% that represents centred balance
+ int seekBarCenter = (canvas.getHeight() - getPaddingBottom()) / 2;
+ canvas.save();
+ canvas.translate((canvas.getWidth() - mCenterMarkerRect.right) / 2,
+ seekBarCenter - (mCenterMarkerRect.bottom / 2));
+ canvas.drawRect(mCenterMarkerRect, mCenterMarkerPaint);
+ canvas.restore();
+ super.onDraw(canvas);
+ }
+}
+
diff --git a/src/com/android/settings/accessibility/BalanceSeekBarPreference.java b/src/com/android/settings/accessibility/BalanceSeekBarPreference.java
new file mode 100644
index 00000000000..a40282ca929
--- /dev/null
+++ b/src/com/android/settings/accessibility/BalanceSeekBarPreference.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 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.accessibility;
+
+import android.content.Context;
+import android.media.AudioSystem;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.SeekBar;
+
+import androidx.core.content.res.TypedArrayUtils;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+import com.android.settings.widget.SeekBarPreference;
+
+/** A slider preference that directly controls audio balance **/
+public class BalanceSeekBarPreference extends SeekBarPreference {
+ private static final String TAG = "BalanceSeekBarPreference";
+ private final Context mContext;
+ private BalanceSeekBar mSeekBar;
+ private ImageView mIconView;
+
+ public BalanceSeekBarPreference(Context context, AttributeSet attrs) {
+ super(context, attrs, TypedArrayUtils.getAttr(context,
+ R.attr.preferenceStyle,
+ android.R.attr.preferenceStyle));
+ mContext = context;
+ setLayoutResource(R.layout.preference_balance_slider);
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder view) {
+ super.onBindViewHolder(view);
+ mSeekBar = (BalanceSeekBar) view.findViewById(com.android.internal.R.id.seekbar);
+ mIconView = (ImageView) view.findViewById(com.android.internal.R.id.icon);
+ init();
+ }
+
+ private void init() {
+ if (mSeekBar == null) {
+ return;
+ }
+ final float balance = Settings.System.getFloatForUser(
+ mContext.getContentResolver(), Settings.System.MASTER_BALANCE,
+ 0.f /* default */, UserHandle.USER_CURRENT);
+ // Rescale balance to range 0-200 centered at 100.
+ mSeekBar.setMax(200);
+ mSeekBar.setProgress((int)(balance * 100.f) + 100);
+ mSeekBar.setEnabled(isEnabled());
+ }
+}