fix: 引入Settings的Module

This commit is contained in:
2024-12-10 14:57:24 +08:00
parent ad8fc8731d
commit df105485bd
6934 changed files with 896168 additions and 2 deletions

View File

@@ -0,0 +1,92 @@
/*
* 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.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import com.android.settings.R;
import com.android.settingslib.RestrictedPreference;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceViewHolder;
/**
* A preference with a plus button on the side representing an "add" action. The plus button will
* only be visible when a non-null click listener is registered.
*/
public class AddPreference extends RestrictedPreference implements View.OnClickListener {
private OnAddClickListener mListener;
private View mWidgetFrame;
private View mAddWidget;
public AddPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@VisibleForTesting
int getAddWidgetResId() {
return R.id.add_preference_widget;
}
/** Sets a listener for clicks on the plus button. Passing null will cause the button to be
* hidden. */
public void setOnAddClickListener(OnAddClickListener listener) {
mListener = listener;
if (mWidgetFrame != null) {
mWidgetFrame.setVisibility(shouldHideSecondTarget() ? View.GONE : View.VISIBLE);
}
}
public void setAddWidgetEnabled(boolean enabled) {
if (mAddWidget != null) {
mAddWidget.setEnabled(enabled);
}
}
@Override
protected int getSecondTargetResId() {
return R.layout.preference_widget_add;
}
@Override
protected boolean shouldHideSecondTarget() {
return mListener == null;
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
mWidgetFrame = holder.findViewById(android.R.id.widget_frame);
mAddWidget = holder.findViewById(getAddWidgetResId());
mAddWidget.setEnabled(true);
mAddWidget.setOnClickListener(this);
}
@Override
public void onClick(View view) {
if (view.getId() == getAddWidgetResId() && mListener != null) {
mListener.onAddClick(this);
}
}
public interface OnAddClickListener {
void onAddClick(AddPreference p);
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2018 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.widget;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import androidx.preference.CheckBoxPreference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
/**
* {@link CheckBoxPreference} that used only to display app
*/
public class AppCheckBoxPreference extends CheckBoxPreference {
public AppCheckBoxPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setLayoutResource(com.android.settingslib.widget.preference.app.R.layout.preference_app);
}
public AppCheckBoxPreference(Context context) {
super(context);
setLayoutResource(com.android.settingslib.widget.preference.app.R.layout.preference_app);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final LinearLayout layout = (LinearLayout) holder.findViewById(R.id.summary_container);
if (layout != null) {
// If summary doesn't exist, make it gone
layout.setVisibility(TextUtils.isEmpty(getSummary()) ? View.GONE : View.VISIBLE);
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (C) 2016 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.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
/**
* A {@link FrameLayout} with customizable aspect ratio.
* This is used to avoid dynamically calculating the height for the frame. Default aspect
* ratio will be 1 if none is set in layout attribute.
*/
public final class AspectRatioFrameLayout extends FrameLayout {
private static final float ASPECT_RATIO_CHANGE_THREASHOLD = 0.01f;
@VisibleForTesting
float mAspectRatio = 1.0f;
public AspectRatioFrameLayout(Context context) {
this(context, null);
}
public AspectRatioFrameLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AspectRatioFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
if (attrs != null) {
TypedArray array =
context.obtainStyledAttributes(attrs, R.styleable.AspectRatioFrameLayout);
mAspectRatio = array.getFloat(
R.styleable.AspectRatioFrameLayout_aspectRatio, 1.0f);
array.recycle();
}
}
public void setAspectRatio(float aspectRadio) {
mAspectRatio = aspectRadio;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
if (width == 0 || height == 0) {
return;
}
final float viewAspectRatio = (float) width / height;
final float aspectRatioDiff = mAspectRatio - viewAspectRatio;
if (Math.abs(aspectRatioDiff) <= ASPECT_RATIO_CHANGE_THREASHOLD) {
// Close enough, skip.
return;
}
width = (int) (height * mAspectRatio);
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2017 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.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
/**
* An extension of LinearLayout that automatically switches to vertical
* orientation when it can't fit its child views horizontally.
*
* Main logic in this class comes from {@link androidx.appcompat.widget.ButtonBarLayout}.
* Compared with {@link androidx.appcompat.widget.ButtonBarLayout}, this layout won't reverse
* children's order and won't update the minimum height
*/
public class BottomLabelLayout extends LinearLayout {
private static final String TAG = "BottomLabelLayout";
public BottomLabelLayout(Context context,
@Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final boolean isStacked = isStacked();
boolean needsRemeasure = false;
// If we're not stacked, make sure the measure spec is AT_MOST rather
// than EXACTLY. This ensures that we'll still get TOO_SMALL so that we
// know to stack the buttons.
final int initialWidthMeasureSpec;
if (!isStacked && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
initialWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
// We'll need to remeasure again to fill excess space.
needsRemeasure = true;
} else {
initialWidthMeasureSpec = widthMeasureSpec;
}
super.onMeasure(initialWidthMeasureSpec, heightMeasureSpec);
if (!isStacked) {
final int measuredWidth = getMeasuredWidthAndState();
final int measuredWidthState = measuredWidth & View.MEASURED_STATE_MASK;
if (measuredWidthState == View.MEASURED_STATE_TOO_SMALL) {
setStacked(true);
// Measure again in the new orientation.
needsRemeasure = true;
}
}
if (needsRemeasure) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@VisibleForTesting
void setStacked(boolean stacked) {
setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
setGravity(stacked ? Gravity.START : Gravity.BOTTOM);
final View spacer = findViewById(com.android.settings.R.id.spacer);
if (spacer != null) {
spacer.setVisibility(stacked ? View.GONE : View.VISIBLE);
}
}
private boolean isStacked() {
return getOrientation() == LinearLayout.VERTICAL;
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (C) 2024 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.widget
import android.content.Context
import android.content.res.Resources
import android.util.AttributeSet
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import com.android.settings.spa.preference.ComposePreference
import com.android.settingslib.spa.widget.card.CardButton
import com.android.settingslib.spa.widget.card.CardModel
import com.android.settingslib.spa.widget.card.SettingsCard
/** A preference for settings banner tips card. */
class CardPreference
@JvmOverloads
constructor(
context: Context,
attr: AttributeSet? = null,
) : ComposePreference(context, attr) {
/** A icon resource id for displaying icon on tips card. */
var iconResId: Int? = null
/** The primary button's text. */
var primaryButtonText: String = ""
/** The accessibility content description of the primary button. */
var primaryButtonContentDescription: String? = null
/** The action for click on primary button. */
var primaryButtonAction: () -> Unit = {}
/** The visibility of primary button on tips card. The default value is `false`. */
var primaryButtonVisibility: Boolean = false
/** The text on the second button of this [SettingsCard]. */
var secondaryButtonText: String = ""
/** The accessibility content description of the secondary button. */
var secondaryButtonContentDescription: String? = null
/** The action for click on secondary button. */
var secondaryButtonAction: () -> Unit = {}
/** The visibility of secondary button on tips card. The default value is `false`. */
var secondaryButtonVisibility: Boolean = false
var onClick: (() -> Unit)? = null
/** The callback for click on card preference itself. */
private var onDismiss: (() -> Unit)? = null
/** Enable the dismiss button on tips card. */
fun enableDismiss(enable: Boolean) =
if (enable) onDismiss = { isVisible = false } else onDismiss = null
/** Clear layout state if needed. */
fun resetLayoutState() {
primaryButtonVisibility = false
secondaryButtonVisibility = false
notifyChanged()
}
/** Build the tips card content to apply any changes of this card's property. */
fun buildContent() {
setContent {
SettingsCard(
CardModel(
title = title?.toString() ?: "",
text = summary?.toString() ?: "",
buttons = listOfNotNull(configPrimaryButton(), configSecondaryButton()),
onDismiss = onDismiss,
imageVector =
iconResId
?.takeIf { it != Resources.ID_NULL }
?.let { ImageVector.vectorResource(it) },
onClick = onClick,
)
)
}
}
private fun configPrimaryButton(): CardButton? {
return if (primaryButtonVisibility)
CardButton(
text = primaryButtonText,
contentDescription = primaryButtonContentDescription,
onClick = primaryButtonAction,
)
else null
}
private fun configSecondaryButton(): CardButton? {
return if (secondaryButtonVisibility)
CardButton(
text = secondaryButtonText,
contentDescription = secondaryButtonContentDescription,
onClick = secondaryButtonAction,
)
else null
}
override fun notifyChanged() {
buildContent()
super.notifyChanged()
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2011 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.widget;
import android.content.res.Resources;
import android.text.SpannableStringBuilder;
/**
* Axis along a {@link ChartView} that knows how to convert between raw point
* and screen coordinate systems.
*/
public interface ChartAxis {
/** Set range of raw values this axis should cover. */
public boolean setBounds(long min, long max);
/** Set range of screen points this axis should cover. */
public boolean setSize(float size);
/** Convert raw value into screen point. */
public float convertToPoint(long value);
/** Convert screen point into raw value. */
public long convertToValue(float point);
/**
* Build label that describes given raw value. If the label is rounded for
* display, return the rounded value.
*/
public long buildLabel(Resources res, SpannableStringBuilder builder, long value);
/** Return list of tick points for drawing a grid. */
public float[] getTickPoints();
/**
* Test if given raw value should cause the axis to grow or shrink;
* returning positive value to grow and negative to shrink.
*/
public int shouldAdjustAxis(long value);
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright (C) 2011 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.widget;
import static com.android.settings.Utils.formatDateRange;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.View;
import com.android.internal.util.Preconditions;
import com.android.settings.R;
/**
* Background of {@link ChartView} that renders grid lines as requested by
* {@link ChartAxis#getTickPoints()}.
*/
public class ChartGridView extends View {
private ChartAxis mHoriz;
private ChartAxis mVert;
private Drawable mPrimary;
private Drawable mSecondary;
private Drawable mBorder;
private int mLabelSize;
private int mLabelColor;
private Layout mLabelStart;
private Layout mLabelMid;
private Layout mLabelEnd;
public ChartGridView(Context context) {
this(context, null, 0);
}
public ChartGridView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ChartGridView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setWillNotDraw(false);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ChartGridView, defStyle, 0);
mPrimary = a.getDrawable(R.styleable.ChartGridView_primaryDrawable);
mSecondary = a.getDrawable(R.styleable.ChartGridView_secondaryDrawable);
mBorder = a.getDrawable(R.styleable.ChartGridView_borderDrawable);
final int taId = a.getResourceId(R.styleable.ChartGridView_android_textAppearance, -1);
final TypedArray ta = context.obtainStyledAttributes(taId,
com.android.internal.R.styleable.TextAppearance);
mLabelSize = ta.getDimensionPixelSize(
com.android.internal.R.styleable.TextAppearance_textSize, 0);
ta.recycle();
final ColorStateList labelColor = a.getColorStateList(
R.styleable.ChartGridView_android_textColor);
mLabelColor = labelColor.getDefaultColor();
a.recycle();
}
void init(ChartAxis horiz, ChartAxis vert) {
mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
mVert = Preconditions.checkNotNull(vert, "missing vert");
}
void setBounds(long start, long end) {
final Context context = getContext();
final long mid = (start + end) / 2;
mLabelStart = makeLabel(formatDateRange(context, start, start));
mLabelMid = makeLabel(formatDateRange(context, mid, mid));
mLabelEnd = makeLabel(formatDateRange(context, end, end));
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
final int width = getWidth();
final int height = getHeight() - getPaddingBottom();
final Drawable secondary = mSecondary;
if (secondary != null) {
final int secondaryHeight = secondary.getIntrinsicHeight();
final float[] vertTicks = mVert.getTickPoints();
for (float y : vertTicks) {
final int bottom = (int) Math.min(y + secondaryHeight, height);
secondary.setBounds(0, (int) y, width, bottom);
secondary.draw(canvas);
}
}
final Drawable primary = mPrimary;
if (primary != null) {
final int primaryWidth = primary.getIntrinsicWidth();
final int primaryHeight = primary.getIntrinsicHeight();
final float[] horizTicks = mHoriz.getTickPoints();
for (float x : horizTicks) {
final int right = (int) Math.min(x + primaryWidth, width);
primary.setBounds((int) x, 0, right, height);
primary.draw(canvas);
}
}
mBorder.setBounds(0, 0, width, height);
mBorder.draw(canvas);
final int padding = mLabelStart != null ? mLabelStart.getHeight() / 8 : 0;
final Layout start = mLabelStart;
if (start != null) {
final int saveCount = canvas.save();
canvas.translate(0, height + padding);
start.draw(canvas);
canvas.restoreToCount(saveCount);
}
final Layout mid = mLabelMid;
if (mid != null) {
final int saveCount = canvas.save();
canvas.translate((width - mid.getWidth()) / 2, height + padding);
mid.draw(canvas);
canvas.restoreToCount(saveCount);
}
final Layout end = mLabelEnd;
if (end != null) {
final int saveCount = canvas.save();
canvas.translate(width - end.getWidth(), height + padding);
end.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
private Layout makeLabel(CharSequence text) {
final Resources res = getResources();
final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
paint.density = res.getDisplayMetrics().density;
paint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
paint.setColor(mLabelColor);
paint.setTextSize(mLabelSize);
return StaticLayout.Builder.obtain(text, 0, text.length(), paint,
(int) Math.ceil(Layout.getDesiredWidth(text, paint)))
.setUseLineSpacingFromFallbacks(true)
.build();
}
}

View File

@@ -0,0 +1,754 @@
/*
* Copyright (C) 2011 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.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.DynamicLayout;
import android.text.Layout;
import android.text.Layout.Alignment;
import android.text.SpannableStringBuilder;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.MathUtils;
import android.view.MotionEvent;
import android.view.View;
import com.android.internal.util.Preconditions;
import com.android.settings.R;
/**
* Sweep across a {@link ChartView} at a specific {@link ChartAxis} value, which
* a user can drag.
*/
public class ChartSweepView extends View {
private static final boolean DRAW_OUTLINE = false;
// TODO: clean up all the various padding/offset/margins
private Drawable mSweep;
private Rect mSweepPadding = new Rect();
/** Offset of content inside this view. */
private Rect mContentOffset = new Rect();
/** Offset of {@link #mSweep} inside this view. */
private Point mSweepOffset = new Point();
private Rect mMargins = new Rect();
private float mNeighborMargin;
private int mSafeRegion;
private int mFollowAxis;
private int mLabelMinSize;
private float mLabelSize;
private int mLabelTemplateRes;
private int mLabelColor;
private SpannableStringBuilder mLabelTemplate;
private DynamicLayout mLabelLayout;
private ChartAxis mAxis;
private long mValue;
private long mLabelValue;
private long mValidAfter;
private long mValidBefore;
private ChartSweepView mValidAfterDynamic;
private ChartSweepView mValidBeforeDynamic;
private float mLabelOffset;
private Paint mOutlinePaint = new Paint();
public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;
private int mTouchMode = MODE_NONE;
private static final int MODE_NONE = 0;
private static final int MODE_DRAG = 1;
private static final int MODE_LABEL = 2;
private static final int LARGE_WIDTH = 1024;
private long mDragInterval = 1;
public interface OnSweepListener {
public void onSweep(ChartSweepView sweep, boolean sweepDone);
public void requestEdit(ChartSweepView sweep);
}
private OnSweepListener mListener;
private float mTrackingStart;
private MotionEvent mTracking;
private ChartSweepView[] mNeighbors = new ChartSweepView[0];
public ChartSweepView(Context context) {
this(context, null);
}
public ChartSweepView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ChartSweepView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ChartSweepView, defStyle, 0);
final int color = a.getColor(R.styleable.ChartSweepView_labelColor, Color.BLUE);
setSweepDrawable(a.getDrawable(R.styleable.ChartSweepView_sweepDrawable), color);
setFollowAxis(a.getInt(R.styleable.ChartSweepView_followAxis, -1));
setNeighborMargin(a.getDimensionPixelSize(R.styleable.ChartSweepView_neighborMargin, 0));
setSafeRegion(a.getDimensionPixelSize(R.styleable.ChartSweepView_safeRegion, 0));
setLabelMinSize(a.getDimensionPixelSize(R.styleable.ChartSweepView_labelSize, 0));
setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0));
setLabelColor(color);
// TODO: moved focused state directly into assets
setBackgroundResource(R.drawable.data_usage_sweep_background);
mOutlinePaint.setColor(Color.RED);
mOutlinePaint.setStrokeWidth(1f);
mOutlinePaint.setStyle(Style.STROKE);
a.recycle();
setClickable(true);
setOnClickListener(mClickListener);
setWillNotDraw(false);
}
private OnClickListener mClickListener = new OnClickListener() {
public void onClick(View v) {
dispatchRequestEdit();
}
};
void init(ChartAxis axis) {
mAxis = Preconditions.checkNotNull(axis, "missing axis");
}
public void setNeighbors(ChartSweepView... neighbors) {
mNeighbors = neighbors;
}
public int getFollowAxis() {
return mFollowAxis;
}
public Rect getMargins() {
return mMargins;
}
public void setDragInterval(long dragInterval) {
mDragInterval = dragInterval;
}
/**
* Return the number of pixels that the "target" area is inset from the
* {@link View} edge, along the current {@link #setFollowAxis(int)}.
*/
private float getTargetInset() {
if (mFollowAxis == VERTICAL) {
final float targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top
- mSweepPadding.bottom;
return mSweepPadding.top + (targetHeight / 2) + mSweepOffset.y;
} else {
final float targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left
- mSweepPadding.right;
return mSweepPadding.left + (targetWidth / 2) + mSweepOffset.x;
}
}
public void addOnSweepListener(OnSweepListener listener) {
mListener = listener;
}
private void dispatchOnSweep(boolean sweepDone) {
if (mListener != null) {
mListener.onSweep(this, sweepDone);
}
}
private void dispatchRequestEdit() {
if (mListener != null) {
mListener.requestEdit(this);
}
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
setFocusable(enabled);
requestLayout();
}
public void setSweepDrawable(Drawable sweep, int color) {
if (mSweep != null) {
mSweep.setCallback(null);
unscheduleDrawable(mSweep);
}
if (sweep != null) {
sweep.setCallback(this);
if (sweep.isStateful()) {
sweep.setState(getDrawableState());
}
sweep.setVisible(getVisibility() == VISIBLE, false);
mSweep = sweep;
// Match the text.
mSweep.setTint(color);
sweep.getPadding(mSweepPadding);
} else {
mSweep = null;
}
invalidate();
}
public void setFollowAxis(int followAxis) {
mFollowAxis = followAxis;
}
public void setLabelMinSize(int minSize) {
mLabelMinSize = minSize;
invalidateLabelTemplate();
}
public void setLabelTemplate(int resId) {
mLabelTemplateRes = resId;
invalidateLabelTemplate();
}
public void setLabelColor(int color) {
mLabelColor = color;
invalidateLabelTemplate();
}
private void invalidateLabelTemplate() {
if (mLabelTemplateRes != 0) {
final CharSequence template = getResources().getText(mLabelTemplateRes);
final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
paint.density = getResources().getDisplayMetrics().density;
paint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
paint.setColor(mLabelColor);
mLabelTemplate = new SpannableStringBuilder(template);
mLabelLayout = DynamicLayout.Builder.obtain(mLabelTemplate, paint, LARGE_WIDTH)
.setAlignment(Alignment.ALIGN_RIGHT)
.setIncludePad(false)
.setUseLineSpacingFromFallbacks(true)
.build();
invalidateLabel();
} else {
mLabelTemplate = null;
mLabelLayout = null;
}
invalidate();
requestLayout();
}
private void invalidateLabel() {
if (mLabelTemplate != null && mAxis != null) {
mLabelValue = mAxis.buildLabel(getResources(), mLabelTemplate, mValue);
setContentDescription(mLabelTemplate);
invalidateLabelOffset();
invalidate();
} else {
mLabelValue = mValue;
}
}
/**
* When overlapping with neighbor, split difference and push label.
*/
public void invalidateLabelOffset() {
float margin;
float labelOffset = 0;
if (mFollowAxis == VERTICAL) {
if (mValidAfterDynamic != null) {
mLabelSize = Math.max(getLabelWidth(this), getLabelWidth(mValidAfterDynamic));
margin = getLabelTop(mValidAfterDynamic) - getLabelBottom(this);
if (margin < 0) {
labelOffset = margin / 2;
}
} else if (mValidBeforeDynamic != null) {
mLabelSize = Math.max(getLabelWidth(this), getLabelWidth(mValidBeforeDynamic));
margin = getLabelTop(this) - getLabelBottom(mValidBeforeDynamic);
if (margin < 0) {
labelOffset = -margin / 2;
}
} else {
mLabelSize = getLabelWidth(this);
}
} else {
// TODO: implement horizontal labels
}
mLabelSize = Math.max(mLabelSize, mLabelMinSize);
// when offsetting label, neighbor probably needs to offset too
if (labelOffset != mLabelOffset) {
mLabelOffset = labelOffset;
invalidate();
if (mValidAfterDynamic != null) mValidAfterDynamic.invalidateLabelOffset();
if (mValidBeforeDynamic != null) mValidBeforeDynamic.invalidateLabelOffset();
}
}
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
if (mSweep != null) {
mSweep.jumpToCurrentState();
}
}
@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
if (mSweep != null) {
mSweep.setVisible(visibility == VISIBLE, false);
}
}
@Override
protected boolean verifyDrawable(Drawable who) {
return who == mSweep || super.verifyDrawable(who);
}
public ChartAxis getAxis() {
return mAxis;
}
public void setValue(long value) {
mValue = value;
invalidateLabel();
}
public long getValue() {
return mValue;
}
public long getLabelValue() {
return mLabelValue;
}
public float getPoint() {
if (isEnabled()) {
return mAxis.convertToPoint(mValue);
} else {
// when disabled, show along top edge
return 0;
}
}
/**
* Set valid range this sweep can move within, in {@link #mAxis} values. The
* most restrictive combination of all valid ranges is used.
*/
public void setValidRange(long validAfter, long validBefore) {
mValidAfter = validAfter;
mValidBefore = validBefore;
}
public void setNeighborMargin(float neighborMargin) {
mNeighborMargin = neighborMargin;
}
public void setSafeRegion(int safeRegion) {
mSafeRegion = safeRegion;
}
/**
* Set valid range this sweep can move within, defined by the given
* {@link ChartSweepView}. The most restrictive combination of all valid
* ranges is used.
*/
public void setValidRangeDynamic(ChartSweepView validAfter, ChartSweepView validBefore) {
mValidAfterDynamic = validAfter;
mValidBeforeDynamic = validBefore;
}
/**
* Test if given {@link MotionEvent} is closer to another
* {@link ChartSweepView} compared to ourselves.
*/
public boolean isTouchCloserTo(MotionEvent eventInParent, ChartSweepView another) {
final float selfDist = getTouchDistanceFromTarget(eventInParent);
final float anotherDist = another.getTouchDistanceFromTarget(eventInParent);
return anotherDist < selfDist;
}
private float getTouchDistanceFromTarget(MotionEvent eventInParent) {
if (mFollowAxis == HORIZONTAL) {
return Math.abs(eventInParent.getX() - (getX() + getTargetInset()));
} else {
return Math.abs(eventInParent.getY() - (getY() + getTargetInset()));
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) return false;
final View parent = (View) getParent();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
// only start tracking when in sweet spot
final boolean acceptDrag;
final boolean acceptLabel;
if (mFollowAxis == VERTICAL) {
acceptDrag = event.getX() > getWidth() - (mSweepPadding.right * 8);
acceptLabel = mLabelLayout != null ? event.getX() < mLabelLayout.getWidth()
: false;
} else {
acceptDrag = event.getY() > getHeight() - (mSweepPadding.bottom * 8);
acceptLabel = mLabelLayout != null ? event.getY() < mLabelLayout.getHeight()
: false;
}
final MotionEvent eventInParent = event.copy();
eventInParent.offsetLocation(getLeft(), getTop());
// ignore event when closer to a neighbor
for (ChartSweepView neighbor : mNeighbors) {
if (isTouchCloserTo(eventInParent, neighbor)) {
return false;
}
}
if (acceptDrag) {
if (mFollowAxis == VERTICAL) {
mTrackingStart = getTop() - mMargins.top;
} else {
mTrackingStart = getLeft() - mMargins.left;
}
mTracking = event.copy();
mTouchMode = MODE_DRAG;
// starting drag should activate entire chart
if (!parent.isActivated()) {
parent.setActivated(true);
}
return true;
} else if (acceptLabel) {
mTouchMode = MODE_LABEL;
return true;
} else {
mTouchMode = MODE_NONE;
return false;
}
}
case MotionEvent.ACTION_MOVE: {
if (mTouchMode == MODE_LABEL) {
return true;
}
getParent().requestDisallowInterceptTouchEvent(true);
// content area of parent
final Rect parentContent = getParentContentRect();
final Rect clampRect = computeClampRect(parentContent);
if (clampRect.isEmpty()) return true;
long value;
if (mFollowAxis == VERTICAL) {
final float currentTargetY = getTop() - mMargins.top;
final float requestedTargetY = mTrackingStart
+ (event.getRawY() - mTracking.getRawY());
final float clampedTargetY = MathUtils.constrain(
requestedTargetY, clampRect.top, clampRect.bottom);
setTranslationY(clampedTargetY - currentTargetY);
value = mAxis.convertToValue(clampedTargetY - parentContent.top);
} else {
final float currentTargetX = getLeft() - mMargins.left;
final float requestedTargetX = mTrackingStart
+ (event.getRawX() - mTracking.getRawX());
final float clampedTargetX = MathUtils.constrain(
requestedTargetX, clampRect.left, clampRect.right);
setTranslationX(clampedTargetX - currentTargetX);
value = mAxis.convertToValue(clampedTargetX - parentContent.left);
}
// round value from drag to nearest increment
value -= value % mDragInterval;
setValue(value);
dispatchOnSweep(false);
return true;
}
case MotionEvent.ACTION_UP: {
if (mTouchMode == MODE_LABEL) {
performClick();
} else if (mTouchMode == MODE_DRAG) {
mTrackingStart = 0;
mTracking = null;
mValue = mLabelValue;
dispatchOnSweep(true);
setTranslationX(0);
setTranslationY(0);
requestLayout();
}
mTouchMode = MODE_NONE;
return true;
}
default: {
return false;
}
}
}
/**
* Update {@link #mValue} based on current position, including any
* {@link #onTouchEvent(MotionEvent)} in progress. Typically used when
* {@link ChartAxis} changes during sweep adjustment.
*/
public void updateValueFromPosition() {
final Rect parentContent = getParentContentRect();
if (mFollowAxis == VERTICAL) {
final float effectiveY = getY() - mMargins.top - parentContent.top;
setValue(mAxis.convertToValue(effectiveY));
} else {
final float effectiveX = getX() - mMargins.left - parentContent.left;
setValue(mAxis.convertToValue(effectiveX));
}
}
public int shouldAdjustAxis() {
return mAxis.shouldAdjustAxis(getValue());
}
private Rect getParentContentRect() {
final View parent = (View) getParent();
return new Rect(parent.getPaddingLeft(), parent.getPaddingTop(),
parent.getWidth() - parent.getPaddingRight(),
parent.getHeight() - parent.getPaddingBottom());
}
@Override
public void addOnLayoutChangeListener(OnLayoutChangeListener listener) {
// ignored to keep LayoutTransition from animating us
}
@Override
public void removeOnLayoutChangeListener(OnLayoutChangeListener listener) {
// ignored to keep LayoutTransition from animating us
}
private long getValidAfterDynamic() {
final ChartSweepView dynamic = mValidAfterDynamic;
return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MIN_VALUE;
}
private long getValidBeforeDynamic() {
final ChartSweepView dynamic = mValidBeforeDynamic;
return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MAX_VALUE;
}
/**
* Compute {@link Rect} in {@link #getParent()} coordinates that we should
* be clamped inside of, usually from {@link #setValidRange(long, long)}
* style rules.
*/
private Rect computeClampRect(Rect parentContent) {
// create two rectangles, and pick most restrictive combination
final Rect rect = buildClampRect(parentContent, mValidAfter, mValidBefore, 0f);
final Rect dynamicRect = buildClampRect(
parentContent, getValidAfterDynamic(), getValidBeforeDynamic(), mNeighborMargin);
if (!rect.intersect(dynamicRect)) {
rect.setEmpty();
}
return rect;
}
private Rect buildClampRect(
Rect parentContent, long afterValue, long beforeValue, float margin) {
if (mAxis instanceof InvertedChartAxis) {
long temp = beforeValue;
beforeValue = afterValue;
afterValue = temp;
}
final boolean afterValid = afterValue != Long.MIN_VALUE && afterValue != Long.MAX_VALUE;
final boolean beforeValid = beforeValue != Long.MIN_VALUE && beforeValue != Long.MAX_VALUE;
final float afterPoint = mAxis.convertToPoint(afterValue) + margin;
final float beforePoint = mAxis.convertToPoint(beforeValue) - margin;
final Rect clampRect = new Rect(parentContent);
if (mFollowAxis == VERTICAL) {
if (beforeValid) clampRect.bottom = clampRect.top + (int) beforePoint;
if (afterValid) clampRect.top += afterPoint;
} else {
if (beforeValid) clampRect.right = clampRect.left + (int) beforePoint;
if (afterValid) clampRect.left += afterPoint;
}
return clampRect;
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (mSweep.isStateful()) {
mSweep.setState(getDrawableState());
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO: handle vertical labels
if (isEnabled() && mLabelLayout != null) {
final int sweepHeight = mSweep.getIntrinsicHeight();
final int templateHeight = mLabelLayout.getHeight();
mSweepOffset.x = 0;
mSweepOffset.y = 0;
mSweepOffset.y = (int) ((templateHeight / 2) - getTargetInset());
setMeasuredDimension(mSweep.getIntrinsicWidth(), Math.max(sweepHeight, templateHeight));
} else {
mSweepOffset.x = 0;
mSweepOffset.y = 0;
setMeasuredDimension(mSweep.getIntrinsicWidth(), mSweep.getIntrinsicHeight());
}
if (mFollowAxis == VERTICAL) {
final int targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top
- mSweepPadding.bottom;
mMargins.top = -(mSweepPadding.top + (targetHeight / 2));
mMargins.bottom = 0;
mMargins.left = -mSweepPadding.left;
mMargins.right = mSweepPadding.right;
} else {
final int targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left
- mSweepPadding.right;
mMargins.left = -(mSweepPadding.left + (targetWidth / 2));
mMargins.right = 0;
mMargins.top = -mSweepPadding.top;
mMargins.bottom = mSweepPadding.bottom;
}
mContentOffset.set(0, 0, 0, 0);
// make touch target area larger
final int widthBefore = getMeasuredWidth();
final int heightBefore = getMeasuredHeight();
if (mFollowAxis == HORIZONTAL) {
final int widthAfter = widthBefore * 3;
setMeasuredDimension(widthAfter, heightBefore);
mContentOffset.left = (widthAfter - widthBefore) / 2;
final int offset = mSweepPadding.bottom * 2;
mContentOffset.bottom -= offset;
mMargins.bottom += offset;
} else {
final int heightAfter = heightBefore * 2;
setMeasuredDimension(widthBefore, heightAfter);
mContentOffset.offset(0, (heightAfter - heightBefore) / 2);
final int offset = mSweepPadding.right * 2;
mContentOffset.right -= offset;
mMargins.right += offset;
}
mSweepOffset.offset(mContentOffset.left, mContentOffset.top);
mMargins.offset(-mSweepOffset.x, -mSweepOffset.y);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
invalidateLabelOffset();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int width = getWidth();
final int height = getHeight();
final int labelSize;
if (isEnabled() && mLabelLayout != null) {
final int count = canvas.save();
{
final float alignOffset = mLabelSize - LARGE_WIDTH;
canvas.translate(
mContentOffset.left + alignOffset, mContentOffset.top + mLabelOffset);
mLabelLayout.draw(canvas);
}
canvas.restoreToCount(count);
labelSize = (int) mLabelSize + mSafeRegion;
} else {
labelSize = 0;
}
if (mFollowAxis == VERTICAL) {
mSweep.setBounds(labelSize, mSweepOffset.y, width + mContentOffset.right,
mSweepOffset.y + mSweep.getIntrinsicHeight());
} else {
mSweep.setBounds(mSweepOffset.x, labelSize, mSweepOffset.x + mSweep.getIntrinsicWidth(),
height + mContentOffset.bottom);
}
mSweep.draw(canvas);
if (DRAW_OUTLINE) {
mOutlinePaint.setColor(Color.RED);
canvas.drawRect(0, 0, width, height, mOutlinePaint);
}
}
public static float getLabelTop(ChartSweepView view) {
return view.getY() + view.mContentOffset.top;
}
public static float getLabelBottom(ChartSweepView view) {
return getLabelTop(view) + view.mLabelLayout.getHeight();
}
public static float getLabelWidth(ChartSweepView view) {
return Layout.getDesiredWidth(view.mLabelLayout.getText(), view.mLabelLayout.getPaint());
}
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright (C) 2011 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.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewDebug;
import android.widget.FrameLayout;
import com.android.internal.util.Preconditions;
import com.android.settings.R;
/**
* Container for two-dimensional chart, drawn with a combination of
* {@link ChartGridView} and {@link ChartSweepView} children. The entire chart uses
* {@link ChartAxis} to map between raw values
* and screen coordinates.
*/
public class ChartView extends FrameLayout {
// TODO: extend something that supports two-dimensional scrolling
private static final int SWEEP_GRAVITY = Gravity.TOP | Gravity.START;
ChartAxis mHoriz;
ChartAxis mVert;
@ViewDebug.ExportedProperty
private int mOptimalWidth = -1;
private float mOptimalWidthWeight = 0;
private Rect mContent = new Rect();
public ChartView(Context context) {
this(context, null, 0);
}
public ChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ChartView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ChartView, defStyle, 0);
setOptimalWidth(a.getDimensionPixelSize(R.styleable.ChartView_optimalWidth, -1),
a.getFloat(R.styleable.ChartView_optimalWidthWeight, 0));
a.recycle();
setClipToPadding(false);
setClipChildren(false);
}
void init(ChartAxis horiz, ChartAxis vert) {
mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
mVert = Preconditions.checkNotNull(vert, "missing vert");
}
public void setOptimalWidth(int optimalWidth, float optimalWidthWeight) {
mOptimalWidth = optimalWidth;
mOptimalWidthWeight = optimalWidthWeight;
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int slack = getMeasuredWidth() - mOptimalWidth;
if (mOptimalWidth > 0 && slack > 0) {
final int targetWidth = (int) (mOptimalWidth + (slack * mOptimalWidthWeight));
widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mContent.set(getPaddingLeft(), getPaddingTop(), r - l - getPaddingRight(),
b - t - getPaddingBottom());
final int width = mContent.width();
final int height = mContent.height();
// no scrolling yet, so tell dimensions to fill exactly
mHoriz.setSize(width);
mVert.setSize(height);
final Rect parentRect = new Rect();
final Rect childRect = new Rect();
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
final LayoutParams params = (LayoutParams) child.getLayoutParams();
parentRect.set(mContent);
if (child instanceof ChartGridView) {
// Grid uses some extra room for labels
Gravity.apply(params.gravity, width, height, parentRect, childRect);
child.layout(childRect.left, childRect.top, childRect.right,
childRect.bottom + child.getPaddingBottom());
} else if (child instanceof ChartSweepView) {
layoutSweep((ChartSweepView) child, parentRect, childRect);
child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
}
}
}
protected void layoutSweep(ChartSweepView sweep) {
final Rect parentRect = new Rect(mContent);
final Rect childRect = new Rect();
layoutSweep(sweep, parentRect, childRect);
sweep.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
}
protected void layoutSweep(ChartSweepView sweep, Rect parentRect, Rect childRect) {
final Rect sweepMargins = sweep.getMargins();
// sweep is always placed along specific dimension
if (sweep.getFollowAxis() == ChartSweepView.VERTICAL) {
parentRect.top += sweepMargins.top + (int) sweep.getPoint();
parentRect.bottom = parentRect.top;
parentRect.left += sweepMargins.left;
parentRect.right += sweepMargins.right;
Gravity.apply(SWEEP_GRAVITY, parentRect.width(), sweep.getMeasuredHeight(),
parentRect, childRect);
} else {
parentRect.left += sweepMargins.left + (int) sweep.getPoint();
parentRect.right = parentRect.left;
parentRect.top += sweepMargins.top;
parentRect.bottom += sweepMargins.bottom;
Gravity.apply(SWEEP_GRAVITY, sweep.getMeasuredWidth(), parentRect.height(),
parentRect, childRect);
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Checkable;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* A RelativeLayout which implements {@link Checkable}. With this implementation, it could be used
* in the list item layout for {@link android.widget.AbsListView} to change UI after item click.
* Its checked state would be propagated to the checkable child.
*
* <p>
* To support accessibility, the state description is from the checkable view and is
* changed with {@link #setChecked(boolean)}. We make the checkable child unclickable, unfocusable
* and non-important for accessibility, so that the announcement wouldn't include
* the checkable view.
* <
*/
public class CheckableRelativeLayout extends RelativeLayout implements Checkable {
private Checkable mCheckable;
private View mCheckableChild;
private boolean mChecked;
public CheckableRelativeLayout(Context context) {
super(context);
}
public CheckableRelativeLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
mCheckableChild = findFirstCheckableView(this);
if (mCheckableChild != null) {
mCheckableChild.setClickable(false);
mCheckableChild.setFocusable(false);
mCheckableChild.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
mCheckable = (Checkable) mCheckableChild;
mCheckable.setChecked(isChecked());
setStateDescriptionIfNeeded();
}
super.onFinishInflate();
}
@Nullable
private static View findFirstCheckableView(@NonNull ViewGroup viewGroup) {
final int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = viewGroup.getChildAt(i);
if (child instanceof Checkable) {
return child;
}
if (child instanceof ViewGroup) {
findFirstCheckableView((ViewGroup) child);
}
}
return null;
}
@Override
public void setChecked(boolean checked) {
if (mChecked != checked) {
mChecked = checked;
if (mCheckable != null) {
mCheckable.setChecked(checked);
}
}
setStateDescriptionIfNeeded();
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
}
private void setStateDescriptionIfNeeded() {
if (mCheckableChild == null) {
return;
}
setStateDescription(mCheckableChild.getStateDescription());
}
@Override
public boolean isChecked() {
return mChecked;
}
@Override
public void toggle() {
setChecked(!mChecked);
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
event.setChecked(mChecked);
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setChecked(mChecked);
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) 2017 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.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.SeekBar;
public class DefaultIndicatorSeekBar extends SeekBar {
private int mDefaultProgress = -1;
public DefaultIndicatorSeekBar(Context context) {
super(context);
}
public DefaultIndicatorSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DefaultIndicatorSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public DefaultIndicatorSeekBar(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
/**
* N.B. Only draws the default indicator tick mark, NOT equally spaced tick marks.
*/
@Override
protected void drawTickMarks(Canvas canvas) {
if (isEnabled() && mDefaultProgress <= getMax() && mDefaultProgress >= getMin()) {
final Drawable defaultIndicator = getTickMark();
// Adjust the drawable's bounds to center it at the point where it's drawn.
final int w = defaultIndicator.getIntrinsicWidth();
final int h = defaultIndicator.getIntrinsicHeight();
final int halfW = w >= 0 ? w / 2 : 1;
final int halfH = h >= 0 ? h / 2 : 1;
defaultIndicator.setBounds(-halfW, -halfH, halfW, halfH);
// This mimics the computation of the thumb position, to get the true "default."
final int availableWidth = getWidth() - mPaddingLeft - mPaddingRight;
final int range = getMax() - getMin();
final float scale = range > 0f ? mDefaultProgress / (float) range : 0f;
final int offset = (int) ((scale * availableWidth) + 0.5f);
final int indicatorPosition = isLayoutRtl() && getMirrorForRtl()
? availableWidth - offset + mPaddingRight : offset + mPaddingLeft;
final int saveCount = canvas.save();
canvas.translate(indicatorPosition, getHeight() / 2);
defaultIndicator.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
/**
* N.B. This sets the default *unadjusted* progress, i.e. in the SeekBar's [0 - max] terms.
*/
public void setDefaultProgress(int defaultProgress) {
if (mDefaultProgress != defaultProgress) {
mDefaultProgress = defaultProgress;
invalidate();
}
}
public int getDefaultProgress() {
return mDefaultProgress;
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright (C) 2018 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.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import androidx.preference.CheckBoxPreference;
import androidx.preference.PreferenceViewHolder;
/**
* A CheckboxPreference that can disable its checkbox separate from its text.
* Differs from CheckboxPreference.setDisabled() in that the text is not dimmed.
*/
public class DisabledCheckBoxPreference extends CheckBoxPreference {
private PreferenceViewHolder mViewHolder;
private View mCheckBox;
private boolean mEnabledCheckBox;
public DisabledCheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setupDisabledCheckBoxPreference(context, attrs, defStyleAttr, defStyleRes);
}
public DisabledCheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setupDisabledCheckBoxPreference(context, attrs, defStyleAttr, 0);
}
public DisabledCheckBoxPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setupDisabledCheckBoxPreference(context, attrs, 0, 0);
}
public DisabledCheckBoxPreference(Context context) {
super(context);
setupDisabledCheckBoxPreference(context, null, 0, 0);
}
private void setupDisabledCheckBoxPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.Preference, defStyleAttr, defStyleRes);
for (int i = a.getIndexCount() - 1; i >= 0; i--) {
int attr = a.getIndex(i);
switch (attr) {
case com.android.internal.R.styleable.Preference_enabled:
mEnabledCheckBox = a.getBoolean(attr, true);
break;
}
}
a.recycle();
// Always tell super class this preference is enabled.
// We manually enable/disable checkbox using enableCheckBox.
super.setEnabled(true);
enableCheckbox(mEnabledCheckBox);
}
public void enableCheckbox(boolean enabled) {
mEnabledCheckBox = enabled;
if (mViewHolder != null && mCheckBox != null) {
mCheckBox.setEnabled(mEnabledCheckBox);
mViewHolder.itemView.setEnabled(mEnabledCheckBox);
}
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
mViewHolder = holder;
mCheckBox = holder.findViewById(android.R.id.checkbox);
enableCheckbox(mEnabledCheckBox);
TextView title = (TextView) holder.findViewById(android.R.id.title);
if (title != null) {
title.setSingleLine(false);
title.setMaxLines(2);
title.setEllipsize(TextUtils.TruncateAt.END);
}
}
@Override
protected void performClick(View view) {
// only perform clicks if the checkbox is enabled
if (mEnabledCheckBox) {
super.performClick(view);
}
}
}

View File

@@ -0,0 +1,920 @@
/*
* Copyright (C) 2016 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.widget;
import static android.view.animation.AnimationUtils.loadInterpolator;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Interpolator;
import androidx.viewpager.widget.ViewPager;
import com.android.settings.R;
import java.util.Arrays;
/**
* Custom pager indicator for use with a {@code ViewPager}.
*/
public class DotsPageIndicator extends View implements ViewPager.OnPageChangeListener {
public static final String TAG = DotsPageIndicator.class.getSimpleName();
// defaults
private static final int DEFAULT_DOT_SIZE = 8; // dp
private static final int DEFAULT_GAP = 12; // dp
private static final int DEFAULT_ANIM_DURATION = 400; // ms
private static final int DEFAULT_UNSELECTED_COLOUR = 0x80ffffff; // 50% white
private static final int DEFAULT_SELECTED_COLOUR = 0xffffffff; // 100% white
// constants
private static final float INVALID_FRACTION = -1f;
private static final float MINIMAL_REVEAL = 0.00001f;
// configurable attributes
private int dotDiameter;
private int gap;
private long animDuration;
private int unselectedColour;
private int selectedColour;
// derived from attributes
private float dotRadius;
private float halfDotRadius;
private long animHalfDuration;
private float dotTopY;
private float dotCenterY;
private float dotBottomY;
// ViewPager
private ViewPager viewPager;
private ViewPager.OnPageChangeListener pageChangeListener;
// state
private int pageCount;
private int currentPage;
private float selectedDotX;
private boolean selectedDotInPosition;
private float[] dotCenterX;
private float[] joiningFractions;
private float retreatingJoinX1;
private float retreatingJoinX2;
private float[] dotRevealFractions;
private boolean attachedState;
// drawing
private final Paint unselectedPaint;
private final Paint selectedPaint;
private final Path combinedUnselectedPath;
private final Path unselectedDotPath;
private final Path unselectedDotLeftPath;
private final Path unselectedDotRightPath;
private final RectF rectF;
// animation
private ValueAnimator moveAnimation;
private ValueAnimator[] joiningAnimations;
private AnimatorSet joiningAnimationSet;
private PendingRetreatAnimator retreatAnimation;
private PendingRevealAnimator[] revealAnimations;
private final Interpolator interpolator;
// working values for beziers
float endX1;
float endY1;
float endX2;
float endY2;
float controlX1;
float controlY1;
float controlX2;
float controlY2;
public DotsPageIndicator(Context context) {
this(context, null, 0);
}
public DotsPageIndicator(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DotsPageIndicator(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final int scaledDensity = (int) context.getResources().getDisplayMetrics().scaledDensity;
// Load attributes
final TypedArray typedArray = getContext().obtainStyledAttributes(
attrs, R.styleable.DotsPageIndicator, defStyle, 0);
dotDiameter = typedArray.getDimensionPixelSize(R.styleable.DotsPageIndicator_dotDiameter,
DEFAULT_DOT_SIZE * scaledDensity);
dotRadius = dotDiameter / 2;
halfDotRadius = dotRadius / 2;
gap = typedArray.getDimensionPixelSize(R.styleable.DotsPageIndicator_dotGap,
DEFAULT_GAP * scaledDensity);
animDuration = (long) typedArray.getInteger(R.styleable.DotsPageIndicator_animationDuration,
DEFAULT_ANIM_DURATION);
animHalfDuration = animDuration / 2;
unselectedColour = typedArray.getColor(R.styleable.DotsPageIndicator_pageIndicatorColor,
DEFAULT_UNSELECTED_COLOUR);
selectedColour = typedArray.getColor(R.styleable.DotsPageIndicator_currentPageIndicatorColor,
DEFAULT_SELECTED_COLOUR);
typedArray.recycle();
unselectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
unselectedPaint.setColor(unselectedColour);
selectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
selectedPaint.setColor(selectedColour);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
interpolator = loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
} else {
interpolator = loadInterpolator(context, android.R.anim.accelerate_decelerate_interpolator);
}
// create paths & rect now reuse & rewind later
combinedUnselectedPath = new Path();
unselectedDotPath = new Path();
unselectedDotLeftPath = new Path();
unselectedDotRightPath = new Path();
rectF = new RectF();
addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
attachedState = true;
}
@Override
public void onViewDetachedFromWindow(View v) {
attachedState = false;
}
});
}
public void setViewPager(ViewPager viewPager) {
this.viewPager = viewPager;
viewPager.setOnPageChangeListener(this);
setPageCount(viewPager.getAdapter().getCount());
viewPager.getAdapter().registerDataSetObserver(new DataSetObserver() {
@Override
public void onChanged() {
setPageCount(DotsPageIndicator.this.viewPager.getAdapter().getCount());
}
});
setCurrentPageImmediate();
}
/***
* As this class <b>must</b> act as the {@link ViewPager.OnPageChangeListener} for the ViewPager
* (as set by {@link #setViewPager(androidx.viewpager.widget.ViewPager)}). Applications may set a
* listener here to be notified of the ViewPager events.
*
* @param onPageChangeListener
*/
public void setOnPageChangeListener(ViewPager.OnPageChangeListener onPageChangeListener) {
pageChangeListener = onPageChangeListener;
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// nothing to do just forward onward to any registered listener
if (pageChangeListener != null) {
pageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
}
@Override
public void onPageSelected(int position) {
if (attachedState) {
// this is the main event we're interested in!
setSelectedPage(position);
} else {
// when not attached, don't animate the move, just store immediately
setCurrentPageImmediate();
}
// forward onward to any registered listener
if (pageChangeListener != null) {
pageChangeListener.onPageSelected(position);
}
}
@Override
public void onPageScrollStateChanged(int state) {
// nothing to do just forward onward to any registered listener
if (pageChangeListener != null) {
pageChangeListener.onPageScrollStateChanged(state);
}
}
private void setPageCount(int pages) {
pageCount = pages;
calculateDotPositions();
resetState();
}
private void calculateDotPositions() {
int left = getPaddingLeft();
int top = getPaddingTop();
int right = getWidth() - getPaddingRight();
int requiredWidth = getRequiredWidth();
float startLeft = left + ((right - left - requiredWidth) / 2) + dotRadius;
dotCenterX = new float[pageCount];
for (int i = 0; i < pageCount; i++) {
dotCenterX[i] = startLeft + i * (dotDiameter + gap);
}
// todo just top aligning for now… should make this smarter
dotTopY = top;
dotCenterY = top + dotRadius;
dotBottomY = top + dotDiameter;
setCurrentPageImmediate();
}
private void setCurrentPageImmediate() {
if (viewPager != null) {
currentPage = viewPager.getCurrentItem();
} else {
currentPage = 0;
}
if (pageCount > 0) {
selectedDotX = dotCenterX[currentPage];
}
}
private void resetState() {
if (pageCount > 0) {
joiningFractions = new float[pageCount - 1];
Arrays.fill(joiningFractions, 0f);
dotRevealFractions = new float[pageCount];
Arrays.fill(dotRevealFractions, 0f);
retreatingJoinX1 = INVALID_FRACTION;
retreatingJoinX2 = INVALID_FRACTION;
selectedDotInPosition = true;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desiredHeight = getDesiredHeight();
int height;
switch (MeasureSpec.getMode(heightMeasureSpec)) {
case MeasureSpec.EXACTLY:
height = MeasureSpec.getSize(heightMeasureSpec);
break;
case MeasureSpec.AT_MOST:
height = Math.min(desiredHeight, MeasureSpec.getSize(heightMeasureSpec));
break;
default: // MeasureSpec.UNSPECIFIED
height = desiredHeight;
break;
}
int desiredWidth = getDesiredWidth();
int width;
switch (MeasureSpec.getMode(widthMeasureSpec)) {
case MeasureSpec.EXACTLY:
width = MeasureSpec.getSize(widthMeasureSpec);
break;
case MeasureSpec.AT_MOST:
width = Math.min(desiredWidth, MeasureSpec.getSize(widthMeasureSpec));
break;
default: // MeasureSpec.UNSPECIFIED
width = desiredWidth;
break;
}
setMeasuredDimension(width, height);
calculateDotPositions();
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
setMeasuredDimension(width, height);
calculateDotPositions();
}
@Override
public void clearAnimation() {
super.clearAnimation();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
cancelRunningAnimations();
}
}
private int getDesiredHeight() {
return getPaddingTop() + dotDiameter + getPaddingBottom();
}
private int getRequiredWidth() {
return pageCount * dotDiameter + (pageCount - 1) * gap;
}
private int getDesiredWidth() {
return getPaddingLeft() + getRequiredWidth() + getPaddingRight();
}
@Override
protected void onDraw(Canvas canvas) {
if (viewPager == null || pageCount == 0) {
return;
}
drawUnselected(canvas);
drawSelected(canvas);
}
private void drawUnselected(Canvas canvas) {
combinedUnselectedPath.rewind();
// draw any settled, revealing or joining dots
for (int page = 0; page < pageCount; page++) {
int nextXIndex = page == pageCount - 1 ? page : page + 1;
// todo Path.op should be supported in KitKat but causes the app to hang for Nexus 5.
// For now disabling for all pre-L devices.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Path unselectedPath = getUnselectedPath(page,
dotCenterX[page],
dotCenterX[nextXIndex],
page == pageCount - 1 ? INVALID_FRACTION : joiningFractions[page],
dotRevealFractions[page]);
combinedUnselectedPath.op(unselectedPath, Path.Op.UNION);
} else {
canvas.drawCircle(dotCenterX[page], dotCenterY, dotRadius, unselectedPaint);
}
}
// draw any retreating joins
if (retreatingJoinX1 != INVALID_FRACTION) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
combinedUnselectedPath.op(getRetreatingJoinPath(), Path.Op.UNION);
}
}
canvas.drawPath(combinedUnselectedPath, unselectedPaint);
}
/**
* Unselected dots can be in 6 states:
*
* #1 At rest
* #2 Joining neighbour, still separate
* #3 Joining neighbour, combined curved
* #4 Joining neighbour, combined straight
* #5 Join retreating
* #6 Dot re-showing / revealing
*
* It can also be in a combination of these states e.g. joining one neighbour while
* retreating from another. We therefore create a Path so that we can examine each
* dot pair separately and later take the union for these cases.
*
* This function returns a path for the given dot **and any action to it's right** e.g. joining
* or retreating from it's neighbour
*
* @param page
*/
private Path getUnselectedPath(int page,
float centerX,
float nextCenterX,
float joiningFraction,
float dotRevealFraction) {
unselectedDotPath.rewind();
if ((joiningFraction == 0f || joiningFraction == INVALID_FRACTION)
&& dotRevealFraction == 0f
&& !(page == currentPage && selectedDotInPosition == true)) {
// case #1 At rest
unselectedDotPath.addCircle(dotCenterX[page], dotCenterY, dotRadius, Path.Direction.CW);
}
if (joiningFraction > 0f && joiningFraction < 0.5f && retreatingJoinX1 == INVALID_FRACTION) {
// case #2 Joining neighbour, still separate
// start with the left dot
unselectedDotLeftPath.rewind();
// start at the bottom center
unselectedDotLeftPath.moveTo(centerX, dotBottomY);
// semi circle to the top center
rectF.set(centerX - dotRadius, dotTopY, centerX + dotRadius, dotBottomY);
unselectedDotLeftPath.arcTo(rectF, 90, 180, true);
// cubic to the right middle
endX1 = centerX + dotRadius + (joiningFraction * gap);
endY1 = dotCenterY;
controlX1 = centerX + halfDotRadius;
controlY1 = dotTopY;
controlX2 = endX1;
controlY2 = endY1 - halfDotRadius;
unselectedDotLeftPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX1, endY1);
// cubic back to the bottom center
endX2 = centerX;
endY2 = dotBottomY;
controlX1 = endX1;
controlY1 = endY1 + halfDotRadius;
controlX2 = centerX + halfDotRadius;
controlY2 = dotBottomY;
unselectedDotLeftPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX2, endY2);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
unselectedDotPath.op(unselectedDotLeftPath, Path.Op.UNION);
}
// now do the next dot to the right
unselectedDotRightPath.rewind();
// start at the bottom center
unselectedDotRightPath.moveTo(nextCenterX, dotBottomY);
// semi circle to the top center
rectF.set(nextCenterX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY);
unselectedDotRightPath.arcTo(rectF, 90, -180, true);
// cubic to the left middle
endX1 = nextCenterX - dotRadius - (joiningFraction * gap);
endY1 = dotCenterY;
controlX1 = nextCenterX - halfDotRadius;
controlY1 = dotTopY;
controlX2 = endX1;
controlY2 = endY1 - halfDotRadius;
unselectedDotRightPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX1, endY1);
// cubic back to the bottom center
endX2 = nextCenterX;
endY2 = dotBottomY;
controlX1 = endX1;
controlY1 = endY1 + halfDotRadius;
controlX2 = endX2 - halfDotRadius;
controlY2 = dotBottomY;
unselectedDotRightPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX2, endY2);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
unselectedDotPath.op(unselectedDotRightPath, Path.Op.UNION);
}
}
if (joiningFraction > 0.5f && joiningFraction < 1f && retreatingJoinX1 == INVALID_FRACTION) {
// case #3 Joining neighbour, combined curved
// start in the bottom left
unselectedDotPath.moveTo(centerX, dotBottomY);
// semi-circle to the top left
rectF.set(centerX - dotRadius, dotTopY, centerX + dotRadius, dotBottomY);
unselectedDotPath.arcTo(rectF, 90, 180, true);
// bezier to the middle top of the join
endX1 = centerX + dotRadius + (gap / 2);
endY1 = dotCenterY - (joiningFraction * dotRadius);
controlX1 = endX1 - (joiningFraction * dotRadius);
controlY1 = dotTopY;
controlX2 = endX1 - ((1 - joiningFraction) * dotRadius);
controlY2 = endY1;
unselectedDotPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX1, endY1);
// bezier to the top right of the join
endX2 = nextCenterX;
endY2 = dotTopY;
controlX1 = endX1 + ((1 - joiningFraction) * dotRadius);
controlY1 = endY1;
controlX2 = endX1 + (joiningFraction * dotRadius);
controlY2 = dotTopY;
unselectedDotPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX2, endY2);
// semi-circle to the bottom right
rectF.set(nextCenterX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY);
unselectedDotPath.arcTo(rectF, 270, 180, true);
// bezier to the middle bottom of the join
// endX1 stays the same
endY1 = dotCenterY + (joiningFraction * dotRadius);
controlX1 = endX1 + (joiningFraction * dotRadius);
controlY1 = dotBottomY;
controlX2 = endX1 + ((1 - joiningFraction) * dotRadius);
controlY2 = endY1;
unselectedDotPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX1, endY1);
// bezier back to the start point in the bottom left
endX2 = centerX;
endY2 = dotBottomY;
controlX1 = endX1 - ((1 - joiningFraction) * dotRadius);
controlY1 = endY1;
controlX2 = endX1 - (joiningFraction * dotRadius);
controlY2 = endY2;
unselectedDotPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX2, endY2);
}
if (joiningFraction == 1 && retreatingJoinX1 == INVALID_FRACTION) {
// case #4 Joining neighbour, combined straight
// technically we could use case 3 for this situation as well
// but assume that this is an optimization rather than faffing around with beziers
// just to draw a rounded rect
rectF.set(centerX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY);
unselectedDotPath.addRoundRect(rectF, dotRadius, dotRadius, Path.Direction.CW);
}
// case #5 is handled by #getRetreatingJoinPath()
// this is done separately so that we can have a single retreating path spanning
// multiple dots and therefore animate it's movement smoothly
if (dotRevealFraction > MINIMAL_REVEAL) {
// case #6 previously hidden dot revealing
unselectedDotPath.addCircle(centerX, dotCenterY, dotRevealFraction * dotRadius,
Path.Direction.CW);
}
return unselectedDotPath;
}
private Path getRetreatingJoinPath() {
unselectedDotPath.rewind();
rectF.set(retreatingJoinX1, dotTopY, retreatingJoinX2, dotBottomY);
unselectedDotPath.addRoundRect(rectF, dotRadius, dotRadius, Path.Direction.CW);
return unselectedDotPath;
}
private void drawSelected(Canvas canvas) {
canvas.drawCircle(selectedDotX, dotCenterY, dotRadius, selectedPaint);
}
private void setSelectedPage(int now) {
if (now == currentPage || pageCount == 0) {
return;
}
int was = currentPage;
currentPage = now;
// These animations are not supported in pre-JB versions.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
cancelRunningAnimations();
// create the anim to move the selected dot this animator will kick off
// retreat animations when it has moved 75% of the way.
// The retreat animation in turn will kick of reveal anims when the
// retreat has passed any dots to be revealed
final int steps = Math.abs(now - was);
moveAnimation = createMoveSelectedAnimator(dotCenterX[now], was, now, steps);
// create animators for joining the dots. This runs independently of the above and relies
// on good timing. Like comedy.
// if joining multiple dots, each dot after the first is delayed by 1/8 of the duration
joiningAnimations = new ValueAnimator[steps];
for (int i = 0; i < steps; i++) {
joiningAnimations[i] = createJoiningAnimator(now > was ? was + i : was - 1 - i,
i * (animDuration / 8L));
}
moveAnimation.start();
startJoiningAnimations();
} else {
setCurrentPageImmediate();
invalidate();
}
}
private ValueAnimator createMoveSelectedAnimator(final float moveTo, int was, int now,
int steps) {
// create the actual move animator
ValueAnimator moveSelected = ValueAnimator.ofFloat(selectedDotX, moveTo);
// also set up a pending retreat anim this starts when the move is 75% complete
retreatAnimation = new PendingRetreatAnimator(was, now, steps,
now > was
? new RightwardStartPredicate(moveTo - ((moveTo - selectedDotX) * 0.25f))
: new LeftwardStartPredicate(moveTo + ((selectedDotX - moveTo) * 0.25f)));
moveSelected.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// todo avoid autoboxing
selectedDotX = (Float) valueAnimator.getAnimatedValue();
retreatAnimation.startIfNecessary(selectedDotX);
postInvalidateOnAnimation();
}
});
moveSelected.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
// set a flag so that we continue to draw the unselected dot in the target position
// until the selected dot has finished moving into place
selectedDotInPosition = false;
}
@Override
public void onAnimationEnd(Animator animation) {
// set a flag when anim finishes so that we don't draw both selected & unselected
// page dots
selectedDotInPosition = true;
}
});
// slightly delay the start to give the joins a chance to run
// unless dot isn't in position yet then don't delay!
moveSelected.setStartDelay(selectedDotInPosition ? animDuration / 4L : 0L);
moveSelected.setDuration(animDuration * 3L / 4L);
moveSelected.setInterpolator(interpolator);
return moveSelected;
}
private ValueAnimator createJoiningAnimator(final int leftJoiningDot, final long startDelay) {
// animate the joining fraction for the given dot
ValueAnimator joining = ValueAnimator.ofFloat(0f, 1.0f);
joining.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
setJoiningFraction(leftJoiningDot, valueAnimator.getAnimatedFraction());
}
});
joining.setDuration(animHalfDuration);
joining.setStartDelay(startDelay);
joining.setInterpolator(interpolator);
return joining;
}
private void setJoiningFraction(int leftDot, float fraction) {
joiningFractions[leftDot] = fraction;
postInvalidateOnAnimation();
}
private void clearJoiningFractions() {
Arrays.fill(joiningFractions, 0f);
postInvalidateOnAnimation();
}
private void setDotRevealFraction(int dot, float fraction) {
dotRevealFractions[dot] = fraction;
postInvalidateOnAnimation();
}
private void cancelRunningAnimations() {
cancelMoveAnimation();
cancelJoiningAnimations();
cancelRetreatAnimation();
cancelRevealAnimations();
resetState();
}
private void cancelMoveAnimation() {
if (moveAnimation != null && moveAnimation.isRunning()) {
moveAnimation.cancel();
}
}
private void startJoiningAnimations() {
joiningAnimationSet = new AnimatorSet();
joiningAnimationSet.playTogether(joiningAnimations);
joiningAnimationSet.start();
}
private void cancelJoiningAnimations() {
if (joiningAnimationSet != null && joiningAnimationSet.isRunning()) {
joiningAnimationSet.cancel();
}
}
private void cancelRetreatAnimation() {
if (retreatAnimation != null && retreatAnimation.isRunning()) {
retreatAnimation.cancel();
}
}
private void cancelRevealAnimations() {
if (revealAnimations != null) {
for (PendingRevealAnimator reveal : revealAnimations) {
reveal.cancel();
}
}
}
int getUnselectedColour() {
return unselectedColour;
}
int getSelectedColour() {
return selectedColour;
}
float getDotCenterY() {
return dotCenterY;
}
float getDotCenterX(int page) {
return dotCenterX[page];
}
float getSelectedDotX() {
return selectedDotX;
}
int getCurrentPage() {
return currentPage;
}
/**
* A {@link android.animation.ValueAnimator} that starts once a given predicate returns true.
*/
public abstract class PendingStartAnimator extends ValueAnimator {
protected boolean hasStarted;
protected StartPredicate predicate;
public PendingStartAnimator(StartPredicate predicate) {
super();
this.predicate = predicate;
hasStarted = false;
}
public void startIfNecessary(float currentValue) {
if (!hasStarted && predicate.shouldStart(currentValue)) {
start();
hasStarted = true;
}
}
}
/**
* An Animator that shows and then shrinks a retreating join between the previous and newly
* selected pages. This also sets up some pending dot reveals to be started when the retreat
* has passed the dot to be revealed.
*/
public class PendingRetreatAnimator extends PendingStartAnimator {
public PendingRetreatAnimator(int was, int now, int steps, StartPredicate predicate) {
super(predicate);
setDuration(animHalfDuration);
setInterpolator(interpolator);
// work out the start/end values of the retreating join from the direction we're
// travelling in. Also look at the current selected dot position, i.e. we're moving on
// before a prior anim has finished.
final float initialX1 = now > was ? Math.min(dotCenterX[was], selectedDotX) - dotRadius
: dotCenterX[now] - dotRadius;
final float finalX1 = now > was ? dotCenterX[now] - dotRadius
: dotCenterX[now] - dotRadius;
final float initialX2 = now > was ? dotCenterX[now] + dotRadius
: Math.max(dotCenterX[was], selectedDotX) + dotRadius;
final float finalX2 = now > was ? dotCenterX[now] + dotRadius
: dotCenterX[now] + dotRadius;
revealAnimations = new PendingRevealAnimator[steps];
// hold on to the indexes of the dots that will be hidden by the retreat so that
// we can initialize their revealFraction's i.e. make sure they're hidden while the
// reveal animation runs
final int[] dotsToHide = new int[steps];
if (initialX1 != finalX1) { // rightward retreat
setFloatValues(initialX1, finalX1);
// create the reveal animations that will run when the retreat passes them
for (int i = 0; i < steps; i++) {
revealAnimations[i] = new PendingRevealAnimator(was + i,
new RightwardStartPredicate(dotCenterX[was + i]));
dotsToHide[i] = was + i;
}
addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// todo avoid autoboxing
retreatingJoinX1 = (Float) valueAnimator.getAnimatedValue();
postInvalidateOnAnimation();
// start any reveal animations if we've passed them
for (PendingRevealAnimator pendingReveal : revealAnimations) {
pendingReveal.startIfNecessary(retreatingJoinX1);
}
}
});
} else { // (initialX2 != finalX2) leftward retreat
setFloatValues(initialX2, finalX2);
// create the reveal animations that will run when the retreat passes them
for (int i = 0; i < steps; i++) {
revealAnimations[i] = new PendingRevealAnimator(was - i,
new LeftwardStartPredicate(dotCenterX[was - i]));
dotsToHide[i] = was - i;
}
addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// todo avoid autoboxing
retreatingJoinX2 = (Float) valueAnimator.getAnimatedValue();
postInvalidateOnAnimation();
// start any reveal animations if we've passed them
for (PendingRevealAnimator pendingReveal : revealAnimations) {
pendingReveal.startIfNecessary(retreatingJoinX2);
}
}
});
}
addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
cancelJoiningAnimations();
clearJoiningFractions();
// we need to set this so that the dots are hidden until the reveal anim runs
for (int dot : dotsToHide) {
setDotRevealFraction(dot, MINIMAL_REVEAL);
}
retreatingJoinX1 = initialX1;
retreatingJoinX2 = initialX2;
postInvalidateOnAnimation();
}
@Override
public void onAnimationEnd(Animator animation) {
retreatingJoinX1 = INVALID_FRACTION;
retreatingJoinX2 = INVALID_FRACTION;
postInvalidateOnAnimation();
}
});
}
}
/**
* An Animator that animates a given dot's revealFraction i.e. scales it up
*/
public class PendingRevealAnimator extends PendingStartAnimator {
private final int dot;
public PendingRevealAnimator(int dot, StartPredicate predicate) {
super(predicate);
this.dot = dot;
setFloatValues(MINIMAL_REVEAL, 1f);
setDuration(animHalfDuration);
setInterpolator(interpolator);
addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// todo avoid autoboxing
setDotRevealFraction(PendingRevealAnimator.this.dot,
(Float) valueAnimator.getAnimatedValue());
}
});
addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
setDotRevealFraction(PendingRevealAnimator.this.dot, 0f);
postInvalidateOnAnimation();
}
});
}
}
/**
* A predicate used to start an animation when a test passes
*/
public abstract class StartPredicate {
protected float thresholdValue;
public StartPredicate(float thresholdValue) {
this.thresholdValue = thresholdValue;
}
abstract boolean shouldStart(float currentValue);
}
/**
* A predicate used to start an animation when a given value is greater than a threshold
*/
public class RightwardStartPredicate extends StartPredicate {
public RightwardStartPredicate(float thresholdValue) {
super(thresholdValue);
}
boolean shouldStart(float currentValue) {
return currentValue > thresholdValue;
}
}
/**
* A predicate used to start an animation then a given value is less than a threshold
*/
public class LeftwardStartPredicate extends StartPredicate {
public LeftwardStartPredicate(float thresholdValue) {
super(thresholdValue);
}
boolean shouldStart(float currentValue) {
return currentValue < thresholdValue;
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2015 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.widget;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
public abstract class EmptyTextSettings extends SettingsPreferenceFragment {
private TextView mEmpty;
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mEmpty = new TextView(getContext());
mEmpty.setGravity(Gravity.CENTER);
final int textPadding = getContext().getResources().getDimensionPixelSize(
R.dimen.empty_text_padding);
mEmpty.setPadding(textPadding, 0 /* top */, textPadding, 0 /* bottom */);
TypedValue value = new TypedValue();
getContext().getTheme().resolveAttribute(android.R.attr.textAppearanceMedium, value, true);
mEmpty.setTextAppearance(value.resourceId);
final int layoutHeight = getContext().getResources()
.getDimensionPixelSize(R.dimen.empty_text_layout_height);
((ViewGroup) view.findViewById(android.R.id.list_container)).addView(mEmpty,
new LayoutParams(LayoutParams.MATCH_PARENT, layoutHeight));
setEmptyView(mEmpty);
}
protected void setEmptyText(int text) {
mEmpty.setText(text);
}
}

View File

@@ -0,0 +1,336 @@
/*
* Copyright (C) 2016 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.widget;
import static com.android.settings.spa.app.appinfo.AppInfoSettingsProvider.startAppInfoSettings;
import android.annotation.IdRes;
import android.annotation.UserIdInt;
import android.app.Activity;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.widget.LayoutPreference;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class EntityHeaderController {
@IntDef({ActionType.ACTION_NONE,
ActionType.ACTION_NOTIF_PREFERENCE,
ActionType.ACTION_EDIT_PREFERENCE,})
@Retention(RetentionPolicy.SOURCE)
public @interface ActionType {
int ACTION_NONE = 0;
int ACTION_NOTIF_PREFERENCE = 1;
int ACTION_EDIT_PREFERENCE = 2;
}
public static final String PREF_KEY_APP_HEADER = "pref_app_header";
private static final String TAG = "AppDetailFeature";
private final Context mAppContext;
private final Fragment mFragment;
private final int mMetricsCategory;
private final View mHeader;
private Drawable mIcon;
private int mPrefOrder = -1000;
private String mIconContentDescription;
private CharSequence mLabel;
private CharSequence mSummary;
// Required for hearing aid devices.
private CharSequence mSecondSummary;
private String mPackageName;
private Intent mAppNotifPrefIntent;
@UserIdInt
private int mUid = UserHandle.USER_NULL;
@ActionType
private int mAction1;
@ActionType
private int mAction2;
private boolean mHasAppInfoLink;
private boolean mIsInstantApp;
private View.OnClickListener mEditOnClickListener;
/**
* Creates a new instance of the controller.
*
* @param fragment The fragment that header will be placed in.
* @param header Optional: header view if it's already created.
*/
public static EntityHeaderController newInstance(Activity activity, Fragment fragment,
View header) {
return new EntityHeaderController(activity, fragment, header);
}
private EntityHeaderController(Activity activity, Fragment fragment, View header) {
mAppContext = activity.getApplicationContext();
mFragment = fragment;
mMetricsCategory = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
.getMetricsCategory(fragment);
if (header != null) {
mHeader = header;
} else {
mHeader = LayoutInflater.from(fragment.getContext())
.inflate(com.android.settingslib.widget.preference.layout.R.layout.settings_entity_header,
null /* root */);
}
}
/**
* Set the icon in the header. Callers should also consider calling setIconContentDescription
* to provide a description of this icon for accessibility purposes.
*/
public EntityHeaderController setIcon(Drawable icon) {
if (icon != null) {
final Drawable.ConstantState state = icon.getConstantState();
mIcon = state != null ? state.newDrawable(mAppContext.getResources()) : icon;
}
return this;
}
/**
* Convenience method to set the header icon from an ApplicationsState.AppEntry. Callers should
* also consider calling setIconContentDescription to provide a description of this icon for
* accessibility purposes.
*/
public EntityHeaderController setIcon(ApplicationsState.AppEntry appEntry) {
mIcon = Utils.getBadgedIcon(mAppContext, appEntry.info);
return this;
}
public EntityHeaderController setIconContentDescription(String contentDescription) {
mIconContentDescription = contentDescription;
return this;
}
public EntityHeaderController setLabel(CharSequence label) {
mLabel = label;
return this;
}
public EntityHeaderController setLabel(ApplicationsState.AppEntry appEntry) {
mLabel = appEntry.label;
return this;
}
public EntityHeaderController setSummary(CharSequence summary) {
mSummary = summary;
return this;
}
public EntityHeaderController setSummary(PackageInfo packageInfo) {
if (packageInfo != null) {
mSummary = packageInfo.versionName;
}
return this;
}
public EntityHeaderController setSecondSummary(CharSequence summary) {
mSecondSummary = summary;
return this;
}
public EntityHeaderController setHasAppInfoLink(boolean hasAppInfoLink) {
mHasAppInfoLink = hasAppInfoLink;
return this;
}
public EntityHeaderController setButtonActions(@ActionType int action1,
@ActionType int action2) {
mAction1 = action1;
mAction2 = action2;
return this;
}
public EntityHeaderController setPackageName(String packageName) {
mPackageName = packageName;
return this;
}
public EntityHeaderController setUid(int uid) {
mUid = uid;
return this;
}
public EntityHeaderController setAppNotifPrefIntent(Intent appNotifPrefIntent) {
mAppNotifPrefIntent = appNotifPrefIntent;
return this;
}
public EntityHeaderController setIsInstantApp(boolean isInstantApp) {
mIsInstantApp = isInstantApp;
return this;
}
public EntityHeaderController setEditListener(View.OnClickListener listener) {
mEditOnClickListener = listener;
return this;
}
/** Sets this preference order. */
public EntityHeaderController setOrder(int order) {
mPrefOrder = order;
return this;
}
/**
* Done mutating entity header, rebinds everything and return a new {@link LayoutPreference}.
*/
public LayoutPreference done(Context uiContext) {
final LayoutPreference pref = new LayoutPreference(uiContext, done());
// Makes sure it's the first preference onscreen.
pref.setOrder(mPrefOrder);
pref.setSelectable(false);
pref.setKey(PREF_KEY_APP_HEADER);
pref.setAllowDividerBelow(true);
return pref;
}
/**
* Done mutating entity header, rebinds everything (optionally skip rebinding buttons).
*/
public View done(boolean rebindActions) {
ImageView iconView = mHeader.findViewById(R.id.entity_header_icon);
if (iconView != null) {
iconView.setImageDrawable(mIcon);
iconView.setContentDescription(mIconContentDescription);
}
setText(R.id.entity_header_title, mLabel);
setText(R.id.entity_header_summary, mSummary);
setText(com.android.settingslib.widget.preference.layout.R.id.entity_header_second_summary, mSecondSummary);
if (mIsInstantApp) {
setText(com.android.settingslib.widget.preference.layout.R.id.install_type,
mHeader.getResources().getString(R.string.install_type_instant));
}
if (rebindActions) {
bindHeaderButtons();
}
return mHeader;
}
/**
* Only binds entity header with button actions.
*/
public EntityHeaderController bindHeaderButtons() {
final View entityHeaderContent = mHeader.findViewById(
com.android.settingslib.widget.preference.layout.R.id.entity_header_content);
final ImageButton button1 = mHeader.findViewById(android.R.id.button1);
final ImageButton button2 = mHeader.findViewById(android.R.id.button2);
bindAppInfoLink(entityHeaderContent);
bindButton(button1, mAction1);
bindButton(button2, mAction2);
return this;
}
private void bindAppInfoLink(View entityHeaderContent) {
if (!mHasAppInfoLink) {
// Caller didn't ask for app link, skip.
return;
}
if (entityHeaderContent == null
|| mPackageName == null
|| mPackageName.equals(Utils.OS_PKG)
|| mUid == UserHandle.USER_NULL) {
Log.w(TAG, "Missing ingredients to build app info link, skip");
return;
}
entityHeaderContent.setOnClickListener(v -> startAppInfoSettings(
mPackageName, mUid, mFragment, 0 /* request */,
mMetricsCategory));
}
/**
* Done mutating entity header, rebinds everything.
*/
@VisibleForTesting
View done() {
return done(true /* rebindActions */);
}
private void bindButton(ImageButton button, @ActionType int action) {
if (button == null) {
return;
}
switch (action) {
case ActionType.ACTION_EDIT_PREFERENCE: {
if (mEditOnClickListener == null) {
button.setVisibility(View.GONE);
} else {
button.setImageResource(com.android.internal.R.drawable.ic_mode_edit);
button.setVisibility(View.VISIBLE);
button.setOnClickListener(mEditOnClickListener);
}
return;
}
case ActionType.ACTION_NOTIF_PREFERENCE: {
if (mAppNotifPrefIntent == null) {
button.setVisibility(View.GONE);
} else {
button.setOnClickListener(v -> {
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
.action(SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_OPEN_APP_NOTIFICATION_SETTING,
mMetricsCategory,
null, 0);
mFragment.startActivity(mAppNotifPrefIntent);
});
button.setVisibility(View.VISIBLE);
}
return;
}
case ActionType.ACTION_NONE: {
button.setVisibility(View.GONE);
return;
}
}
}
private void setText(@IdRes int id, CharSequence text) {
TextView textView = mHeader.findViewById(id);
if (textView != null) {
textView.setText(text);
textView.setVisibility(TextUtils.isEmpty(text) ? View.GONE : View.VISIBLE);
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2020 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.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.preference.PreferenceViewHolder;
import com.android.settingslib.RestrictedSwitchPreference;
/**
* This widget with enabled filterTouchesWhenObscured attribute use to replace
* the {@link RestrictedSwitchPreference} in the Special access app pages for
* security.
*/
public class FilterTouchesRestrictedSwitchPreference extends RestrictedSwitchPreference {
public FilterTouchesRestrictedSwitchPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public FilterTouchesRestrictedSwitchPreference(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public FilterTouchesRestrictedSwitchPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FilterTouchesRestrictedSwitchPreference(Context context) {
super(context);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final View switchView = holder.findViewById(androidx.preference.R.id.switchWidget);
if (switchView != null) {
final View rootView = switchView.getRootView();
rootView.setFilterTouchesWhenObscured(true);
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2020 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.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.preference.PreferenceViewHolder;
import androidx.preference.SwitchPreferenceCompat;
/**
* This widget with enabled filterTouchesWhenObscured attribute use to replace
* the {@link SwitchPreferenceCompat} in the Special access app pages for security.
*/
public class FilterTouchesSwitchPreference extends SwitchPreferenceCompat {
public FilterTouchesSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public FilterTouchesSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public FilterTouchesSwitchPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FilterTouchesSwitchPreference(Context context) {
super(context);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final View switchView = holder.findViewById(androidx.preference.R.id.switchWidget);
if (switchView != null) {
final View rootView = switchView.getRootView();
rootView.setFilterTouchesWhenObscured(true);
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2017 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.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet;
import android.widget.TextView;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
/**
* A preference whose summary text will only span one single line.
*/
public class FixedLineSummaryPreference extends Preference {
private int mSummaryLineCount;
public FixedLineSummaryPreference(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FixedLineSummaryPreference,
0, 0);
if (a.hasValue(R.styleable.FixedLineSummaryPreference_summaryLineCount)) {
mSummaryLineCount = a.getInteger(
R.styleable.FixedLineSummaryPreference_summaryLineCount, 1);
} else {
mSummaryLineCount = 1;
}
a.recycle();
}
public void setSummaryLineCount(int count) {
mSummaryLineCount = count;
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
TextView summary = (TextView) holder.findViewById(android.R.id.summary);
if (summary != null) {
summary.setMinLines(mSummaryLineCount);
summary.setMaxLines(mSummaryLineCount);
summary.setEllipsize(TruncateAt.END);
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2018 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.widget;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.VisibleForTesting;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.appbar.AppBarLayout;
/**
* This scrolling view behavior will set the background of the {@link AppBarLayout} as
* transparent and without the elevation. Also make header overlapped the scrolling child view.
*/
public class FloatingAppBarScrollingViewBehavior extends AppBarLayout.ScrollingViewBehavior {
private boolean initialized;
public FloatingAppBarScrollingViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
boolean changed = super.onDependentViewChanged(parent, child, dependency);
if (!initialized && dependency instanceof AppBarLayout) {
initialized = true;
AppBarLayout appBarLayout = (AppBarLayout) dependency;
setAppBarLayoutTransparent(appBarLayout);
}
return changed;
}
@VisibleForTesting
void setAppBarLayoutTransparent(AppBarLayout appBarLayout) {
appBarLayout.setBackgroundColor(Color.TRANSPARENT);
appBarLayout.setTargetElevation(0);
}
@Override
protected boolean shouldHeaderOverlapScrollingChild() {
return true;
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2017 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.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settingslib.RestrictedPreference;
/**
* A preference with a Gear on the side
*/
public class GearPreference extends RestrictedPreference implements View.OnClickListener {
// Default true for gear available even if the preference itself is disabled.
protected boolean mGearState = true;
private OnGearClickListener mOnGearClickListener;
public GearPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public GearPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setOnGearClickListener(OnGearClickListener l) {
mOnGearClickListener = l;
notifyChanged();
}
/** Sets state of gear button. */
public void setGearEnabled(boolean enabled) {
mGearState = enabled;
}
/** Gets state of gear button. */
public boolean isGearEnabled() {
return mGearState;
}
@Override
protected int getSecondTargetResId() {
return R.layout.preference_widget_gear;
}
@Override
protected boolean shouldHideSecondTarget() {
return mOnGearClickListener == null;
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final View gear = holder.findViewById(R.id.settings_button);
if (mOnGearClickListener != null) {
gear.setVisibility(View.VISIBLE);
gear.setOnClickListener(this);
} else {
gear.setVisibility(View.GONE);
gear.setOnClickListener(null);
}
// Make gear available even if the preference itself is disabled.
gear.setEnabled(mGearState);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.settings_button) {
if (mOnGearClickListener != null) {
mOnGearClickListener.onGearClick(this);
}
}
}
public interface OnGearClickListener {
void onGearClick(GearPreference p);
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright (C) 2020 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.widget;
import androidx.preference.Preference;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.PrimarySwitchPreference;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import com.android.settingslib.RestrictedSwitchPreference;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
/**
* The switch controller that is used to update the switch widget in the PrimarySwitchPreference
* and RestrictedSwitchPreference layouts.
*/
public class GenericSwitchController extends SwitchWidgetController implements
Preference.OnPreferenceChangeListener {
private Preference mPreference;
private MetricsFeatureProvider mMetricsFeatureProvider;
public GenericSwitchController(PrimarySwitchPreference preference) {
setPreference(preference);
}
public GenericSwitchController(RestrictedSwitchPreference preference) {
setPreference(preference);
}
private void setPreference(Preference preference) {
mPreference = preference;
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
}
@Override
public void setTitle(String title) {
}
@Override
public void startListening() {
mPreference.setOnPreferenceChangeListener(this);
}
@Override
public void stopListening() {
mPreference.setOnPreferenceChangeListener(null);
}
@Override
public void setChecked(boolean checked) {
if (mPreference instanceof PrimarySwitchPreference) {
((PrimarySwitchPreference) mPreference).setChecked(checked);
} else if (mPreference instanceof RestrictedSwitchPreference) {
((RestrictedSwitchPreference) mPreference).setChecked(checked);
}
}
@Override
public boolean isChecked() {
if (mPreference instanceof PrimarySwitchPreference) {
return ((PrimarySwitchPreference) mPreference).isChecked();
} else if (mPreference instanceof RestrictedSwitchPreference) {
return ((RestrictedSwitchPreference) mPreference).isChecked();
}
return false;
}
@Override
public void setEnabled(boolean enabled) {
if (mPreference instanceof PrimarySwitchPreference) {
((PrimarySwitchPreference) mPreference).setSwitchEnabled(enabled);
} else if (mPreference instanceof RestrictedSwitchPreference) {
((RestrictedSwitchPreference) mPreference).setEnabled(enabled);
}
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (mListener != null) {
final boolean result = mListener.onSwitchToggled((Boolean) newValue);
if (result) {
mMetricsFeatureProvider.logClickedPreference(preference,
preference.getExtras().getInt(DashboardFragment.CATEGORY));
}
return result;
}
return false;
}
@Override
public void setDisabledByAdmin(EnforcedAdmin admin) {
if (mPreference instanceof PrimarySwitchPreference) {
((PrimarySwitchPreference) mPreference).setDisabledByAdmin(admin);
} else if (mPreference instanceof RestrictedSwitchPreference) {
((RestrictedSwitchPreference) mPreference).setDisabledByAdmin(admin);
}
}
}

View File

@@ -0,0 +1,283 @@
/*
* Copyright (C) 2018 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.widget;
import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceGroupAdapter;
import androidx.preference.PreferenceScreen;
import androidx.preference.PreferenceViewHolder;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
import com.google.android.material.appbar.AppBarLayout;
public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter {
private static final String TAG = "HighlightableAdapter";
@VisibleForTesting
static final long DELAY_COLLAPSE_DURATION_MILLIS = 300L;
@VisibleForTesting
static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L;
private static final long HIGHLIGHT_DURATION = 15000L;
private static final long HIGHLIGHT_FADE_OUT_DURATION = 500L;
private static final long HIGHLIGHT_FADE_IN_DURATION = 200L;
@VisibleForTesting
final int mHighlightColor;
@VisibleForTesting
boolean mFadeInAnimated;
private final int mNormalBackgroundRes;
private final String mHighlightKey;
private boolean mHighlightRequested;
private int mHighlightPosition = RecyclerView.NO_POSITION;
/**
* Tries to override initial expanded child count.
* <p/>
* Initial expanded child count will be ignored if:
* 1. fragment contains request to highlight a particular row.
* 2. count value is invalid.
*/
public static void adjustInitialExpandedChildCount(SettingsPreferenceFragment host) {
if (host == null) {
return;
}
final PreferenceScreen screen = host.getPreferenceScreen();
if (screen == null) {
return;
}
final Bundle arguments = host.getArguments();
if (arguments != null) {
final String highlightKey = arguments.getString(EXTRA_FRAGMENT_ARG_KEY);
if (!TextUtils.isEmpty(highlightKey)) {
// Has highlight row - expand everything
screen.setInitialExpandedChildrenCount(Integer.MAX_VALUE);
return;
}
}
final int initialCount = host.getInitialExpandedChildCount();
if (initialCount <= 0) {
return;
}
screen.setInitialExpandedChildrenCount(initialCount);
}
public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, String key,
boolean highlightRequested) {
super(preferenceGroup);
mHighlightKey = key;
mHighlightRequested = highlightRequested;
final Context context = preferenceGroup.getContext();
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
outValue, true /* resolveRefs */);
mNormalBackgroundRes = outValue.resourceId;
mHighlightColor = context.getColor(R.color.preference_highlight_color);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
updateBackground(holder, position);
}
@VisibleForTesting
void updateBackground(PreferenceViewHolder holder, int position) {
View v = holder.itemView;
if (position == mHighlightPosition
&& (mHighlightKey != null
&& TextUtils.equals(mHighlightKey, getItem(position).getKey()))) {
// This position should be highlighted. If it's highlighted before - skip animation.
addHighlightBackground(holder, !mFadeInAnimated);
if (v != null) {
v.requestAccessibilityFocus();
}
} else if (Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
// View with highlight is reused for a view that should not have highlight
removeHighlightBackground(holder, false /* animate */);
}
}
/**
* A function can highlight a specific setting in recycler view.
* note: Before highlighting a setting, screen collapses tool bar with an animation.
*/
public void requestHighlight(View root, RecyclerView recyclerView, AppBarLayout appBarLayout) {
if (mHighlightRequested || recyclerView == null || TextUtils.isEmpty(mHighlightKey)) {
return;
}
final int position = getPreferenceAdapterPosition(mHighlightKey);
if (position < 0) {
return;
}
// Highlight request accepted
mHighlightRequested = true;
// Collapse app bar after 300 milliseconds.
if (appBarLayout != null) {
root.postDelayed(() -> {
appBarLayout.setExpanded(false, true);
}, DELAY_COLLAPSE_DURATION_MILLIS);
}
// Remove the animator as early as possible to avoid a RecyclerView crash.
recyclerView.setItemAnimator(null);
// Scroll to correct position after 600 milliseconds.
root.postDelayed(() -> {
if (ensureHighlightPosition()) {
recyclerView.smoothScrollToPosition(mHighlightPosition);
highlightAndFocusTargetItem(recyclerView, mHighlightPosition);
}
}, DELAY_HIGHLIGHT_DURATION_MILLIS);
}
private void highlightAndFocusTargetItem(RecyclerView recyclerView, int highlightPosition) {
ViewHolder target = recyclerView.findViewHolderForAdapterPosition(highlightPosition);
if (target != null) { // view already visible
notifyItemChanged(mHighlightPosition);
target.itemView.requestFocus();
} else { // otherwise we're about to scroll to that view (but we might not be scrolling yet)
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
notifyItemChanged(mHighlightPosition);
ViewHolder target = recyclerView
.findViewHolderForAdapterPosition(highlightPosition);
if (target != null) {
target.itemView.requestFocus();
}
recyclerView.removeOnScrollListener(this);
}
}
});
}
}
/**
* Make sure we highlight the real-wanted position in case of preference position already
* changed when the delay time comes.
*/
private boolean ensureHighlightPosition() {
if (TextUtils.isEmpty(mHighlightKey)) {
return false;
}
final int position = getPreferenceAdapterPosition(mHighlightKey);
final boolean allowHighlight = position >= 0;
if (allowHighlight && mHighlightPosition != position) {
Log.w(TAG, "EnsureHighlight: position has changed since last highlight request");
// Make sure RecyclerView always uses latest correct position to avoid exceptions.
mHighlightPosition = position;
}
return allowHighlight;
}
public boolean isHighlightRequested() {
return mHighlightRequested;
}
@VisibleForTesting
void requestRemoveHighlightDelayed(PreferenceViewHolder holder) {
final View v = holder.itemView;
v.postDelayed(() -> {
mHighlightPosition = RecyclerView.NO_POSITION;
removeHighlightBackground(holder, true /* animate */);
}, HIGHLIGHT_DURATION);
}
private void addHighlightBackground(PreferenceViewHolder holder, boolean animate) {
final View v = holder.itemView;
v.setTag(R.id.preference_highlighted, true);
if (!animate) {
v.setBackgroundColor(mHighlightColor);
Log.d(TAG, "AddHighlight: Not animation requested - setting highlight background");
requestRemoveHighlightDelayed(holder);
return;
}
mFadeInAnimated = true;
final int colorFrom = mNormalBackgroundRes;
final int colorTo = mHighlightColor;
final ValueAnimator fadeInLoop = ValueAnimator.ofObject(
new ArgbEvaluator(), colorFrom, colorTo);
fadeInLoop.setDuration(HIGHLIGHT_FADE_IN_DURATION);
fadeInLoop.addUpdateListener(
animator -> v.setBackgroundColor((int) animator.getAnimatedValue()));
fadeInLoop.setRepeatMode(ValueAnimator.REVERSE);
fadeInLoop.setRepeatCount(4);
fadeInLoop.start();
Log.d(TAG, "AddHighlight: starting fade in animation");
holder.setIsRecyclable(false);
requestRemoveHighlightDelayed(holder);
}
private void removeHighlightBackground(PreferenceViewHolder holder, boolean animate) {
final View v = holder.itemView;
if (!animate) {
v.setTag(R.id.preference_highlighted, false);
v.setBackgroundResource(mNormalBackgroundRes);
Log.d(TAG, "RemoveHighlight: No animation requested - setting normal background");
return;
}
if (!Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
// Not highlighted, no-op
Log.d(TAG, "RemoveHighlight: Not highlighted - skipping");
return;
}
int colorFrom = mHighlightColor;
int colorTo = mNormalBackgroundRes;
v.setTag(R.id.preference_highlighted, false);
final ValueAnimator colorAnimation = ValueAnimator.ofObject(
new ArgbEvaluator(), colorFrom, colorTo);
colorAnimation.setDuration(HIGHLIGHT_FADE_OUT_DURATION);
colorAnimation.addUpdateListener(
animator -> v.setBackgroundColor((int) animator.getAnimatedValue()));
colorAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Animation complete - the background is now white. Change to mNormalBackgroundRes
// so it is white and has ripple on touch.
v.setBackgroundResource(mNormalBackgroundRes);
holder.setIsRecyclable(true);
}
});
colorAnimation.start();
Log.d(TAG, "Starting fade out animation");
}
}

View File

@@ -0,0 +1,256 @@
/*
* 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.widget;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceGroupAdapter;
import androidx.preference.PreferenceViewHolder;
import androidx.recyclerview.widget.RecyclerView;
import androidx.window.embedding.ActivityEmbeddingController;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.homepage.SettingsHomepageActivity;
/**
* Adapter for highlighting top level preferences
*/
public class HighlightableTopLevelPreferenceAdapter extends PreferenceGroupAdapter implements
SettingsHomepageActivity.HomepageLoadedListener {
private static final String TAG = "HighlightableTopLevelAdapter";
static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 100L;
private static final int RES_NORMAL_BACKGROUND =
R.drawable.homepage_selectable_item_background;
private static final int RES_HIGHLIGHTED_BACKGROUND =
R.drawable.homepage_highlighted_item_background;
private final int mTitleColorNormal;
private final int mTitleColorHighlight;
private final int mSummaryColorNormal;
private final int mSummaryColorHighlight;
private final int mIconColorNormal;
private final int mIconColorHighlight;
private final SettingsHomepageActivity mHomepageActivity;
private final RecyclerView mRecyclerView;
private String mHighlightKey;
private int mHighlightPosition = RecyclerView.NO_POSITION;
private int mScrollPosition = RecyclerView.NO_POSITION;
private boolean mHighlightNeeded;
private boolean mScrolled;
private SparseArray<PreferenceViewHolder> mViewHolders;
public HighlightableTopLevelPreferenceAdapter(SettingsHomepageActivity homepageActivity,
PreferenceGroup preferenceGroup, RecyclerView recyclerView, String key,
boolean scrollNeeded) {
super(preferenceGroup);
mRecyclerView = recyclerView;
mHighlightKey = key;
mScrolled = !scrollNeeded;
mViewHolders = new SparseArray<>();
mHomepageActivity = homepageActivity;
Context context = preferenceGroup.getContext();
mTitleColorNormal = Utils.getColorAttrDefaultColor(context,
android.R.attr.textColorPrimary);
mTitleColorHighlight = context.getColor(R.color.accent_select_primary_text);
mSummaryColorNormal = Utils.getColorAttrDefaultColor(context,
android.R.attr.textColorSecondary);
mSummaryColorHighlight = context.getColor(R.color.accent_select_secondary_text);
mIconColorNormal = Utils.getHomepageIconColor(context);
mIconColorHighlight = Utils.getHomepageIconColorHighlight(context);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
mViewHolders.put(position, holder);
updateBackground(holder, position);
}
@VisibleForTesting
void updateBackground(PreferenceViewHolder holder, int position) {
if (!isHighlightNeeded()) {
removeHighlightBackground(holder);
return;
}
if (position == mHighlightPosition
&& mHighlightKey != null
&& TextUtils.equals(mHighlightKey, getItem(position).getKey())) {
// This position should be highlighted.
addHighlightBackground(holder);
} else {
removeHighlightBackground(holder);
}
}
/**
* A function can highlight a specific setting in recycler view.
*/
public void requestHighlight() {
if (mRecyclerView == null) {
return;
}
final int previousPosition = mHighlightPosition;
if (TextUtils.isEmpty(mHighlightKey)) {
// De-highlight previous preference.
mHighlightPosition = RecyclerView.NO_POSITION;
mScrolled = true;
if (previousPosition >= 0) {
notifyItemChanged(previousPosition);
}
return;
}
final int position = getPreferenceAdapterPosition(mHighlightKey);
if (position < 0) {
return;
}
// Scroll before highlight if needed.
final boolean highlightNeeded = isHighlightNeeded();
if (highlightNeeded) {
mScrollPosition = position;
scroll();
}
// Turn on/off highlight when screen split mode is changed.
if (highlightNeeded != mHighlightNeeded) {
Log.d(TAG, "Highlight needed change: " + highlightNeeded);
mHighlightNeeded = highlightNeeded;
mHighlightPosition = position;
notifyItemChanged(position);
if (!highlightNeeded) {
// De-highlight to prevent a flicker
removeHighlightAt(previousPosition);
}
return;
}
if (position == mHighlightPosition) {
return;
}
mHighlightPosition = position;
Log.d(TAG, "Request highlight position " + position);
Log.d(TAG, "Is highlight needed: " + highlightNeeded);
if (!highlightNeeded) {
return;
}
// Highlight preference.
notifyItemChanged(position);
// De-highlight previous preference.
if (previousPosition >= 0) {
notifyItemChanged(previousPosition);
}
}
/**
* A function that highlights a setting by specifying a preference key. Usually used whenever a
* preference is clicked.
*/
public void highlightPreference(String key, boolean scrollNeeded) {
mHighlightKey = key;
mScrolled = !scrollNeeded;
requestHighlight();
}
@Override
public void onHomepageLoaded() {
scroll();
}
private void scroll() {
if (mScrolled || mScrollPosition < 0) {
return;
}
if (mHomepageActivity.addHomepageLoadedListener(this)) {
return;
}
// Only when the recyclerView is loaded, it can be scrolled
final View view = mRecyclerView.getChildAt(mScrollPosition);
if (view == null) {
mRecyclerView.postDelayed(() -> scroll(), DELAY_HIGHLIGHT_DURATION_MILLIS);
return;
}
mScrolled = true;
Log.d(TAG, "Scroll to position " + mScrollPosition);
// Scroll to the top to reset the position.
mRecyclerView.nestedScrollBy(0, -mRecyclerView.getHeight());
final int scrollY = view.getTop();
if (scrollY > 0) {
mRecyclerView.nestedScrollBy(0, scrollY);
}
}
private void removeHighlightAt(int position) {
if (position >= 0) {
// De-highlight the existing preference view holder at an early stage
final PreferenceViewHolder holder = mViewHolders.get(position);
if (holder != null) {
removeHighlightBackground(holder);
}
notifyItemChanged(position);
}
}
private void addHighlightBackground(PreferenceViewHolder holder) {
final View v = holder.itemView;
v.setBackgroundResource(RES_HIGHLIGHTED_BACKGROUND);
((TextView) v.findViewById(android.R.id.title)).setTextColor(mTitleColorHighlight);
((TextView) v.findViewById(android.R.id.summary)).setTextColor(mSummaryColorHighlight);
final Drawable drawable = ((ImageView) v.findViewById(android.R.id.icon)).getDrawable();
if (drawable != null) {
drawable.setTint(mIconColorHighlight);
}
}
private void removeHighlightBackground(PreferenceViewHolder holder) {
final View v = holder.itemView;
v.setBackgroundResource(RES_NORMAL_BACKGROUND);
((TextView) v.findViewById(android.R.id.title)).setTextColor(mTitleColorNormal);
((TextView) v.findViewById(android.R.id.summary)).setTextColor(mSummaryColorNormal);
final Drawable drawable = ((ImageView) v.findViewById(android.R.id.icon)).getDrawable();
if (drawable != null) {
drawable.setTint(mIconColorNormal);
}
}
private boolean isHighlightNeeded() {
return ActivityEmbeddingController.getInstance(mHomepageActivity)
.isActivityEmbedded(mHomepageActivity);
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.widget;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.VisibleForTesting;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.appbar.AppBarLayout;
/**
* This scrolling view behavior will set the background of the {@link AppBarLayout} as
* transparent and without the elevation.
*/
public class HomepageAppBarScrollingViewBehavior extends AppBarLayout.ScrollingViewBehavior {
private boolean mInitialized;
public HomepageAppBarScrollingViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
boolean changed = super.onDependentViewChanged(parent, child, dependency);
if (!mInitialized && dependency instanceof AppBarLayout) {
mInitialized = true;
AppBarLayout appBarLayout = (AppBarLayout) dependency;
setAppBarLayoutTransparent(appBarLayout);
}
return changed;
}
@VisibleForTesting
void setAppBarLayoutTransparent(AppBarLayout appBarLayout) {
appBarLayout.setBackgroundColor(Color.TRANSPARENT);
appBarLayout.setTargetElevation(0);
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.widget;
import android.content.Context;
import android.util.AttributeSet;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
/** A customized layout for homepage preference. */
public class HomepagePreference extends Preference implements
HomepagePreferenceLayoutHelper.HomepagePreferenceLayout {
private final HomepagePreferenceLayoutHelper mHelper;
public HomepagePreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mHelper = new HomepagePreferenceLayoutHelper(this);
}
public HomepagePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mHelper = new HomepagePreferenceLayoutHelper(this);
}
public HomepagePreference(Context context, AttributeSet attrs) {
super(context, attrs);
mHelper = new HomepagePreferenceLayoutHelper(this);
}
public HomepagePreference(Context context) {
super(context);
mHelper = new HomepagePreferenceLayoutHelper(this);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
mHelper.onBindViewHolder(holder);
}
@Override
public HomepagePreferenceLayoutHelper getHelper() {
return mHelper;
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2022 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.widget;
import android.view.View;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
/** Helper for homepage preference to manage layout. */
public class HomepagePreferenceLayoutHelper {
private View mIcon;
private View mText;
private boolean mIconVisible = true;
private int mIconPaddingStart = -1;
private int mTextPaddingStart = -1;
/** The interface for managing preference layouts on homepage */
public interface HomepagePreferenceLayout {
/** Returns a {@link HomepagePreferenceLayoutHelper} */
HomepagePreferenceLayoutHelper getHelper();
}
public HomepagePreferenceLayoutHelper(Preference preference) {
preference.setLayoutResource(R.layout.homepage_preference);
}
/** Sets whether the icon should be visible */
public void setIconVisible(boolean visible) {
mIconVisible = visible;
if (mIcon != null) {
mIcon.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
/** Sets the icon padding start */
public void setIconPaddingStart(int paddingStart) {
mIconPaddingStart = paddingStart;
if (mIcon != null && paddingStart >= 0) {
mIcon.setPaddingRelative(paddingStart, mIcon.getPaddingTop(), mIcon.getPaddingEnd(),
mIcon.getPaddingBottom());
}
}
/** Sets the text padding start */
public void setTextPaddingStart(int paddingStart) {
mTextPaddingStart = paddingStart;
if (mText != null && paddingStart >= 0) {
mText.setPaddingRelative(paddingStart, mText.getPaddingTop(), mText.getPaddingEnd(),
mText.getPaddingBottom());
}
}
void onBindViewHolder(PreferenceViewHolder holder) {
mIcon = holder.findViewById(R.id.icon_frame);
mText = holder.findViewById(R.id.text_frame);
setIconVisible(mIconVisible);
setIconPaddingStart(mIconPaddingStart);
setTextPaddingStart(mTextPaddingStart);
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2011 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.widget;
import android.content.res.Resources;
import android.text.SpannableStringBuilder;
/**
* Utility to invert another {@link ChartAxis}.
*/
public class InvertedChartAxis implements ChartAxis {
private final ChartAxis mWrapped;
private float mSize;
public InvertedChartAxis(ChartAxis wrapped) {
mWrapped = wrapped;
}
@Override
public boolean setBounds(long min, long max) {
return mWrapped.setBounds(min, max);
}
@Override
public boolean setSize(float size) {
mSize = size;
return mWrapped.setSize(size);
}
@Override
public float convertToPoint(long value) {
return mSize - mWrapped.convertToPoint(value);
}
@Override
public long convertToValue(float point) {
return mWrapped.convertToValue(mSize - point);
}
@Override
public long buildLabel(Resources res, SpannableStringBuilder builder, long value) {
return mWrapped.buildLabel(res, builder, value);
}
@Override
public float[] getTickPoints() {
final float[] points = mWrapped.getTickPoints();
for (int i = 0; i < points.length; i++) {
points[i] = mSize - points[i];
}
return points;
}
@Override
public int shouldAdjustAxis(long value) {
return mWrapped.shouldAdjustAxis(value);
}
}

View File

@@ -0,0 +1,234 @@
/*
* Copyright (C) 2016 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.widget;
import static android.view.HapticFeedbackConstants.CLOCK_TICK;
import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.SeekBar;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.customview.widget.ExploreByTouchHelper;
import java.util.List;
/**
* LabeledSeekBar represent a seek bar assigned with labeled, discrete values.
* It pretends to be a group of radio button for AccessibilityServices, in order to adjust the
* behavior of these services to keep the mental model of the visual discrete SeekBar.
*/
public class LabeledSeekBar extends SeekBar {
private final ExploreByTouchHelper mAccessHelper;
/** Seek bar change listener set via public method. */
private OnSeekBarChangeListener mOnSeekBarChangeListener;
/** Labels for discrete progress values. */
private String[] mLabels;
private int mLastProgress = -1;
public LabeledSeekBar(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.seekBarStyle);
}
public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mAccessHelper = new LabeledSeekBarExploreByTouchHelper(this);
ViewCompat.setAccessibilityDelegate(this, mAccessHelper);
super.setOnSeekBarChangeListener(mProxySeekBarListener);
}
@Override
public synchronized void setProgress(int progress) {
// This method gets called from the constructor, so mAccessHelper may
// not have been assigned yet.
if (mAccessHelper != null) {
mAccessHelper.invalidateRoot();
}
super.setProgress(progress);
}
public void setLabels(String[] labels) {
mLabels = labels;
}
@Override
public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) {
// The callback set in the constructor will proxy calls to this
// listener.
mOnSeekBarChangeListener = l;
}
@Override
protected boolean dispatchHoverEvent(MotionEvent event) {
return mAccessHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
}
private void sendClickEventForAccessibility(int progress) {
mAccessHelper.invalidateRoot();
mAccessHelper.sendEventForVirtualView(progress, AccessibilityEvent.TYPE_VIEW_CLICKED);
}
private final OnSeekBarChangeListener mProxySeekBarListener = new OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (mOnSeekBarChangeListener != null) {
mOnSeekBarChangeListener.onStopTrackingTouch(seekBar);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
if (mOnSeekBarChangeListener != null) {
mOnSeekBarChangeListener.onStartTrackingTouch(seekBar);
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (mOnSeekBarChangeListener != null) {
mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser);
sendClickEventForAccessibility(progress);
}
if (progress != mLastProgress) {
seekBar.performHapticFeedback(CLOCK_TICK);
mLastProgress = progress;
}
}
};
private class LabeledSeekBarExploreByTouchHelper extends ExploreByTouchHelper {
private boolean mIsLayoutRtl;
public LabeledSeekBarExploreByTouchHelper(LabeledSeekBar forView) {
super(forView);
mIsLayoutRtl = forView.getResources().getConfiguration()
.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
}
@Override
protected int getVirtualViewAt(float x, float y) {
return getVirtualViewIdIndexFromX(x);
}
@Override
protected void getVisibleVirtualViews(List<Integer> list) {
for (int i = 0, c = LabeledSeekBar.this.getMax(); i <= c; ++i) {
list.add(i);
}
}
@Override
protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
Bundle arguments) {
if (virtualViewId == ExploreByTouchHelper.HOST_ID) {
// Do nothing
return false;
}
switch (action) {
case AccessibilityNodeInfoCompat.ACTION_CLICK:
LabeledSeekBar.this.setProgress(virtualViewId);
sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED);
return true;
default:
return false;
}
}
@Override
protected void onPopulateNodeForVirtualView(
int virtualViewId, AccessibilityNodeInfoCompat node) {
node.setClassName(RadioButton.class.getName());
node.setBoundsInParent(getBoundsInParentFromVirtualViewId(virtualViewId));
node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
node.setContentDescription(mLabels[virtualViewId]);
node.setClickable(true);
node.setCheckable(true);
node.setChecked(virtualViewId == LabeledSeekBar.this.getProgress());
}
@Override
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
event.setClassName(RadioButton.class.getName());
event.setContentDescription(mLabels[virtualViewId]);
event.setChecked(virtualViewId == LabeledSeekBar.this.getProgress());
}
@Override
protected void onPopulateNodeForHost(AccessibilityNodeInfoCompat node) {
node.setClassName(RadioGroup.class.getName());
}
@Override
protected void onPopulateEventForHost(AccessibilityEvent event) {
event.setClassName(RadioGroup.class.getName());
}
private int getHalfVirtualViewWidth() {
final int width = LabeledSeekBar.this.getWidth();
final int barWidth = width - LabeledSeekBar.this.getPaddingStart()
- LabeledSeekBar.this.getPaddingEnd();
return Math.max(0, barWidth / (LabeledSeekBar.this.getMax() * 2));
}
private int getVirtualViewIdIndexFromX(float x) {
int posBase = Math.max(0,
((int) x - LabeledSeekBar.this.getPaddingStart()) / getHalfVirtualViewWidth());
posBase = (posBase + 1) / 2;
posBase = Math.min(posBase, LabeledSeekBar.this.getMax());
return mIsLayoutRtl ? LabeledSeekBar.this.getMax() - posBase : posBase;
}
private Rect getBoundsInParentFromVirtualViewId(int virtualViewId) {
final int updatedVirtualViewId = mIsLayoutRtl
? LabeledSeekBar.this.getMax() - virtualViewId : virtualViewId;
int left = (updatedVirtualViewId * 2 - 1) * getHalfVirtualViewWidth()
+ LabeledSeekBar.this.getPaddingStart();
int right = (updatedVirtualViewId * 2 + 1) * getHalfVirtualViewWidth()
+ LabeledSeekBar.this.getPaddingStart();
// Edge case
left = updatedVirtualViewId == 0 ? 0 : left;
right = updatedVirtualViewId == LabeledSeekBar.this.getMax()
? LabeledSeekBar.this.getWidth() : right;
final Rect r = new Rect();
r.set(left, 0, right, LabeledSeekBar.this.getHeight());
return r;
}
}
}

View File

@@ -0,0 +1,258 @@
/*
* 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.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.core.content.res.TypedArrayUtils;
import androidx.preference.PreferenceViewHolder;
import com.android.internal.util.Preconditions;
import com.android.settings.R;
import com.android.settings.Utils;
/**
* A labeled {@link SeekBarPreference} with left and right text label, icon label, or both.
*
* <p>
* The component provides the attribute usage below.
* <attr name="textStart" format="reference" />
* <attr name="textEnd" format="reference" />
* <attr name="tickMark" format="reference" />
* <attr name="iconStart" format="reference" />
* <attr name="iconEnd" format="reference" />
* <attr name="iconStartContentDescription" format="reference" />
* <attr name="iconEndContentDescription" format="reference" />
* </p>
*
* <p> If you set the attribute values {@code iconStartContentDescription} or {@code
* iconEndContentDescription} from XML, you must also set the corresponding attributes {@code
* iconStart} or {@code iconEnd}, otherwise throws an {@link IllegalArgumentException}.</p>
*/
public class LabeledSeekBarPreference extends SeekBarPreference {
private final int mTextStartId;
private final int mTextEndId;
private final int mTickMarkId;
private final int mIconStartId;
private final int mIconEndId;
private final int mIconStartContentDescriptionId;
private final int mIconEndContentDescriptionId;
private OnPreferenceChangeListener mStopListener;
private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener;
private SeekBar mSeekBar;
public LabeledSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setLayoutResource(R.layout.preference_labeled_slider);
final TypedArray styledAttrs = context.obtainStyledAttributes(attrs,
R.styleable.LabeledSeekBarPreference);
mTextStartId = styledAttrs.getResourceId(
R.styleable.LabeledSeekBarPreference_textStart, /* defValue= */ 0);
mTextEndId = styledAttrs.getResourceId(
R.styleable.LabeledSeekBarPreference_textEnd, /* defValue= */ 0);
mTickMarkId = styledAttrs.getResourceId(
R.styleable.LabeledSeekBarPreference_tickMark, /* defValue= */ 0);
mIconStartId = styledAttrs.getResourceId(
R.styleable.LabeledSeekBarPreference_iconStart, /* defValue= */ 0);
mIconEndId = styledAttrs.getResourceId(
R.styleable.LabeledSeekBarPreference_iconEnd, /* defValue= */ 0);
mIconStartContentDescriptionId = styledAttrs.getResourceId(
R.styleable.LabeledSeekBarPreference_iconStartContentDescription,
/* defValue= */ 0);
Preconditions.checkArgument(!(mIconStartContentDescriptionId != 0 && mIconStartId == 0),
"The resource of the iconStart attribute may be invalid or not set, "
+ "you should set the iconStart attribute and have the valid resource.");
mIconEndContentDescriptionId = styledAttrs.getResourceId(
R.styleable.LabeledSeekBarPreference_iconEndContentDescription,
/* defValue= */ 0);
Preconditions.checkArgument(!(mIconEndContentDescriptionId != 0 && mIconEndId == 0),
"The resource of the iconEnd attribute may be invalid or not set, "
+ "you should set the iconEnd attribute and have the valid resource.");
styledAttrs.recycle();
}
public LabeledSeekBarPreference(Context context, AttributeSet attrs) {
this(context, attrs, TypedArrayUtils.getAttr(context,
androidx.preference.R.attr.seekBarPreferenceStyle,
com.android.internal.R.attr.seekBarPreferenceStyle), 0);
}
public SeekBar getSeekbar() {
return mSeekBar;
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final TextView summaryView = (TextView) holder.findViewById(android.R.id.summary);
boolean isSummaryVisible = false;
if (summaryView != null) {
isSummaryVisible = (summaryView.getVisibility() == View.VISIBLE);
}
final TextView titleView = (TextView) holder.findViewById(android.R.id.title);
if (titleView != null && !isSelectable() && isEnabled() && isSummaryVisible) {
titleView.setTextColor(
Utils.getColorAttr(getContext(), android.R.attr.textColorPrimary));
}
final TextView startText = (TextView) holder.findViewById(android.R.id.text1);
if (mTextStartId > 0) {
startText.setText(mTextStartId);
}
final TextView endText = (TextView) holder.findViewById(android.R.id.text2);
if (mTextEndId > 0) {
endText.setText(mTextEndId);
}
final View labelFrame = holder.findViewById(R.id.label_frame);
final boolean isValidTextResIdExist = mTextStartId > 0 || mTextEndId > 0;
labelFrame.setVisibility(isValidTextResIdExist ? View.VISIBLE : View.GONE);
mSeekBar = (SeekBar) holder.findViewById(com.android.internal.R.id.seekbar);
if (mTickMarkId != 0) {
final Drawable tickMark = getContext().getDrawable(mTickMarkId);
mSeekBar.setTickMark(tickMark);
}
final ViewGroup iconStartFrame = (ViewGroup) holder.findViewById(R.id.icon_start_frame);
final ImageView iconStartView = (ImageView) holder.findViewById(R.id.icon_start);
updateIconStartIfNeeded(iconStartFrame, iconStartView, mSeekBar);
final ViewGroup iconEndFrame = (ViewGroup) holder.findViewById(R.id.icon_end_frame);
final ImageView iconEndView = (ImageView) holder.findViewById(R.id.icon_end);
updateIconEndIfNeeded(iconEndFrame, iconEndView, mSeekBar);
}
public void setOnPreferenceChangeStopListener(OnPreferenceChangeListener listener) {
mStopListener = listener;
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
super.onStartTrackingTouch(seekBar);
if (mSeekBarChangeListener != null) {
mSeekBarChangeListener.onStartTrackingTouch(seekBar);
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
super.onProgressChanged(seekBar, progress, fromUser);
if (mSeekBarChangeListener != null) {
mSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser);
}
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
super.onStopTrackingTouch(seekBar);
if (mSeekBarChangeListener != null) {
mSeekBarChangeListener.onStopTrackingTouch(seekBar);
}
if (mStopListener != null) {
mStopListener.onPreferenceChange(this, seekBar.getProgress());
}
// Need to update the icon enabled status
notifyChanged();
}
public void setOnSeekBarChangeListener(SeekBar.OnSeekBarChangeListener seekBarChangeListener) {
mSeekBarChangeListener = seekBarChangeListener;
}
private void updateIconStartIfNeeded(ViewGroup iconFrame, ImageView iconStart,
SeekBar seekBar) {
if (mIconStartId == 0) {
return;
}
if (iconStart.getDrawable() == null) {
iconStart.setImageResource(mIconStartId);
}
if (mIconStartContentDescriptionId != 0) {
final String contentDescription =
iconFrame.getContext().getString(mIconStartContentDescriptionId);
iconFrame.setContentDescription(contentDescription);
}
iconFrame.setOnClickListener((view) -> {
final int progress = getProgress();
if (progress > 0) {
setProgress(progress - 1);
}
});
iconFrame.setVisibility(View.VISIBLE);
setIconViewAndFrameEnabled(iconStart, seekBar.getProgress() > 0);
}
private void updateIconEndIfNeeded(ViewGroup iconFrame, ImageView iconEnd, SeekBar seekBar) {
if (mIconEndId == 0) {
return;
}
if (iconEnd.getDrawable() == null) {
iconEnd.setImageResource(mIconEndId);
}
if (mIconEndContentDescriptionId != 0) {
final String contentDescription =
iconFrame.getContext().getString(mIconEndContentDescriptionId);
iconFrame.setContentDescription(contentDescription);
}
iconFrame.setOnClickListener((view) -> {
final int progress = getProgress();
if (progress < getMax()) {
setProgress(progress + 1);
}
});
iconFrame.setVisibility(View.VISIBLE);
setIconViewAndFrameEnabled(iconEnd, seekBar.getProgress() < seekBar.getMax());
}
private static void setIconViewAndFrameEnabled(View iconView, boolean enabled) {
iconView.setEnabled(enabled);
final ViewGroup iconFrame = (ViewGroup) iconView.getParent();
iconFrame.setEnabled(enabled);
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2020 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.widget;
import android.content.Context;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
/** A preference which supports linkify text in the summary **/
public class LinkifySummaryPreference extends Preference {
public LinkifySummaryPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LinkifySummaryPreference(Context context) {
super(context);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final TextView summaryView = (TextView) holder.findViewById(android.R.id.summary);
if (summaryView == null || summaryView.getVisibility() != View.VISIBLE) {
return;
}
final CharSequence summary = getSummary();
if (!TextUtils.isEmpty(summary)) {
final SpannableString spannableSummary = new SpannableString(summary);
if (spannableSummary.getSpans(0, spannableSummary.length(), ClickableSpan.class)
.length > 0) {
summaryView.setMovementMethod(LinkMovementMethod.getInstance());
}
}
}
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright (C) 2017 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.widget;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import androidx.annotation.Nullable;
/**
* A helper class that manages show/hide loading spinner, content view and empty view (optional).
*/
public class LoadingViewController {
private static final long DELAY_SHOW_LOADING_CONTAINER_THRESHOLD_MS = 100L;
private final Handler mFgHandler;
private final View mLoadingView;
private final View mContentView;
private final View mEmptyView;
public LoadingViewController(View loadingView, View contentView) {
this(loadingView, contentView, null /* emptyView*/);
}
public LoadingViewController(View loadingView, View contentView, @Nullable View emptyView) {
mLoadingView = loadingView;
mContentView = contentView;
mEmptyView = emptyView;
mFgHandler = new Handler(Looper.getMainLooper());
}
private Runnable mShowLoadingContainerRunnable = new Runnable() {
public void run() {
showLoadingView();
}
};
/**
* Shows content view and hides loading view & empty view.
*/
public void showContent(boolean animate) {
// Cancel any pending task to show the loading animation and show the list of
// apps directly.
mFgHandler.removeCallbacks(mShowLoadingContainerRunnable);
handleLoadingContainer(true /* showContent */, false /* showEmpty*/, animate);
}
/**
* Shows empty view and hides loading view & content view.
*/
public void showEmpty(boolean animate) {
if (mEmptyView == null) {
return;
}
// Cancel any pending task to show the loading animation and show the list of
// apps directly.
mFgHandler.removeCallbacks(mShowLoadingContainerRunnable);
handleLoadingContainer(false /* showContent */, true /* showEmpty */, animate);
}
/**
* Shows loading view and hides content view & empty view.
*/
public void showLoadingView() {
handleLoadingContainer(false /* showContent */, false /* showEmpty */, false /* animate */);
}
public void showLoadingViewDelayed() {
mFgHandler.postDelayed(
mShowLoadingContainerRunnable, DELAY_SHOW_LOADING_CONTAINER_THRESHOLD_MS);
}
private void handleLoadingContainer(boolean showContent, boolean showEmpty, boolean animate) {
handleLoadingContainer(mLoadingView, mContentView, mEmptyView,
showContent, showEmpty, animate);
}
/**
* Show/hide loading view and content view.
*
* @param loading The loading spinner view
* @param content The content view
* @param done If true, content is set visible and loading is set invisible.
* @param animate Whether or not content/loading views should animate in/out.
*/
public static void handleLoadingContainer(View loading, View content, boolean done,
boolean animate) {
setViewShown(loading, !done, animate);
setViewShown(content, done, animate);
}
/**
* Show/hide loading view and content view and empty view.
*
* @param loading The loading spinner view
* @param content The content view
* @param empty The empty view shows no item summary to users.
* @param showContent If true, content is set visible and loading is set invisible.
* @param showEmpty If true, empty is set visible and loading is set invisible.
* @param animate Whether or not content/loading views should animate in/out.
*/
public static void handleLoadingContainer(View loading, View content, View empty,
boolean showContent, boolean showEmpty, boolean animate) {
if (empty != null) {
setViewShown(empty, showEmpty, animate);
}
setViewShown(content, showContent, animate);
setViewShown(loading, !showContent && !showEmpty, animate);
}
private static void setViewShown(final View view, boolean shown, boolean animate) {
if (animate) {
Animation animation = AnimationUtils.loadAnimation(view.getContext(),
shown ? android.R.anim.fade_in : android.R.anim.fade_out);
if (shown) {
view.setVisibility(View.VISIBLE);
} else {
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
view.setVisibility(View.INVISIBLE);
}
});
}
view.startAnimation(animation);
} else {
view.clearAnimation();
view.setVisibility(shown ? View.VISIBLE : View.INVISIBLE);
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright (C) 2020 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.widget;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import com.android.settingslib.RestrictedLockUtils;
/**
* The switch controller that is used to update the switch widget in the SettingsMainSwitchBar.
*/
public class MainSwitchBarController extends SwitchWidgetController implements
OnCheckedChangeListener {
private final SettingsMainSwitchBar mMainSwitch;
public MainSwitchBarController(SettingsMainSwitchBar mainSwitch) {
mMainSwitch = mainSwitch;
}
@Override
public void setupView() {
mMainSwitch.show();
}
@Override
public void teardownView() {
mMainSwitch.hide();
}
@Override
public void setTitle(String title) {
mMainSwitch.setTitle(title);
}
@Override
public void startListening() {
mMainSwitch.addOnSwitchChangeListener(this);
}
@Override
public void stopListening() {
mMainSwitch.removeOnSwitchChangeListener(this);
}
@Override
public void setChecked(boolean checked) {
mMainSwitch.setChecked(checked);
}
@Override
public boolean isChecked() {
return mMainSwitch.isChecked();
}
@Override
public void setEnabled(boolean enabled) {
mMainSwitch.setEnabled(enabled);
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (mListener != null) {
mListener.onSwitchToggled(isChecked);
}
}
@Override
public void setDisabledByAdmin(RestrictedLockUtils.EnforcedAdmin admin) {
mMainSwitch.setDisabledByAdmin(admin);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,156 @@
/*
* 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.widget;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.media.MediaPlayer;
import android.net.Uri;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
/**
* A {@link VideoPreference.AnimationController} containing a {@link
* MediaPlayer}. The controller is used by {@link VideoPreference} to display
* a mp4 resource.
*/
class MediaAnimationController implements VideoPreference.AnimationController {
private MediaPlayer mMediaPlayer;
private boolean mVideoReady;
private Surface mSurface;
MediaAnimationController(Context context, int videoId) {
final Uri videoPath = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
.authority(context.getPackageName())
.appendPath(String.valueOf(videoId))
.build();
mMediaPlayer = MediaPlayer.create(context, videoPath);
// when the playback res is invalid or others, MediaPlayer create may fail
// and return null, so need add the null judgement.
if (mMediaPlayer != null) {
mMediaPlayer.seekTo(0);
mMediaPlayer.setOnSeekCompleteListener(mp -> mVideoReady = true);
mMediaPlayer.setOnPreparedListener(mediaPlayer -> mediaPlayer.setLooping(true));
}
}
@Override
public int getVideoWidth() {
return mMediaPlayer.getVideoWidth();
}
@Override
public int getVideoHeight() {
return mMediaPlayer.getVideoHeight();
}
@Override
public void pause() {
mMediaPlayer.pause();
}
@Override
public void start() {
mMediaPlayer.start();
}
@Override
public boolean isPlaying() {
return mMediaPlayer.isPlaying();
}
@Override
public int getDuration() {
return mMediaPlayer.getDuration();
}
@Override
public void attachView(TextureView video, View preview, View playButton) {
updateViewStates(preview, playButton);
video.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width,
int height) {
setSurface(surfaceTexture);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width,
int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
preview.setVisibility(View.VISIBLE);
mSurface = null;
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
setSurface(surfaceTexture);
if (mVideoReady) {
if (preview.getVisibility() == View.VISIBLE) {
preview.setVisibility(View.GONE);
}
if (mMediaPlayer != null
&& !mMediaPlayer.isPlaying()) {
mMediaPlayer.start();
playButton.setVisibility(View.GONE);
}
}
if (mMediaPlayer != null && !mMediaPlayer.isPlaying()
&& playButton.getVisibility() != View.VISIBLE) {
playButton.setVisibility(View.VISIBLE);
}
}
});
video.setOnClickListener(v -> updateViewStates(preview, playButton));
}
@Override
public void release() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.reset();
mMediaPlayer.release();
mMediaPlayer = null;
mVideoReady = false;
}
}
private void updateViewStates(View imageView, View playButton) {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
playButton.setVisibility(View.VISIBLE);
imageView.setVisibility(View.VISIBLE);
} else {
imageView.setVisibility(View.GONE);
playButton.setVisibility(View.GONE);
mMediaPlayer.start();
}
}
private void setSurface(SurfaceTexture surfaceTexture) {
if (mMediaPlayer != null && mSurface == null) {
mSurface = new Surface(surfaceTexture);
mMediaPlayer.setSurface(mSurface);
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.ImageView;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settingslib.utils.ColorUtil;
/** A preference with a Gear on the side and mutable Gear color. */
public class MutableGearPreference extends GearPreference {
private static final int VALUE_ENABLED_ALPHA = 255;
private ImageView mGear;
private Context mContext;
private int mDisabledAlphaValue;
public MutableGearPreference(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
mDisabledAlphaValue = (int) (ColorUtil.getDisabledAlpha(context) * VALUE_ENABLED_ALPHA);
}
@Override
public void setGearEnabled(boolean enabled) {
if (mGear != null) {
mGear.setEnabled(enabled);
mGear.setImageAlpha(enabled ? VALUE_ENABLED_ALPHA : mDisabledAlphaValue);
}
mGearState = enabled;
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
mGear = (ImageView) holder.findViewById(R.id.settings_button);
setGearEnabled(mGearState);
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2017 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.widget;
import android.content.Context;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.core.AbstractPreferenceController;
import java.util.ArrayList;
import java.util.List;
/**
* A controller for generic Preference categories. If all controllers for its children reports
* not-available, this controller will also report not-available, and subsequently will be hidden by
* UI.
*/
public class PreferenceCategoryController extends BasePreferenceController {
private final String mKey;
private final List<AbstractPreferenceController> mChildren;
public PreferenceCategoryController(Context context, String key) {
super(context, key);
mKey = key;
mChildren = new ArrayList<>();
}
@Override
public int getAvailabilityStatus() {
if (mChildren == null || mChildren.isEmpty()) {
return UNSUPPORTED_ON_DEVICE;
}
// Category is available if any child is available
for (AbstractPreferenceController controller : mChildren) {
if (controller.isAvailable()) {
return AVAILABLE;
}
}
return CONDITIONALLY_UNAVAILABLE;
}
@Override
public String getPreferenceKey() {
return mKey;
}
public PreferenceCategoryController setChildren(
List<AbstractPreferenceController> childrenController) {
mChildren.clear();
if (childrenController != null) {
mChildren.addAll(childrenController);
}
return this;
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (C) 2018 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.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.CheckBox;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settingslib.widget.TwoTargetPreference;
/**
* A custom preference that provides inline checkbox. It has a mandatory field for title, and
* optional fields for icon and sub-text.
*/
public class PrimaryCheckBoxPreference extends TwoTargetPreference {
private CheckBox mCheckBox;
private boolean mChecked;
private boolean mEnableCheckBox = true;
public PrimaryCheckBoxPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public PrimaryCheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public PrimaryCheckBoxPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PrimaryCheckBoxPreference(Context context) {
super(context);
}
@Override
protected int getSecondTargetResId() {
return R.layout.preference_widget_primary_checkbox;
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final View widgetView = holder.findViewById(android.R.id.widget_frame);
if (widgetView != null) {
widgetView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mCheckBox != null && !mCheckBox.isEnabled()) {
return;
}
setChecked(!mChecked);
if (!callChangeListener(mChecked)) {
setChecked(!mChecked);
} else {
persistBoolean(mChecked);
}
}
});
}
mCheckBox = (CheckBox) holder.findViewById(R.id.checkboxWidget);
if (mCheckBox != null) {
mCheckBox.setContentDescription(getTitle());
mCheckBox.setChecked(mChecked);
mCheckBox.setEnabled(mEnableCheckBox);
}
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
setCheckBoxEnabled(enabled);
}
public boolean isChecked() {
return mCheckBox != null && mChecked;
}
/**
* Set the check status of checkbox
* @param checked
*/
public void setChecked(boolean checked) {
mChecked = checked;
if (mCheckBox != null) {
mCheckBox.setChecked(checked);
}
}
/**
* Set the enabled status of CheckBox
* @param enabled
*/
public void setCheckBoxEnabled(boolean enabled) {
mEnableCheckBox = enabled;
if (mCheckBox != null) {
mCheckBox.setEnabled(enabled);
}
}
public CheckBox getCheckBox() {
return mCheckBox;
}
}

View File

@@ -0,0 +1,316 @@
/*
* Copyright (C) 2017 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.widget;
import android.annotation.AnyRes;
import android.content.Context;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.LayoutRes;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.Utils;
import com.android.settings.core.PreferenceXmlParserUtils;
import com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag;
import com.android.settingslib.widget.CandidateInfo;
import com.android.settingslib.widget.IllustrationPreference;
import com.android.settingslib.widget.SelectorWithWidgetPreference;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* A fragment to handle general radio button picker
*/
public abstract class RadioButtonPickerFragment extends SettingsPreferenceFragment implements
SelectorWithWidgetPreference.OnClickListener {
@VisibleForTesting
static final String EXTRA_FOR_WORK = "for_work";
private static final String TAG = "RadioButtonPckrFrgmt";
@VisibleForTesting
boolean mAppendStaticPreferences = false;
private final Map<String, CandidateInfo> mCandidates = new ArrayMap<>();
protected UserManager mUserManager;
protected int mUserId;
private int mIllustrationId;
private int mIllustrationPreviewId;
private IllustrationType mIllustrationType;
@Override
public void onAttach(Context context) {
super.onAttach(context);
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
final Bundle arguments = getArguments();
boolean mForWork = false;
if (arguments != null) {
mForWork = arguments.getBoolean(EXTRA_FOR_WORK);
}
final UserHandle managedProfile = Utils.getManagedProfile(mUserManager);
mUserId = mForWork && managedProfile != null
? managedProfile.getIdentifier()
: UserHandle.myUserId();
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
try {
// Check if the xml specifies if static preferences should go on the top or bottom
final List<Bundle> metadata = PreferenceXmlParserUtils.extractMetadata(getContext(),
getPreferenceScreenResId(),
MetadataFlag.FLAG_INCLUDE_PREF_SCREEN |
MetadataFlag.FLAG_NEED_PREF_APPEND);
mAppendStaticPreferences = metadata.get(0)
.getBoolean(PreferenceXmlParserUtils.METADATA_APPEND);
} catch (IOException e) {
Log.e(TAG, "Error trying to open xml file", e);
} catch (XmlPullParserException e) {
Log.e(TAG, "Error parsing xml", e);
}
updateCandidates();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View view = super.onCreateView(inflater, container, savedInstanceState);
setHasOptionsMenu(true);
return view;
}
@Override
protected abstract int getPreferenceScreenResId();
@Override
public void onRadioButtonClicked(SelectorWithWidgetPreference selected) {
final String selectedKey = selected.getKey();
onRadioButtonConfirmed(selectedKey);
}
/**
* Called after the user tries to select an item.
*/
protected void onSelectionPerformed(boolean success) {
}
/**
* Whether the UI should show a "None" item selection.
*/
protected boolean shouldShowItemNone() {
return false;
}
/**
* Populate any static preferences, independent of the radio buttons.
* These might be used to provide extra information about the choices.
**/
protected void addStaticPreferences(PreferenceScreen screen) {
}
protected CandidateInfo getCandidate(String key) {
return mCandidates.get(key);
}
protected void onRadioButtonConfirmed(String selectedKey) {
final boolean success = setDefaultKey(selectedKey);
if (success) {
updateCheckedState(selectedKey);
}
onSelectionPerformed(success);
}
/**
* A chance for subclasses to bind additional things to the preference.
*/
public void bindPreferenceExtra(SelectorWithWidgetPreference pref,
String key, CandidateInfo info, String defaultKey, String systemDefaultKey) {
}
public void updateCandidates() {
mCandidates.clear();
final List<? extends CandidateInfo> candidateList = getCandidates();
if (candidateList != null) {
for (CandidateInfo info : candidateList) {
mCandidates.put(info.getKey(), info);
}
}
final String defaultKey = getDefaultKey();
final String systemDefaultKey = getSystemDefaultKey();
final PreferenceScreen screen = getPreferenceScreen();
screen.removeAll();
if (mIllustrationId != 0) {
addIllustration(screen);
}
if (!mAppendStaticPreferences) {
addStaticPreferences(screen);
}
final int customLayoutResId = getRadioButtonPreferenceCustomLayoutResId();
if (shouldShowItemNone()) {
final SelectorWithWidgetPreference nonePref =
new SelectorWithWidgetPreference(getPrefContext());
if (customLayoutResId > 0) {
nonePref.setLayoutResource(customLayoutResId);
}
nonePref.setIcon(R.drawable.ic_remove_circle);
nonePref.setTitle(R.string.app_list_preference_none);
nonePref.setChecked(TextUtils.isEmpty(defaultKey));
nonePref.setOnClickListener(this);
screen.addPreference(nonePref);
}
if (candidateList != null) {
for (CandidateInfo info : candidateList) {
SelectorWithWidgetPreference pref =
new SelectorWithWidgetPreference(getPrefContext());
if (customLayoutResId > 0) {
pref.setLayoutResource(customLayoutResId);
}
bindPreference(pref, info.getKey(), info, defaultKey);
bindPreferenceExtra(pref, info.getKey(), info, defaultKey, systemDefaultKey);
screen.addPreference(pref);
}
}
mayCheckOnlyRadioButton();
if (mAppendStaticPreferences) {
addStaticPreferences(screen);
}
}
public SelectorWithWidgetPreference bindPreference(SelectorWithWidgetPreference pref,
String key, CandidateInfo info, String defaultKey) {
pref.setTitle(info.loadLabel());
pref.setIcon(Utils.getSafeIcon(info.loadIcon()));
pref.setKey(key);
if (TextUtils.equals(defaultKey, key)) {
pref.setChecked(true);
}
pref.setEnabled(info.enabled);
pref.setOnClickListener(this);
return pref;
}
public void updateCheckedState(String selectedKey) {
final PreferenceScreen screen = getPreferenceScreen();
if (screen != null) {
final int count = screen.getPreferenceCount();
for (int i = 0; i < count; i++) {
final Preference pref = screen.getPreference(i);
if (pref instanceof SelectorWithWidgetPreference) {
final SelectorWithWidgetPreference radioPref =
(SelectorWithWidgetPreference) pref;
final boolean newCheckedState = TextUtils.equals(pref.getKey(), selectedKey);
if (radioPref.isChecked() != newCheckedState) {
radioPref.setChecked(TextUtils.equals(pref.getKey(), selectedKey));
}
}
}
}
}
public void mayCheckOnlyRadioButton() {
final PreferenceScreen screen = getPreferenceScreen();
// If there is only 1 thing on screen, select it.
if (screen != null && screen.getPreferenceCount() == 1) {
final Preference onlyPref = screen.getPreference(0);
if (onlyPref instanceof SelectorWithWidgetPreference) {
((SelectorWithWidgetPreference) onlyPref).setChecked(true);
}
}
}
/**
* Allows you to set an illustration at the top of this screen. Set the illustration id to 0
* if you want to remove the illustration.
*
* @param illustrationId The res id for the raw of the illustration.
* @param previewId The res id for the drawable of the illustration.
* @param illustrationType The illustration type for the raw of the illustration.
*/
protected void setIllustration(@AnyRes int illustrationId, @AnyRes int previewId,
IllustrationType illustrationType) {
mIllustrationId = illustrationId;
mIllustrationPreviewId = previewId;
mIllustrationType = illustrationType;
}
/**
* Allows you to set an illustration at the top of this screen. Set the illustration id to 0
* if you want to remove the illustration.
*
* @param illustrationId The res id for the raw of the illustration.
* @param illustrationType The illustration type for the raw of the illustration.
*/
protected void setIllustration(@AnyRes int illustrationId, IllustrationType illustrationType) {
setIllustration(illustrationId, 0, illustrationType);
}
private void addIllustration(PreferenceScreen screen) {
switch (mIllustrationType) {
case LOTTIE_ANIMATION:
IllustrationPreference illustrationPreference = new IllustrationPreference(
getContext());
illustrationPreference.setLottieAnimationResId(mIllustrationId);
screen.addPreference(illustrationPreference);
break;
default:
throw new IllegalArgumentException(
"Invalid illustration type: " + mIllustrationType);
}
}
protected abstract List<? extends CandidateInfo> getCandidates();
protected abstract String getDefaultKey();
protected abstract boolean setDefaultKey(String key);
protected String getSystemDefaultKey() {
return null;
}
/**
* Provides a custom layout for each candidate row.
*/
@LayoutRes
protected int getRadioButtonPreferenceCustomLayoutResId() {
return 0;
}
protected enum IllustrationType {
LOTTIE_ANIMATION
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright (C) 2018 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.widget;
import android.content.Context;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceViewHolder;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedPreferenceHelper;
import com.android.settingslib.widget.AppPreference;
/**
* {@link AppPreference} that implements user restriction utilities using
* {@link com.android.settingslib.RestrictedPreferenceHelper}.
* Used to show policy transparency on {@link AppPreference}.
*/
public class RestrictedAppPreference extends AppPreference {
private RestrictedPreferenceHelper mHelper;
private String userRestriction;
public RestrictedAppPreference(Context context) {
super(context);
initialize(null, null);
}
public RestrictedAppPreference(Context context, String userRestriction) {
super(context);
initialize(null, userRestriction);
}
public RestrictedAppPreference(Context context, AttributeSet attrs, String userRestriction) {
super(context, attrs);
initialize(attrs, userRestriction);
}
private void initialize(AttributeSet attrs, String userRestriction) {
mHelper = new RestrictedPreferenceHelper(getContext(), this, attrs);
this.userRestriction = userRestriction;
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
mHelper.onBindViewHolder(holder);
}
@Override
public void performClick() {
if (!mHelper.performClick()) {
super.performClick();
}
}
@Override
public void setEnabled(boolean enabled) {
boolean changed = false;
if (enabled && isDisabledByAdmin()) {
mHelper.setDisabledByAdmin(null);
changed = true;
}
if (enabled && isDisabledByEcm()) {
mHelper.setDisabledByEcm(null);
changed = true;
}
if (!changed) {
super.setEnabled(enabled);
}
}
public void setDisabledByAdmin(RestrictedLockUtils.EnforcedAdmin admin) {
if (mHelper.setDisabledByAdmin(admin)) {
notifyChanged();
}
}
public boolean isDisabledByAdmin() {
return mHelper.isDisabledByAdmin();
}
public boolean isDisabledByEcm() {
return mHelper.isDisabledByEcm();
}
public void useAdminDisabledSummary(boolean useSummary) {
mHelper.useAdminDisabledSummary(useSummary);
}
@Override
protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
mHelper.onAttachedToHierarchy();
super.onAttachedToHierarchy(preferenceManager);
}
public void checkRestrictionAndSetDisabled() {
if (TextUtils.isEmpty(userRestriction)) {
return;
}
mHelper.checkRestrictionAndSetDisabled(userRestriction, UserHandle.myUserId());
}
public void checkRestrictionAndSetDisabled(String userRestriction) {
mHelper.checkRestrictionAndSetDisabled(userRestriction, UserHandle.myUserId());
}
public void checkRestrictionAndSetDisabled(String userRestriction, int userId) {
mHelper.checkRestrictionAndSetDisabled(userRestriction, userId);
}
/**
* Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this
* package. Marks the preference as disabled if so.
* @param settingIdentifier The key identifying the setting
* @param packageName the package to check the settingIdentifier for
*/
public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
@NonNull String packageName) {
mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName);
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2023 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.widget;
import android.content.Context;
import android.os.UserHandle;
import android.util.AttributeSet;
import android.widget.Button;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import com.android.settingslib.RestrictedLockUtilsInternal;
/**
* A preference with a plus button on the side representing an "add" action. The plus button will
* only be visible when a non-null click listener is registered.
*/
public class RestrictedButton extends Button {
private UserHandle mUserHandle;
private String mUserRestriction;
public RestrictedButton(Context context) {
super(context);
}
public RestrictedButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RestrictedButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public RestrictedButton(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean performClick() {
EnforcedAdmin admin = getEnforcedAdmin();
if (admin != null) {
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mContext, admin);
return false;
}
return super.performClick();
}
/** Initialize the button with {@link UserHandle} and a restriction */
public void init(UserHandle userHandle, String restriction) {
setAllowClickWhenDisabled(true);
mUserHandle = userHandle;
mUserRestriction = restriction;
}
/** Update the restriction state */
public void updateState() {
setEnabled(getEnforcedAdmin() == null);
}
private EnforcedAdmin getEnforcedAdmin() {
if (mUserHandle != null) {
EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
mContext, mUserRestriction, mUserHandle.getIdentifier());
if (admin != null) {
return admin;
}
}
return null;
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.widget;
import android.content.Context;
import android.util.AttributeSet;
import androidx.preference.PreferenceViewHolder;
import com.android.settingslib.RestrictedTopLevelPreference;
/** Homepage preference that can be disabled by a device admin using a user restriction. */
public class RestrictedHomepagePreference extends RestrictedTopLevelPreference implements
HomepagePreferenceLayoutHelper.HomepagePreferenceLayout {
private final HomepagePreferenceLayoutHelper mHelper;
public RestrictedHomepagePreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mHelper = new HomepagePreferenceLayoutHelper(this);
}
public RestrictedHomepagePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mHelper = new HomepagePreferenceLayoutHelper(this);
}
public RestrictedHomepagePreference(Context context, AttributeSet attrs) {
super(context, attrs);
mHelper = new HomepagePreferenceLayoutHelper(this);
}
public RestrictedHomepagePreference(Context context) {
super(context);
mHelper = new HomepagePreferenceLayoutHelper(this);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
mHelper.onBindViewHolder(holder);
}
@Override
public HomepagePreferenceLayoutHelper getHelper() {
return mHelper;
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2016 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.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.ProgressBar;
import com.android.settings.R;
/**
* A (determinate) progress bar in the form of a ring. The progress bar goes clockwise starting
* from the 12 o'clock position. This view maintain equal width and height using a strategy similar
* to "centerInside" for ImageView.
*/
public class RingProgressBar extends ProgressBar {
public RingProgressBar(Context context) {
this(context, null);
}
public RingProgressBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RingProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, R.style.RingProgressBarStyle);
}
public RingProgressBar(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int measuredHeight = getMeasuredHeight();
final int measuredWidth = getMeasuredWidth();
final int shortSide = Math.min(measuredHeight, measuredWidth);
setMeasuredDimension(shortSide, shortSide);
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright (C) 2016 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.widget;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import androidx.viewpager.widget.ViewPager;
import java.util.Locale;
/**
* A {@link ViewPager} that's aware of RTL changes when used with FragmentPagerAdapter.
*/
public final class RtlCompatibleViewPager extends ViewPager {
/**
* Callback interface for responding to changing state of the selected page.
* Positions supplied will always be the logical position in the adapter -
* that is, the 0 index corresponds to the left-most page in LTR and the
* right-most page in RTL.
*/
public RtlCompatibleViewPager(Context context) {
this(context, null /* attrs */);
}
public RtlCompatibleViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public int getCurrentItem() {
return getRtlAwareIndex(super.getCurrentItem());
}
@Override
public void setCurrentItem(int item) {
super.setCurrentItem(getRtlAwareIndex(item));
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable parcelable = super.onSaveInstanceState();
RtlSavedState rtlSavedState = new RtlSavedState(parcelable);
rtlSavedState.position = getCurrentItem();
return rtlSavedState;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
RtlSavedState rtlSavedState = (RtlSavedState) state;
super.onRestoreInstanceState(rtlSavedState.getSuperState());
setCurrentItem(rtlSavedState.position);
}
/**
* Get a "RTL friendly" index. If the locale is LTR, the index is returned as is.
* Otherwise it's transformed so view pager can render views using the new index for RTL. For
* example, the second view will be rendered to the left of first view.
*
* @param index The logical index.
*/
public int getRtlAwareIndex(int index) {
// Using TextUtils rather than View.getLayoutDirection() because LayoutDirection is not
// defined until onMeasure, and this is called before then.
if (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
== View.LAYOUT_DIRECTION_RTL) {
return getAdapter().getCount() - index - 1;
}
return index;
}
static class RtlSavedState extends BaseSavedState {
int position;
public RtlSavedState(Parcelable superState) {
super(superState);
}
private RtlSavedState(Parcel in, ClassLoader loader) {
super(in, loader);
position = in.readInt();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(position);
}
public static final Parcelable.ClassLoaderCreator<RtlSavedState> CREATOR
= new Parcelable.ClassLoaderCreator<RtlSavedState>() {
@Override
public RtlSavedState createFromParcel(Parcel source,
ClassLoader loader) {
return new RtlSavedState(source, loader);
}
@Override
public RtlSavedState createFromParcel(Parcel in) {
return new RtlSavedState(in, null);
}
@Override
public RtlSavedState[] newArray(int size) {
return new RtlSavedState[size];
}
};
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2017 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.widget;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewParent;
import android.widget.ImeAwareEditText;
/**
* An EditText that, instead of scrolling to itself when focused, will request scrolling to its
* parent. This is used in ChooseLockPassword to do make a best effort for not hiding the error
* messages for why the password is invalid under the keyboard.
*/
public class ScrollToParentEditText extends ImeAwareEditText {
private Rect mRect = new Rect();
public ScrollToParentEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean requestRectangleOnScreen(Rect rectangle, boolean immediate) {
ViewParent parent = getParent();
if (parent instanceof View) {
// Request the entire parent view to be shown, which in ChooseLockPassword's case,
// will include messages for why the password is invalid (if any).
((View) parent).getDrawingRect(mRect);
return ((View) parent).requestRectangleOnScreen(mRect, immediate);
} else {
return super.requestRectangleOnScreen(rectangle, immediate);
}
}
}

View File

@@ -0,0 +1,478 @@
/*
* Copyright (C) 2011 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.widget;
import static android.view.HapticFeedbackConstants.CLOCK_TICK;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_SETTINGS_SLIDER;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import androidx.core.content.res.TypedArrayUtils;
import androidx.preference.PreferenceViewHolder;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.settingslib.RestrictedPreference;
/**
* Based on android.preference.SeekBarPreference, but uses support preference as base.
*/
public class SeekBarPreference extends RestrictedPreference
implements OnSeekBarChangeListener, View.OnKeyListener, View.OnHoverListener {
public static final int HAPTIC_FEEDBACK_MODE_NONE = 0;
public static final int HAPTIC_FEEDBACK_MODE_ON_TICKS = 1;
public static final int HAPTIC_FEEDBACK_MODE_ON_ENDS = 2;
private final InteractionJankMonitor mJankMonitor = InteractionJankMonitor.getInstance();
private int mProgress;
private int mMax;
private int mMin;
private boolean mTrackingTouch;
private boolean mContinuousUpdates;
private int mHapticFeedbackMode = HAPTIC_FEEDBACK_MODE_NONE;
private int mDefaultProgress = -1;
private SeekBar mSeekBar;
private boolean mShouldBlink;
private int mAccessibilityRangeInfoType = AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT;
private CharSequence mOverrideSeekBarStateDescription;
private CharSequence mSeekBarContentDescription;
private CharSequence mSeekBarStateDescription;
private OnSeekBarChangeListener mOnSeekBarChangeListener;
public SeekBarPreference(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.ProgressBar, defStyleAttr, defStyleRes);
setMax(a.getInt(com.android.internal.R.styleable.ProgressBar_max, mMax));
setMin(a.getInt(com.android.internal.R.styleable.ProgressBar_min, mMin));
a.recycle();
a = context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.SeekBarPreference, defStyleAttr, defStyleRes);
final int layoutResId = a.getResourceId(
com.android.internal.R.styleable.SeekBarPreference_layout,
com.android.internal.R.layout.preference_widget_seekbar);
a.recycle();
setSelectable(false);
setLayoutResource(layoutResId);
}
public SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public SeekBarPreference(Context context, AttributeSet attrs) {
this(context, attrs, TypedArrayUtils.getAttr(context,
androidx.preference.R.attr.seekBarPreferenceStyle,
com.android.internal.R.attr.seekBarPreferenceStyle));
}
public SeekBarPreference(Context context) {
this(context, null);
}
/**
* A callback that notifies clients when the seekbar progress level has been
* changed. See {@link OnSeekBarChangeListener} for more info.
*/
public void setOnSeekBarChangeListener(OnSeekBarChangeListener listener) {
mOnSeekBarChangeListener = listener;
}
public void setShouldBlink(boolean shouldBlink) {
mShouldBlink = shouldBlink;
notifyChanged();
}
@Override
public boolean isSelectable() {
if(isDisabledByAdmin()) {
return true;
} else {
return super.isSelectable();
}
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
view.itemView.setOnKeyListener(this);
view.itemView.setOnHoverListener(this);
mSeekBar = (SeekBar) view.findViewById(
com.android.internal.R.id.seekbar);
mSeekBar.setOnSeekBarChangeListener(this);
mSeekBar.setMax(mMax);
mSeekBar.setMin(mMin);
mSeekBar.setProgress(mProgress);
mSeekBar.setEnabled(isEnabled());
final CharSequence title = getTitle();
if (!TextUtils.isEmpty(mSeekBarContentDescription)) {
mSeekBar.setContentDescription(mSeekBarContentDescription);
} else if (!TextUtils.isEmpty(title)) {
mSeekBar.setContentDescription(title);
}
if (!TextUtils.isEmpty(mSeekBarStateDescription)) {
mSeekBar.setStateDescription(mSeekBarStateDescription);
}
if (mSeekBar instanceof DefaultIndicatorSeekBar) {
((DefaultIndicatorSeekBar) mSeekBar).setDefaultProgress(mDefaultProgress);
}
if (mShouldBlink) {
View v = view.itemView;
v.post(() -> {
if (v.getBackground() != null) {
final int centerX = v.getWidth() / 2;
final int centerY = v.getHeight() / 2;
v.getBackground().setHotspot(centerX, centerY);
}
v.setPressed(true);
v.setPressed(false);
mShouldBlink = false;
});
}
mSeekBar.setAccessibilityDelegate(new View.AccessibilityDelegate() {
@Override
public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(view, info);
// Update the range info with the correct type
AccessibilityNodeInfo.RangeInfo rangeInfo = info.getRangeInfo();
if (rangeInfo != null) {
info.setRangeInfo(AccessibilityNodeInfo.RangeInfo.obtain(
mAccessibilityRangeInfoType, rangeInfo.getMin(),
rangeInfo.getMax(), rangeInfo.getCurrent()));
}
if (mOverrideSeekBarStateDescription != null) {
info.setStateDescription(mOverrideSeekBarStateDescription);
}
}
});
}
@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
setProgress(restoreValue ? getPersistedInt(mProgress)
: (Integer) defaultValue);
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return a.getInt(index, 0);
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() != KeyEvent.ACTION_DOWN) {
return false;
}
SeekBar seekBar = (SeekBar) v.findViewById(com.android.internal.R.id.seekbar);
if (seekBar == null) {
return false;
}
return seekBar.onKeyDown(keyCode, event);
}
public void setMax(int max) {
if (max != mMax) {
mMax = max;
notifyChanged();
}
}
public void setMin(int min) {
if (min != mMin) {
mMin = min;
notifyChanged();
}
}
public int getMax() {
return mMax;
}
public int getMin() {
return mMin;
}
public void setProgress(int progress) {
setProgress(progress, true);
}
/**
* Sets the progress point to draw a single tick mark representing a default value.
*/
public void setDefaultProgress(int defaultProgress) {
if (mDefaultProgress != defaultProgress) {
mDefaultProgress = defaultProgress;
if (mSeekBar instanceof DefaultIndicatorSeekBar) {
((DefaultIndicatorSeekBar) mSeekBar).setDefaultProgress(mDefaultProgress);
}
}
}
/**
* When {@code continuousUpdates} is true, update the persisted setting immediately as the thumb
* is dragged along the SeekBar. Otherwise, only update the value of the setting when the thumb
* is dropped.
*/
public void setContinuousUpdates(boolean continuousUpdates) {
mContinuousUpdates = continuousUpdates;
}
/**
* Sets the haptic feedback mode. HAPTIC_FEEDBACK_MODE_ON_TICKS means to perform haptic feedback
* as the SeekBar's progress is updated; HAPTIC_FEEDBACK_MODE_ON_ENDS means to perform haptic
* feedback as the SeekBar's progress value is equal to the min/max value.
*
* @param hapticFeedbackMode the haptic feedback mode.
*/
public void setHapticFeedbackMode(int hapticFeedbackMode) {
mHapticFeedbackMode = hapticFeedbackMode;
}
private void setProgress(int progress, boolean notifyChanged) {
if (progress > mMax) {
progress = mMax;
}
if (progress < mMin) {
progress = mMin;
}
if (progress != mProgress) {
mProgress = progress;
persistInt(progress);
if (notifyChanged) {
notifyChanged();
}
}
}
public int getProgress() {
return mProgress;
}
/**
* Persist the seekBar's progress value if callChangeListener
* returns true, otherwise set the seekBar's progress to the stored value
*/
void syncProgress(SeekBar seekBar) {
int progress = seekBar.getProgress();
if (progress != mProgress) {
if (callChangeListener(progress)) {
setProgress(progress, false);
switch (mHapticFeedbackMode) {
case HAPTIC_FEEDBACK_MODE_ON_TICKS:
seekBar.performHapticFeedback(CLOCK_TICK);
break;
case HAPTIC_FEEDBACK_MODE_ON_ENDS:
if (progress == mMax || progress == mMin) {
seekBar.performHapticFeedback(CLOCK_TICK);
}
break;
}
} else {
seekBar.setProgress(mProgress);
}
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser && (mContinuousUpdates || !mTrackingTouch)) {
syncProgress(seekBar);
}
if (mOnSeekBarChangeListener != null) {
mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
mTrackingTouch = true;
mJankMonitor.begin(InteractionJankMonitor.Configuration.Builder
.withView(CUJ_SETTINGS_SLIDER, seekBar)
.setTag(getKey()));
if (mOnSeekBarChangeListener != null) {
mOnSeekBarChangeListener.onStartTrackingTouch(seekBar);
}
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
mTrackingTouch = false;
if (seekBar.getProgress() != mProgress) {
syncProgress(seekBar);
}
if (mOnSeekBarChangeListener != null) {
mOnSeekBarChangeListener.onStopTrackingTouch(seekBar);
}
mJankMonitor.end(CUJ_SETTINGS_SLIDER);
}
/**
* Specify the type of range this seek bar represents.
*
* @param rangeInfoType The type of range to be shared with accessibility
*
* @see android.view.accessibility.AccessibilityNodeInfo.RangeInfo
*/
public void setAccessibilityRangeInfoType(int rangeInfoType) {
mAccessibilityRangeInfoType = rangeInfoType;
}
public void setSeekBarContentDescription(CharSequence contentDescription) {
mSeekBarContentDescription = contentDescription;
if (mSeekBar != null) {
mSeekBar.setContentDescription(contentDescription);
}
}
/**
* Specify the state description for this seek bar represents.
*
* @param stateDescription the state description of seek bar
*/
public void setSeekBarStateDescription(CharSequence stateDescription) {
mSeekBarStateDescription = stateDescription;
if (mSeekBar != null) {
mSeekBar.setStateDescription(stateDescription);
}
}
/**
* Overrides the state description of {@link SeekBar} with given content.
*/
public void overrideSeekBarStateDescription(CharSequence stateDescription) {
mOverrideSeekBarStateDescription = stateDescription;
}
@Override
protected Parcelable onSaveInstanceState() {
/*
* Suppose a client uses this preference type without persisting. We
* must save the instance state so it is able to, for example, survive
* orientation changes.
*/
final Parcelable superState = super.onSaveInstanceState();
if (isPersistent()) {
// No need to save instance state since it's persistent
return superState;
}
// Save the instance state
final SavedState myState = new SavedState(superState);
myState.progress = mProgress;
myState.max = mMax;
myState.min = mMin;
return myState;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (!state.getClass().equals(SavedState.class)) {
// Didn't save state for us in onSaveInstanceState
super.onRestoreInstanceState(state);
return;
}
// Restore the instance state
SavedState myState = (SavedState) state;
super.onRestoreInstanceState(myState.getSuperState());
mProgress = myState.progress;
mMax = myState.max;
mMin = myState.min;
notifyChanged();
}
@Override
public boolean onHover(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_HOVER_ENTER:
v.setHovered(true);
break;
case MotionEvent.ACTION_HOVER_EXIT:
v.setHovered(false);
break;
}
return false;
}
/**
* SavedState, a subclass of {@link BaseSavedState}, will store the state
* of MyPreference, a subclass of Preference.
* <p>
* It is important to always call through to super methods.
*/
private static class SavedState extends BaseSavedState {
int progress;
int max;
int min;
public SavedState(Parcel source) {
super(source);
// Restore the click counter
progress = source.readInt();
max = source.readInt();
min = source.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
// Save the click counter
dest.writeInt(progress);
dest.writeInt(max);
dest.writeInt(min);
}
public SavedState(Parcelable superState) {
super(superState);
}
@SuppressWarnings("unused")
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}

View File

@@ -0,0 +1,153 @@
/*
* Copyright (C) 2020 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.widget;
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.widget.MainSwitchBar;
/**
* A {@link MainSwitchBar} with a customized Switch and provides the metrics feature.
*/
public class SettingsMainSwitchBar extends MainSwitchBar {
/**
* Called before the checked state of the Switch has changed.
*/
public interface OnBeforeCheckedChangeListener {
/**
* @param isChecked The new checked state of switchView.
*/
boolean onBeforeCheckedChanged(boolean isChecked);
}
private EnforcedAdmin mEnforcedAdmin;
private boolean mDisabledByAdmin;
private final MetricsFeatureProvider mMetricsFeatureProvider;
private OnBeforeCheckedChangeListener mOnBeforeListener;
private int mMetricsCategory;
public SettingsMainSwitchBar(Context context) {
this(context, null);
}
public SettingsMainSwitchBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SettingsMainSwitchBar(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public SettingsMainSwitchBar(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
addOnSwitchChangeListener((switchView, isChecked) -> logMetrics(isChecked));
}
/**
* If admin is not null, disables the text and switch but keeps the view clickable (unless the
* switch is disabled for other reasons). Otherwise, calls setEnabled.
*/
public void setDisabledByAdmin(EnforcedAdmin admin) {
mEnforcedAdmin = admin;
if (admin != null) {
super.setEnabled(true);
mDisabledByAdmin = true;
mTextView.setEnabled(false);
mSwitch.setEnabled(false);
} else {
mDisabledByAdmin = false;
mSwitch.setVisibility(View.VISIBLE);
setEnabled(isEnabled());
}
}
@Override
public void setEnabled(boolean enabled) {
if (enabled && mDisabledByAdmin) {
setDisabledByAdmin(null);
return;
}
super.setEnabled(enabled);
}
/**
* Called by the restricted icon clicked.
*/
@Override
public boolean performClick() {
if (mDisabledByAdmin) {
performRestrictedClick();
return true;
}
return mSwitch.performClick();
}
@Override
public void setChecked(boolean checked) {
if (mOnBeforeListener != null
&& mOnBeforeListener.onBeforeCheckedChanged(checked)) {
return;
}
super.setChecked(checked);
}
/**
* Update the status of switch but doesn't notify the mOnBeforeListener.
*/
public void setCheckedInternal(boolean checked) {
super.setChecked(checked);
}
/**
* Set the OnBeforeCheckedChangeListener.
*/
public void setOnBeforeCheckedChangeListener(OnBeforeCheckedChangeListener listener) {
mOnBeforeListener = listener;
}
/**
* Set the metrics tag.
*/
public void setMetricsCategory(int category) {
mMetricsCategory = category;
}
private void logMetrics(boolean isChecked) {
mMetricsFeatureProvider.changed(mMetricsCategory, "switch_bar", isChecked ? 1 : 0);
}
private void performRestrictedClick() {
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), mEnforcedAdmin);
mMetricsFeatureProvider.clicked(mMetricsCategory, "switch_bar|restricted");
}
}

View File

@@ -0,0 +1,246 @@
/*
* 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.widget;
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import androidx.preference.PreferenceViewHolder;
import androidx.preference.TwoStatePreference;
import com.android.settings.R;
import com.android.settings.widget.SettingsMainSwitchBar.OnBeforeCheckedChangeListener;
import com.android.settingslib.RestrictedPreferenceHelper;
import com.android.settingslib.core.instrumentation.SettingsJankMonitor;
import java.util.ArrayList;
import java.util.List;
/**
* SettingsMainSwitchPreference is a Preference with a customized Switch.
* This component is used as the main switch of the page
* to enable or disable the preferences on the page.
*/
public class SettingsMainSwitchPreference extends TwoStatePreference implements
OnCheckedChangeListener {
private final List<OnBeforeCheckedChangeListener> mBeforeCheckedChangeListeners =
new ArrayList<>();
private final List<OnCheckedChangeListener> mSwitchChangeListeners = new ArrayList<>();
private SettingsMainSwitchBar mMainSwitchBar;
private EnforcedAdmin mEnforcedAdmin;
private RestrictedPreferenceHelper mRestrictedHelper;
public SettingsMainSwitchPreference(Context context) {
super(context);
init(context, null);
}
public SettingsMainSwitchPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public SettingsMainSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
public SettingsMainSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
holder.setDividerAllowedAbove(false);
holder.setDividerAllowedBelow(false);
if (mEnforcedAdmin == null && mRestrictedHelper != null) {
mEnforcedAdmin = mRestrictedHelper.checkRestrictionEnforced();
}
mMainSwitchBar = (SettingsMainSwitchBar) holder.findViewById(R.id.main_switch_bar);
initMainSwitchBar();
if (isVisible()) {
mMainSwitchBar.show();
if (mMainSwitchBar.isChecked() != isChecked()) {
setChecked(isChecked());
}
registerListenerToSwitchBar();
} else {
mMainSwitchBar.hide();
}
}
private void init(Context context, AttributeSet attrs) {
setLayoutResource(R.layout.preference_widget_main_switch);
mSwitchChangeListeners.add(this);
if (attrs != null) {
mRestrictedHelper = new RestrictedPreferenceHelper(context, this, attrs);
}
}
@Override
public void setChecked(boolean checked) {
super.setChecked(checked);
if (mMainSwitchBar != null) {
mMainSwitchBar.setChecked(checked);
}
}
/**
* Return the SettingsMainSwitchBar
*/
public final SettingsMainSwitchBar getSwitchBar() {
return mMainSwitchBar;
}
@Override
public void setTitle(CharSequence title) {
super.setTitle(title);
if (mMainSwitchBar != null) {
mMainSwitchBar.setTitle(title);
}
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
super.setChecked(isChecked);
SettingsJankMonitor.detectToggleJank(getKey(), buttonView);
}
/**
* Show the MainSwitchBar
*/
public void show() {
setVisible(true);
if (mMainSwitchBar != null) {
mMainSwitchBar.show();
}
}
/**
* Hide the MainSwitchBar
*/
public void hide() {
setVisible(false);
if (mMainSwitchBar != null) {
mMainSwitchBar.hide();
}
}
/**
* Returns if the MainSwitchBar is visible.
*/
public boolean isShowing() {
if (mMainSwitchBar != null) {
return mMainSwitchBar.isShowing();
}
return false;
}
/**
* Update the status of switch but doesn't notify the mOnBeforeListener.
*/
public void setCheckedInternal(boolean checked) {
super.setChecked(checked);
if (mMainSwitchBar != null) {
mMainSwitchBar.setCheckedInternal(checked);
}
}
/**
* Enable or disable the text and switch.
*/
public void setSwitchBarEnabled(boolean enabled) {
setEnabled(enabled);
if (mMainSwitchBar != null) {
mMainSwitchBar.setEnabled(enabled);
}
}
/**
* Set the OnBeforeCheckedChangeListener.
*/
public void setOnBeforeCheckedChangeListener(OnBeforeCheckedChangeListener listener) {
if (!mBeforeCheckedChangeListeners.contains(listener)) {
mBeforeCheckedChangeListeners.add(listener);
}
if (mMainSwitchBar != null) {
mMainSwitchBar.setOnBeforeCheckedChangeListener(listener);
}
}
/**
* Adds a listener for switch changes
*/
public void addOnSwitchChangeListener(OnCheckedChangeListener listener) {
if (!mSwitchChangeListeners.contains(listener)) {
mSwitchChangeListeners.add(listener);
}
if (mMainSwitchBar != null) {
mMainSwitchBar.addOnSwitchChangeListener(listener);
}
}
/**
* Remove a listener for switch changes
*/
public void removeOnSwitchChangeListener(OnCheckedChangeListener listener) {
mSwitchChangeListeners.remove(listener);
if (mMainSwitchBar != null) {
mMainSwitchBar.removeOnSwitchChangeListener(listener);
}
}
/**
* If admin is not null, disables the text and switch but keeps the view clickable.
* Otherwise, calls setEnabled which will enables the entire view including
* the text and switch.
*/
public void setDisabledByAdmin(EnforcedAdmin admin) {
mEnforcedAdmin = admin;
if (mMainSwitchBar != null) {
mMainSwitchBar.setDisabledByAdmin(mEnforcedAdmin);
}
}
private void initMainSwitchBar() {
if (mMainSwitchBar != null) {
mMainSwitchBar.setTitle(getTitle());
mMainSwitchBar.setDisabledByAdmin(mEnforcedAdmin);
}
}
private void registerListenerToSwitchBar() {
for (OnBeforeCheckedChangeListener listener : mBeforeCheckedChangeListeners) {
mMainSwitchBar.setOnBeforeCheckedChangeListener(listener);
}
for (OnCheckedChangeListener listener : mSwitchChangeListeners) {
mMainSwitchBar.addOnSwitchChangeListener(listener);
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.widget;
import android.content.Context;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.core.TogglePreferenceController;
import com.android.settingslib.widget.MainSwitchPreference;
/**
* Preference controller for MainSwitchPreference.
*/
public abstract class SettingsMainSwitchPreferenceController extends
TogglePreferenceController implements OnCheckedChangeListener {
protected MainSwitchPreference mSwitchPreference;
public SettingsMainSwitchPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
final Preference pref = screen.findPreference(getPreferenceKey());
if (pref != null && pref instanceof MainSwitchPreference) {
mSwitchPreference = (MainSwitchPreference) pref;
mSwitchPreference.addOnSwitchChangeListener(this);
}
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mSwitchPreference.setChecked(isChecked);
setChecked(isChecked);
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2018 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.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
/**
* A preference with single target and a gear icon on the side.
*/
public class SingleTargetGearPreference extends Preference {
public SingleTargetGearPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public SingleTargetGearPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public SingleTargetGearPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SingleTargetGearPreference(Context context) {
super(context);
init();
}
private void init() {
setLayoutResource(R.layout.preference_single_target);
setWidgetLayoutResource(R.layout.preference_widget_gear_optional_background);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final View divider = holder.findViewById(
com.android.settingslib.widget.preference.twotarget.R.id.two_target_divider);
if (divider != null) {
divider.setVisibility(View.INVISIBLE);
}
}
}

View File

@@ -0,0 +1,187 @@
/*
* Copyright (C) 2016 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.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.viewpager.widget.PagerAdapter;
import com.android.settings.R;
/**
* To be used with ViewPager to provide a tab indicator component which give constant feedback as
* to the user's scroll progress.
*/
public final class SlidingTabLayout extends FrameLayout implements View.OnClickListener {
private final LinearLayout mTitleView;
private final View mIndicatorView;
private final LayoutInflater mLayoutInflater;
private RtlCompatibleViewPager mViewPager;
private int mSelectedPosition;
private float mSelectionOffset;
public SlidingTabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mLayoutInflater = LayoutInflater.from(context);
mTitleView = new LinearLayout(context);
mTitleView.setGravity(Gravity.CENTER_HORIZONTAL);
mIndicatorView = mLayoutInflater.inflate(R.layout.sliding_tab_indicator_view, this, false);
addView(mTitleView, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
addView(mIndicatorView, mIndicatorView.getLayoutParams());
}
/**
* Sets the associated view pager. Note that the assumption here is that the pager content
* (number of tabs and tab titles) does not change after this call has been made.
*/
public void setViewPager(RtlCompatibleViewPager viewPager) {
mTitleView.removeAllViews();
mViewPager = viewPager;
if (viewPager != null) {
viewPager.addOnPageChangeListener(new InternalViewPagerListener());
populateTabStrip();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int titleCount = mTitleView.getChildCount();
if (titleCount > 0) {
final int width = MeasureSpec.makeMeasureSpec(
mTitleView.getMeasuredWidth() / titleCount, MeasureSpec.EXACTLY);
final int height = MeasureSpec.makeMeasureSpec(
mIndicatorView.getMeasuredHeight(), MeasureSpec.EXACTLY);
mIndicatorView.measure(width, height);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (mTitleView.getChildCount() > 0) {
final int indicatorBottom = getMeasuredHeight();
final int indicatorHeight = mIndicatorView.getMeasuredHeight();
final int indicatorWidth = mIndicatorView.getMeasuredWidth();
final int totalWidth = getMeasuredWidth();
final int leftPadding = getPaddingLeft();
final int rightPadding = getPaddingRight();
mTitleView.layout(leftPadding, 0, mTitleView.getMeasuredWidth() + rightPadding,
mTitleView.getMeasuredHeight());
// IndicatorView should start on the right when RTL mode is enabled
if (isRtlMode()) {
mIndicatorView.layout(totalWidth - indicatorWidth,
indicatorBottom - indicatorHeight, totalWidth,
indicatorBottom);
} else {
mIndicatorView.layout(0, indicatorBottom - indicatorHeight,
indicatorWidth, indicatorBottom);
}
}
}
@Override
public void onClick(View v) {
final int titleCount = mTitleView.getChildCount();
for (int i = 0; i < titleCount; i++) {
if (v == mTitleView.getChildAt(i)) {
mViewPager.setCurrentItem(i);
return;
}
}
}
private void onViewPagerPageChanged(int position, float positionOffset) {
mSelectedPosition = position;
mSelectionOffset = positionOffset;
// Translation should be reversed in RTL mode
final int leftIndicator = isRtlMode() ? -getIndicatorLeft() : getIndicatorLeft();
mIndicatorView.setTranslationX(leftIndicator);
}
private void populateTabStrip() {
final PagerAdapter adapter = mViewPager.getAdapter();
for (int i = 0; i < adapter.getCount(); i++) {
final TextView tabTitleView = (TextView) mLayoutInflater.inflate(
R.layout.sliding_tab_title_view, mTitleView, false);
tabTitleView.setText(adapter.getPageTitle(i));
tabTitleView.setOnClickListener(this);
mTitleView.addView(tabTitleView);
tabTitleView.setSelected(i == mViewPager.getCurrentItem());
}
}
private int getIndicatorLeft() {
View selectedTitle = mTitleView.getChildAt(mSelectedPosition);
int left = selectedTitle.getLeft();
if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) {
View nextTitle = mTitleView.getChildAt(mSelectedPosition + 1);
left = (int) (mSelectionOffset * nextTitle.getLeft()
+ (1.0f - mSelectionOffset) * left);
}
return left;
}
private boolean isRtlMode() {
return getLayoutDirection() == LAYOUT_DIRECTION_RTL;
}
private final class InternalViewPagerListener implements
RtlCompatibleViewPager.OnPageChangeListener {
private int mScrollState;
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
final int titleCount = mTitleView.getChildCount();
if ((titleCount == 0) || (position < 0) || (position >= titleCount)) {
return;
}
onViewPagerPageChanged(position, positionOffset);
}
@Override
public void onPageScrollStateChanged(int state) {
mScrollState = state;
}
@Override
public void onPageSelected(int position) {
position = mViewPager.getRtlAwareIndex(position);
if (mScrollState == RtlCompatibleViewPager.SCROLL_STATE_IDLE) {
onViewPagerPageChanged(position, 0f);
}
final int titleCount = mTitleView.getChildCount();
for (int i = 0; i < titleCount; i++) {
mTitleView.getChildAt(i).setSelected(position == i);
}
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2017 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.widget;
import android.content.Context;
import android.text.TextUtils;
/**
* Helper class that listens to settings changes and notifies client when there is update in
* corresponding summary info.
*/
public abstract class SummaryUpdater {
protected final Context mContext;
private final OnSummaryChangeListener mListener;
private String mSummary;
/**
* Interface definition for a callback to be invoked when the summary has been changed.
*/
public interface OnSummaryChangeListener {
/**
* Called when summary has changed.
*
* @param summary The new summary .
*/
void onSummaryChanged(String summary);
}
/**
* Constructor
*
* @param context The Context the updater is running in, through which it can register broadcast
* receiver etc.
* @param listener The listener that would like to receive summary change notification.
*
*/
public SummaryUpdater(Context context, OnSummaryChangeListener listener) {
mContext = context;
mListener = listener;
}
/**
* Notifies the listener when there is update in summary
*/
protected void notifyChangeIfNeeded() {
String summary = getSummary();
if (!TextUtils.equals(mSummary, summary)) {
mSummary = summary;
if (mListener != null) {
mListener.onSummaryChanged(summary);
}
}
}
/**
* Starts/stops receiving updates on the summary.
*
* @param register true if we want to receive updates, false otherwise
*/
public abstract void register(boolean register);
/**
* Gets the summary. Subclass should checks latest conditions and update the summary
* accordingly.
*
* @return the latest summary text
*/
protected abstract String getSummary();
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright (C) 2017 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.widget;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
/*
* A controller class for general switch widget handling. We have different containers that provide
* different forms of switch layout. Provide a centralized control for updating the switch widget.
*/
public abstract class SwitchWidgetController {
protected OnSwitchChangeListener mListener;
/**
* Interface definition for a callback to be invoked when the switch has been toggled.
*/
public interface OnSwitchChangeListener {
/**
* Called when the checked state of the Switch has changed.
*
* @param isChecked The new checked state of switchView.
*
* @return true to update the state of the switch with the new value.
*/
boolean onSwitchToggled(boolean isChecked);
}
/**
* Perform any view setup.
*/
public void setupView() {
}
/**
* Perform any view teardown.
*/
public void teardownView() {
}
/**
* Set the callback to be invoked when the switch is toggled by the user (but before the
* internal state has been updated).
*
* @param listener the callback to be invoked
*/
public void setListener(OnSwitchChangeListener listener) {
mListener = listener;
}
/**
* Update the preference title associated with the switch.
*
* @param title the preference title
*/
public abstract void setTitle(String title);
/**
* Start listening to switch toggling.
*/
public abstract void startListening();
/**
* Stop listening to switch toggling.
*/
public abstract void stopListening();
/**
* Set the checked state for the switch.
*
* @param checked whether the switch should be checked or not.
*/
public abstract void setChecked(boolean checked);
/**
* Get the checked state for the switch.
*
* @return true if the switch is currently checked, false otherwise.
*/
public abstract boolean isChecked();
/**
* Set the enabled state for the switch.
*
* @param enabled whether the switch should be enabled or not.
*/
public abstract void setEnabled(boolean enabled);
/**
* Disable the switch based on the enforce admin.
*
* @param admin Details of the admin who enforced the restriction. If it
* is {@code null}, then this preference will be enabled. Otherwise, it will be disabled.
*/
public abstract void setDisabledByAdmin(EnforcedAdmin admin);
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2023 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.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
/** A preference with tick icon. */
public class TickButtonPreference extends Preference {
private ImageView mCheckIcon;
private boolean mIsSelected = false;
public TickButtonPreference(Context context) {
super(context);
init(context, null);
}
public TickButtonPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
setWidgetLayoutResource(R.layout.preference_check_icon);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
mCheckIcon = (ImageView) holder.findViewById(R.id.check_icon);
setSelected(mIsSelected);
}
/** Set icon state.*/
public void setSelected(boolean isSelected) {
if (mCheckIcon != null) {
mCheckIcon.setVisibility(isSelected ? View.VISIBLE : View.INVISIBLE);
}
mIsSelected = isSelected;
}
/** Return state of presenting icon. */
public boolean isSelected() {
return mIsSelected;
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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.widget;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import android.graphics.drawable.DrawableWrapper;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.R;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
/**
* A Drawable that tints a contained Drawable, overriding the existing tint specified in the
* underlying drawable. This class should only be used in XML.
*
* @attr ref android.R.styleable#DrawableWrapper_drawable
* @attr ref R.styleable#TintDrawable_tint
*/
public class TintDrawable extends DrawableWrapper {
private ColorStateList mTint;
private int[] mThemeAttrs;
/** No-arg constructor used by drawable inflation. */
public TintDrawable() {
super(null);
}
@Override
public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Theme theme)
throws XmlPullParserException, IOException {
final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.TintDrawable);
super.inflate(r, parser, attrs, theme);
mThemeAttrs = a.extractThemeAttrs();
updateStateFromTypedArray(a);
a.recycle();
applyTint();
}
@Override
public void applyTheme(Theme t) {
super.applyTheme(t);
if (mThemeAttrs != null) {
final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.TintDrawable);
updateStateFromTypedArray(a);
a.recycle();
}
// Ensure tint is reapplied after applying the theme to ensure this drawables'
// tint overrides the underlying drawables' tint.
applyTint();
}
@Override
public boolean canApplyTheme() {
return (mThemeAttrs != null && mThemeAttrs.length > 0) || super.canApplyTheme();
}
private void updateStateFromTypedArray(@NonNull TypedArray a) {
if (a.hasValue(R.styleable.TintDrawable_android_drawable)) {
setDrawable(a.getDrawable(R.styleable.TintDrawable_android_drawable));
}
if (a.hasValue(R.styleable.TintDrawable_android_tint)) {
mTint = a.getColorStateList(R.styleable.TintDrawable_android_tint);
}
}
private void applyTint() {
if (getDrawable() != null && mTint != null) {
getDrawable().mutate().setTintList(mTint);
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2018 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.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.res.TypedArrayUtils;
import com.android.settings.R;
import com.android.settingslib.widget.LayoutPreference;
/**
* Preference that presents a button with two states(On vs Off)
*/
public class TwoStateButtonPreference extends LayoutPreference implements
View.OnClickListener {
private boolean mIsChecked;
private final Button mButtonOn;
private final Button mButtonOff;
public TwoStateButtonPreference(Context context, AttributeSet attrs) {
super(context, attrs, TypedArrayUtils.getAttr(
context, R.attr.twoStateButtonPreferenceStyle, android.R.attr.preferenceStyle));
if (attrs == null) {
mButtonOn = null;
mButtonOff = null;
} else {
final TypedArray styledAttrs = context.obtainStyledAttributes(attrs,
R.styleable.TwoStateButtonPreference);
final int textOnId = styledAttrs.getResourceId(
R.styleable.TwoStateButtonPreference_textOn,
R.string.summary_placeholder);
final int textOffId = styledAttrs.getResourceId(
R.styleable.TwoStateButtonPreference_textOff,
R.string.summary_placeholder);
styledAttrs.recycle();
mButtonOn = findViewById(R.id.state_on_button);
mButtonOn.setText(textOnId);
mButtonOn.setOnClickListener(this);
mButtonOff = findViewById(R.id.state_off_button);
mButtonOff.setText(textOffId);
mButtonOff.setOnClickListener(this);
setChecked(isChecked());
}
}
@Override
public void onClick(View v) {
final boolean stateOn = v.getId() == R.id.state_on_button;
setChecked(stateOn);
callChangeListener(stateOn);
}
public void setChecked(boolean checked) {
// Update state
mIsChecked = checked;
// And update UI
if (checked) {
mButtonOn.setVisibility(View.GONE);
mButtonOff.setVisibility(View.VISIBLE);
} else {
mButtonOn.setVisibility(View.VISIBLE);
mButtonOff.setVisibility(View.GONE);
}
}
public boolean isChecked() {
return mIsChecked;
}
public void setButtonEnabled(boolean enabled) {
mButtonOn.setEnabled(enabled);
mButtonOff.setEnabled(enabled);
}
@VisibleForTesting
public Button getStateOnButton() {
return mButtonOn;
}
@VisibleForTesting
public Button getStateOffButton() {
return mButtonOff;
}
}

View File

@@ -0,0 +1,174 @@
/*
* Copyright (C) 2023 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.widget;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog.Builder;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceDialogFragmentCompat;
import com.android.settingslib.core.instrumentation.Instrumentable;
import java.util.ArrayList;
/**
* {@link PreferenceDialogFragmentCompat} that updates the available options
* when {@code onListPreferenceUpdated} is called."
*/
public class UpdatableListPreferenceDialogFragment extends PreferenceDialogFragmentCompat implements
Instrumentable {
private static final String SAVE_STATE_INDEX = "UpdatableListPreferenceDialogFragment.index";
private static final String SAVE_STATE_ENTRIES =
"UpdatableListPreferenceDialogFragment.entries";
private static final String SAVE_STATE_ENTRY_VALUES =
"UpdatableListPreferenceDialogFragment.entryValues";
private static final String METRICS_CATEGORY_KEY = "metrics_category_key";
private ArrayAdapter mAdapter;
private int mClickedDialogEntryIndex;
private ArrayList<CharSequence> mEntries;
private CharSequence[] mEntryValues;
private int mMetricsCategory = METRICS_CATEGORY_UNKNOWN;
/**
* Creates a new instance of {@link UpdatableListPreferenceDialogFragment}.
*/
public static UpdatableListPreferenceDialogFragment newInstance(
String key, int metricsCategory) {
UpdatableListPreferenceDialogFragment fragment =
new UpdatableListPreferenceDialogFragment();
Bundle args = new Bundle(1);
args.putString(ARG_KEY, key);
args.putInt(METRICS_CATEGORY_KEY, metricsCategory);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Bundle bundle = getArguments();
mMetricsCategory =
bundle.getInt(METRICS_CATEGORY_KEY, METRICS_CATEGORY_UNKNOWN);
if (savedInstanceState == null) {
mEntries = new ArrayList<>();
setPreferenceData(getListPreference());
} else {
mClickedDialogEntryIndex = savedInstanceState.getInt(SAVE_STATE_INDEX, 0);
mEntries = savedInstanceState.getCharSequenceArrayList(SAVE_STATE_ENTRIES);
mEntryValues =
savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRY_VALUES);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(SAVE_STATE_INDEX, mClickedDialogEntryIndex);
outState.putCharSequenceArrayList(SAVE_STATE_ENTRIES, mEntries);
outState.putCharSequenceArray(SAVE_STATE_ENTRY_VALUES, mEntryValues);
}
@Override
public void onDialogClosed(boolean positiveResult) {
if (positiveResult && mClickedDialogEntryIndex >= 0) {
final ListPreference preference = getListPreference();
final String value = mEntryValues[mClickedDialogEntryIndex].toString();
if (preference.callChangeListener(value)) {
preference.setValue(value);
}
}
}
@VisibleForTesting
void setAdapter(ArrayAdapter adapter) {
mAdapter = adapter;
}
@VisibleForTesting
void setEntries(ArrayList<CharSequence> entries) {
mEntries = entries;
}
@VisibleForTesting
ArrayAdapter getAdapter() {
return mAdapter;
}
@VisibleForTesting
void setMetricsCategory(Bundle bundle) {
mMetricsCategory =
bundle.getInt(METRICS_CATEGORY_KEY, METRICS_CATEGORY_UNKNOWN);
}
@Override
protected void onPrepareDialogBuilder(Builder builder) {
super.onPrepareDialogBuilder(builder);
final TypedArray a = getContext().obtainStyledAttributes(
null,
com.android.internal.R.styleable.AlertDialog,
com.android.internal.R.attr.alertDialogStyle, 0);
mAdapter = new ArrayAdapter<>(
getContext(),
a.getResourceId(
com.android.internal.R.styleable.AlertDialog_singleChoiceItemLayout,
com.android.internal.R.layout.select_dialog_singlechoice),
mEntries);
builder.setSingleChoiceItems(mAdapter, mClickedDialogEntryIndex,
(dialog, which) -> {
mClickedDialogEntryIndex = which;
onClick(dialog, -1);
dialog.dismiss();
});
builder.setPositiveButton(null, null);
a.recycle();
}
@Override
public int getMetricsCategory() {
return mMetricsCategory;
}
@VisibleForTesting
ListPreference getListPreference() {
return (ListPreference) getPreference();
}
private void setPreferenceData(ListPreference preference) {
mEntries.clear();
mClickedDialogEntryIndex = preference.findIndexOfValue(preference.getValue());
for (CharSequence entry : preference.getEntries()) {
mEntries.add(entry);
}
mEntryValues = preference.getEntryValues();
}
/**
* Update new data set for list preference.
*/
public void onListPreferenceUpdated(ListPreference preference) {
if (mAdapter != null) {
setPreferenceData(preference);
mAdapter.notifyDataSetChanged();
}
}
}

View File

@@ -0,0 +1,331 @@
/*
* Copyright (C) 2016 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.widget;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.CornerPathEffect;
import android.graphics.DashPathEffect;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Join;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Shader.TileMode;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.SparseIntArray;
import android.util.TypedValue;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.fuelgauge.BatteryUtils;
public class UsageGraph extends View {
private static final int PATH_DELIM = -1;
public static final String LOG_TAG = "UsageGraph";
private final Paint mLinePaint;
private final Paint mFillPaint;
private final Paint mDottedPaint;
private final Drawable mDivider;
private final Drawable mTintedDivider;
private final int mDividerSize;
private final Path mPath = new Path();
// Paths in coordinates they are passed in.
private final SparseIntArray mPaths = new SparseIntArray();
// Paths in local coordinates for drawing.
private final SparseIntArray mLocalPaths = new SparseIntArray();
// Paths for projection in coordinates they are passed in.
private final SparseIntArray mProjectedPaths = new SparseIntArray();
// Paths for projection in local coordinates for drawing.
private final SparseIntArray mLocalProjectedPaths = new SparseIntArray();
private final int mCornerRadius;
private int mAccentColor;
private float mMaxX = 100;
private float mMaxY = 100;
private float mMiddleDividerLoc = .5f;
private int mMiddleDividerTint = -1;
private int mTopDividerTint = -1;
public UsageGraph(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
final Resources resources = context.getResources();
mLinePaint = new Paint();
mLinePaint.setStyle(Style.STROKE);
mLinePaint.setStrokeCap(Cap.ROUND);
mLinePaint.setStrokeJoin(Join.ROUND);
mLinePaint.setAntiAlias(true);
mCornerRadius = resources.getDimensionPixelSize(
com.android.settingslib.R.dimen.usage_graph_line_corner_radius);
mLinePaint.setPathEffect(new CornerPathEffect(mCornerRadius));
mLinePaint.setStrokeWidth(resources.getDimensionPixelSize(
com.android.settingslib.R.dimen.usage_graph_line_width));
mFillPaint = new Paint(mLinePaint);
mFillPaint.setStyle(Style.FILL);
mDottedPaint = new Paint(mLinePaint);
mDottedPaint.setStyle(Style.STROKE);
float dots = resources.getDimensionPixelSize(
com.android.settingslib.R.dimen.usage_graph_dot_size);
float interval = resources.getDimensionPixelSize(
com.android.settingslib.R.dimen.usage_graph_dot_interval);
mDottedPaint.setStrokeWidth(dots * 3);
mDottedPaint.setPathEffect(new DashPathEffect(new float[] {dots, interval}, 0));
mDottedPaint.setColor(context.getColor(R.color.usage_graph_dots));
TypedValue v = new TypedValue();
context.getTheme().resolveAttribute(com.android.internal.R.attr.listDivider, v, true);
mDivider = context.getDrawable(v.resourceId);
mTintedDivider = context.getDrawable(v.resourceId);
mDividerSize = resources.getDimensionPixelSize(
com.android.settingslib.R.dimen.usage_graph_divider_size);
}
void clearPaths() {
mPaths.clear();
mLocalPaths.clear();
mProjectedPaths.clear();
mLocalProjectedPaths.clear();
}
void setMax(int maxX, int maxY) {
final long startTime = System.currentTimeMillis();
mMaxX = maxX;
mMaxY = maxY;
calculateLocalPaths();
postInvalidate();
BatteryUtils.logRuntime(LOG_TAG, "setMax", startTime);
}
void setDividerLoc(int height) {
mMiddleDividerLoc = 1 - height / mMaxY;
}
void setDividerColors(int middleColor, int topColor) {
mMiddleDividerTint = middleColor;
mTopDividerTint = topColor;
}
public void addPath(SparseIntArray points) {
addPathAndUpdate(points, mPaths, mLocalPaths);
}
public void addProjectedPath(SparseIntArray points) {
addPathAndUpdate(points, mProjectedPaths, mLocalProjectedPaths);
}
private void addPathAndUpdate(
SparseIntArray points, SparseIntArray paths, SparseIntArray localPaths) {
final long startTime = System.currentTimeMillis();
for (int i = 0, size = points.size(); i < size; i++) {
paths.put(points.keyAt(i), points.valueAt(i));
}
// Add a delimiting value immediately after the last point.
paths.put(points.keyAt(points.size() - 1) + 1, PATH_DELIM);
calculateLocalPaths(paths, localPaths);
postInvalidate();
BatteryUtils.logRuntime(LOG_TAG, "addPathAndUpdate", startTime);
}
void setAccentColor(int color) {
mAccentColor = color;
mLinePaint.setColor(mAccentColor);
updateGradient();
postInvalidate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
final long startTime = System.currentTimeMillis();
super.onSizeChanged(w, h, oldw, oldh);
updateGradient();
calculateLocalPaths();
BatteryUtils.logRuntime(LOG_TAG, "onSizeChanged", startTime);
}
private void calculateLocalPaths() {
calculateLocalPaths(mPaths, mLocalPaths);
calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths);
}
@VisibleForTesting
void calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths) {
final long startTime = System.currentTimeMillis();
if (getWidth() == 0) {
return;
}
localPaths.clear();
// Store the local coordinates of the most recent point.
int lx = 0;
int ly = PATH_DELIM;
boolean skippedLastPoint = false;
for (int i = 0; i < paths.size(); i++) {
int x = paths.keyAt(i);
int y = paths.valueAt(i);
if (y == PATH_DELIM) {
if (i == 1) {
localPaths.put(getX(x+1) - 1, getY(0));
continue;
}
if (i == paths.size() - 1 && skippedLastPoint) {
// Add back skipped point to complete the path.
localPaths.put(lx, ly);
}
skippedLastPoint = false;
localPaths.put(lx + 1, PATH_DELIM);
} else {
lx = getX(x);
ly = getY(y);
// Skip this point if it is not far enough from the last one added.
if (localPaths.size() > 0) {
int lastX = localPaths.keyAt(localPaths.size() - 1);
int lastY = localPaths.valueAt(localPaths.size() - 1);
if (lastY != PATH_DELIM && !hasDiff(lastX, lx) && !hasDiff(lastY, ly)) {
skippedLastPoint = true;
continue;
}
}
skippedLastPoint = false;
localPaths.put(lx, ly);
}
}
BatteryUtils.logRuntime(LOG_TAG, "calculateLocalPaths", startTime);
}
private boolean hasDiff(int x1, int x2) {
return Math.abs(x2 - x1) >= mCornerRadius;
}
private int getX(float x) {
return (int) (x / mMaxX * getWidth());
}
private int getY(float y) {
return (int) (getHeight() * (1 - (y / mMaxY)));
}
private void updateGradient() {
mFillPaint.setShader(
new LinearGradient(
0, 0, 0, getHeight(), getColor(mAccentColor, .2f), 0, TileMode.CLAMP));
}
private int getColor(int color, float alphaScale) {
return (color & (((int) (0xff * alphaScale) << 24) | 0xffffff));
}
@Override
protected void onDraw(Canvas canvas) {
final long startTime = System.currentTimeMillis();
// Draw lines across the top, middle, and bottom.
if (mMiddleDividerLoc != 0) {
drawDivider(0, canvas, mTopDividerTint);
}
drawDivider(
(int) ((canvas.getHeight() - mDividerSize) * mMiddleDividerLoc),
canvas,
mMiddleDividerTint);
drawDivider(canvas.getHeight() - mDividerSize, canvas, -1);
if (mLocalPaths.size() == 0 && mLocalProjectedPaths.size() == 0) {
return;
}
canvas.save();
if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
// Flip the canvas along the y-axis of the center of itself before drawing paths.
canvas.scale(-1, 1, canvas.getWidth() * 0.5f, 0);
}
drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint);
drawFilledPath(canvas, mLocalPaths, mFillPaint);
drawLinePath(canvas, mLocalPaths, mLinePaint);
canvas.restore();
BatteryUtils.logRuntime(LOG_TAG, "onDraw", startTime);
}
private void drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
if (localPaths.size() == 0) {
return;
}
mPath.reset();
mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
for (int i = 1; i < localPaths.size(); i++) {
int x = localPaths.keyAt(i);
int y = localPaths.valueAt(i);
if (y == PATH_DELIM) {
if (++i < localPaths.size()) {
mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
}
} else {
mPath.lineTo(x, y);
}
}
canvas.drawPath(mPath, paint);
}
@VisibleForTesting
void drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
if (localPaths.size() == 0) {
return;
}
mPath.reset();
float lastStartX = localPaths.keyAt(0);
mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
for (int i = 1; i < localPaths.size(); i++) {
int x = localPaths.keyAt(i);
int y = localPaths.valueAt(i);
if (y == PATH_DELIM) {
mPath.lineTo(localPaths.keyAt(i - 1), getHeight());
mPath.lineTo(lastStartX, getHeight());
mPath.close();
if (++i < localPaths.size()) {
lastStartX = localPaths.keyAt(i);
mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
}
} else {
mPath.lineTo(x, y);
}
}
canvas.drawPath(mPath, paint);
}
private void drawDivider(int y, Canvas canvas, int tintColor) {
Drawable d = mDivider;
if (tintColor != -1) {
mTintedDivider.setTint(tintColor);
d = mTintedDivider;
}
d.setBounds(0, y, canvas.getWidth(), y + mDividerSize);
d.draw(canvas);
}
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright (C) 2016 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.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.SparseIntArray;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.settings.R;
import java.util.Locale;
public class UsageView extends FrameLayout {
private final UsageGraph mUsageGraph;
private final TextView[] mLabels;
private final TextView[] mBottomLabels;
public UsageView(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.usage_view, this);
mUsageGraph = findViewById(R.id.usage_graph);
mLabels = new TextView[] {
findViewById(R.id.label_bottom),
findViewById(R.id.label_middle),
findViewById(R.id.label_top),
};
mBottomLabels = new TextView[] {
findViewById(R.id.label_start),
findViewById(R.id.label_end),
};
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UsageView, 0, 0);
if (a.hasValue(R.styleable.UsageView_sideLabels)) {
setSideLabels(a.getTextArray(R.styleable.UsageView_sideLabels));
}
if (a.hasValue(R.styleable.UsageView_bottomLabels)) {
setBottomLabels(a.getTextArray(R.styleable.UsageView_bottomLabels));
}
if (a.hasValue(R.styleable.UsageView_textColor)) {
int color = a.getColor(R.styleable.UsageView_textColor, 0);
for (TextView v : mLabels) {
v.setTextColor(color);
}
for (TextView v : mBottomLabels) {
v.setTextColor(color);
}
}
if (a.hasValue(R.styleable.UsageView_android_gravity)) {
int gravity = a.getInt(R.styleable.UsageView_android_gravity, 0);
if (gravity == Gravity.END) {
LinearLayout layout = findViewById(R.id.graph_label_group);
LinearLayout labels = findViewById(R.id.label_group);
// Swap the children order.
layout.removeView(labels);
layout.addView(labels);
// Set gravity.
labels.setGravity(Gravity.END);
// Swap the bottom space order.
LinearLayout bottomLabels = findViewById(R.id.bottom_label_group);
View bottomSpace = bottomLabels.findViewById(R.id.bottom_label_space);
bottomLabels.removeView(bottomSpace);
bottomLabels.addView(bottomSpace);
} else if (gravity != Gravity.START) {
throw new IllegalArgumentException("Unsupported gravity " + gravity);
}
}
mUsageGraph.setAccentColor(a.getColor(R.styleable.UsageView_android_colorAccent, 0));
a.recycle();
// Locale Persian & Urdu are RTL languages but request LTR graph direction layout.
final String defaultLanguageCode = Locale.getDefault().getLanguage();
if (TextUtils.equals(defaultLanguageCode, new Locale("fa").getLanguage())
|| TextUtils.equals(defaultLanguageCode, new Locale("ur").getLanguage())) {
findViewById(R.id.graph_label_group).setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
findViewById(R.id.bottom_label_group).setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
}
}
public void clearPaths() {
mUsageGraph.clearPaths();
}
public void addPath(SparseIntArray points) {
mUsageGraph.addPath(points);
}
public void addProjectedPath(SparseIntArray points) {
mUsageGraph.addProjectedPath(points);
}
public void configureGraph(int maxX, int maxY) {
mUsageGraph.setMax(maxX, maxY);
}
public void setAccentColor(int color) {
mUsageGraph.setAccentColor(color);
}
public void setDividerLoc(int dividerLoc) {
mUsageGraph.setDividerLoc(dividerLoc);
}
public void setDividerColors(int middleColor, int topColor) {
mUsageGraph.setDividerColors(middleColor, topColor);
}
public void setSideLabelWeights(float before, float after) {
setWeight(R.id.space1, before);
setWeight(R.id.space2, after);
}
private void setWeight(int id, float weight) {
View v = findViewById(id);
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) v.getLayoutParams();
params.weight = weight;
v.setLayoutParams(params);
}
public void setSideLabels(CharSequence[] labels) {
if (labels.length != mLabels.length) {
throw new IllegalArgumentException("Invalid number of labels");
}
for (int i = 0; i < mLabels.length; i++) {
mLabels[i].setText(labels[i]);
}
}
public void setBottomLabels(CharSequence[] labels) {
if (labels.length != mBottomLabels.length) {
throw new IllegalArgumentException("Invalid number of labels");
}
for (int i = 0; i < mBottomLabels.length; i++) {
mBottomLabels[i].setText(labels[i]);
}
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (C) 2017 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.widget;
import android.content.Context;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.PreferenceViewHolder;
import com.android.settingslib.CustomEditTextPreferenceCompat;
/**
* {@code EditTextPreference} that supports input validation.
*/
public class ValidatedEditTextPreference extends CustomEditTextPreferenceCompat {
public interface Validator {
boolean isTextValid(String value);
}
private final EditTextWatcher mTextWatcher = new EditTextWatcher();
private Validator mValidator;
private boolean mIsPassword;
private boolean mIsSummaryPassword;
public ValidatedEditTextPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ValidatedEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ValidatedEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ValidatedEditTextPreference(Context context) {
super(context);
}
@Override
protected void onBindDialogView(View view) {
super.onBindDialogView(view);
final EditText editText = view.findViewById(android.R.id.edit);
if (editText != null && !TextUtils.isEmpty(editText.getText())) {
editText.setSelection(editText.getText().length());
}
if (mValidator != null && editText != null) {
editText.removeTextChangedListener(mTextWatcher);
if (mIsPassword) {
editText.setInputType(
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
editText.setMaxLines(1);
}
editText.addTextChangedListener(mTextWatcher);
}
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final TextView textView = (TextView) holder.findViewById(android.R.id.summary);
if (textView == null) {
return;
}
if (mIsSummaryPassword) {
textView.setInputType(
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
} else {
textView.setInputType(
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
}
}
public void setIsPassword(boolean isPassword) {
mIsPassword = isPassword;
}
public void setIsSummaryPassword(boolean isPassword) {
mIsSummaryPassword = isPassword;
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
public boolean isPassword() {
return mIsPassword;
}
public void setValidator(Validator validator) {
mValidator = validator;
}
private class EditTextWatcher implements TextWatcher {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void beforeTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
final EditText editText = getEditText();
if (mValidator != null && editText != null) {
final AlertDialog dialog = (AlertDialog) getDialog();
final boolean valid = mValidator.isTextValid(editText.getText().toString());
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(valid);
}
}
}
}

View File

@@ -0,0 +1,111 @@
/*
* 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.widget;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.TextureView;
import android.view.View;
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
/**
* A {@link VideoPreference.AnimationController} containing a {@link
* AnimatedVectorDrawableCompat}. The controller is used by {@link VideoPreference}
* to display AnimatedVectorDrawable content.
*/
class VectorAnimationController implements VideoPreference.AnimationController {
private AnimatedVectorDrawableCompat mAnimatedVectorDrawableCompat;
private Drawable mPreviewDrawable;
private Animatable2Compat.AnimationCallback mAnimationCallback;
/**
* Called by a preference panel fragment to finish itself.
*
* @param context Application Context
* @param animationId An {@link android.graphics.drawable.AnimationDrawable} resource id
*/
VectorAnimationController(Context context, int animationId) {
mAnimatedVectorDrawableCompat = AnimatedVectorDrawableCompat.create(context, animationId);
mAnimationCallback = new Animatable2Compat.AnimationCallback() {
@Override
public void onAnimationEnd(Drawable drawable) {
mAnimatedVectorDrawableCompat.start();
}
};
}
@Override
public int getVideoWidth() {
return mAnimatedVectorDrawableCompat.getIntrinsicWidth();
}
@Override
public int getVideoHeight() {
return mAnimatedVectorDrawableCompat.getIntrinsicHeight();
}
@Override
public void pause() {
mAnimatedVectorDrawableCompat.stop();
}
@Override
public void start() {
mAnimatedVectorDrawableCompat.start();
}
@Override
public boolean isPlaying() {
return mAnimatedVectorDrawableCompat.isRunning();
}
@Override
public int getDuration() {
// We can't get duration from AnimatedVectorDrawable, just return a non zero value.
return 5000;
}
@Override
public void attachView(TextureView video, View preview, View playButton) {
mPreviewDrawable = preview.getForeground();
video.setVisibility(View.GONE);
updateViewStates(preview, playButton);
preview.setOnClickListener(v -> updateViewStates(preview, playButton));
}
@Override
public void release() {
mAnimatedVectorDrawableCompat.stop();
mAnimatedVectorDrawableCompat.clearAnimationCallbacks();
}
private void updateViewStates(View imageView, View playButton) {
if (mAnimatedVectorDrawableCompat.isRunning()) {
mAnimatedVectorDrawableCompat.stop();
mAnimatedVectorDrawableCompat.clearAnimationCallbacks();
playButton.setVisibility(View.VISIBLE);
imageView.setForeground(mPreviewDrawable);
} else {
playButton.setVisibility(View.GONE);
imageView.setForeground(mAnimatedVectorDrawableCompat);
mAnimatedVectorDrawableCompat.start();
mAnimatedVectorDrawableCompat.registerAnimationCallback(mAnimationCallback);
}
}
}

View File

@@ -0,0 +1,250 @@
/*
* Copyright (C) 2016 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.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.TextureView;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
import com.android.settings.R;
/**
* A full width preference that hosts a MP4 video or a {@link AnimatedVectorDrawableCompat}.
*/
public class VideoPreference extends Preference {
private static final String TAG = "VideoPreference";
private final Context mContext;
@VisibleForTesting
AnimationController mAnimationController;
@VisibleForTesting
boolean mAnimationAvailable;
private float mAspectRatio = 1.0f;
private int mPreviewId;
private int mAnimationId;
private int mVectorAnimationId;
private int mHeight = LinearLayout.LayoutParams.MATCH_PARENT - 1; // video height in pixels
private TextureView mVideo;
private ImageView mPreviewImage;
private ImageView mPlayButton;
public VideoPreference(Context context) {
super(context);
mContext = context;
initialize(context, null);
}
public VideoPreference(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
initialize(context, attrs);
}
private void initialize(Context context, AttributeSet attrs) {
TypedArray attributes = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.VideoPreference,
0, 0);
try {
// if these are already set that means they were set dynamically and don't need
// to be loaded from xml
mAnimationAvailable = false;
mAnimationId = mAnimationId == 0
? attributes.getResourceId(R.styleable.VideoPreference_animation, 0)
: mAnimationId;
mPreviewId = mPreviewId == 0
? attributes.getResourceId(R.styleable.VideoPreference_preview, 0)
: mPreviewId;
mVectorAnimationId = attributes.getResourceId(
R.styleable.VideoPreference_vectorAnimation, 0);
if (mPreviewId == 0 && mAnimationId == 0 && mVectorAnimationId == 0) {
setVisible(false);
return;
}
initAnimationController();
if (mAnimationController != null && mAnimationController.getDuration() > 0) {
setVisible(true);
setLayoutResource(R.layout.video_preference);
mAnimationAvailable = true;
updateAspectRatio();
} else {
setVisible(false);
}
} catch (Exception e) {
Log.w(TAG, "Animation resource not found. Will not show animation.");
} finally {
attributes.recycle();
}
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
if (!mAnimationAvailable) {
return;
}
mVideo = (TextureView) holder.findViewById(R.id.video_texture_view);
mPreviewImage = (ImageView) holder.findViewById(R.id.video_preview_image);
mPlayButton = (ImageView) holder.findViewById(R.id.video_play_button);
final AspectRatioFrameLayout layout = (AspectRatioFrameLayout) holder.findViewById(
R.id.video_container);
mPreviewImage.setImageResource(mPreviewId);
layout.setAspectRatio(mAspectRatio);
if (mHeight >= LinearLayout.LayoutParams.MATCH_PARENT) {
layout.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, mHeight));
}
if (mAnimationController != null) {
mAnimationController.attachView(mVideo, mPreviewImage, mPlayButton);
}
}
@Override
public void onDetached() {
releaseAnimationController();
super.onDetached();
}
/**
* Called from {@link VideoPreferenceController} when the view is onResume
*/
public void onViewVisible() {
initAnimationController();
}
/**
* Called from {@link VideoPreferenceController} when the view is onPause
*/
public void onViewInvisible() {
releaseAnimationController();
}
/**
* Sets the video for this preference. If a previous video was set this one will override it
* and properly release any resources and re-initialize the preference to play the new video.
*
* @param videoId The raw res id of the video
* @param previewId The drawable res id of the preview image to use if the video fails to load.
*/
public void setVideo(int videoId, int previewId) {
mAnimationId = videoId;
mPreviewId = previewId;
releaseAnimationController();
initialize(mContext, null);
}
private void initAnimationController() {
if (mVectorAnimationId != 0) {
mAnimationController = new VectorAnimationController(mContext, mVectorAnimationId);
return;
}
if (mAnimationId != 0) {
mAnimationController = new MediaAnimationController(mContext, mAnimationId);
if (mVideo != null) {
mAnimationController.attachView(mVideo, mPreviewImage, mPlayButton);
}
}
}
private void releaseAnimationController() {
if (mAnimationController != null) {
mAnimationController.release();
mAnimationController = null;
}
}
public boolean isAnimationAvailable() {
return mAnimationAvailable;
}
/**
* sets the height of the video preference
*
* @param height in dp
*/
public void setHeight(float height) {
mHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, height,
mContext.getResources().getDisplayMetrics());
}
@VisibleForTesting
void updateAspectRatio() {
mAspectRatio = mAnimationController.getVideoWidth()
/ (float) mAnimationController.getVideoHeight();
}
/**
* Handle animation operations.
*/
interface AnimationController {
/**
* Pauses the animation.
*/
void pause();
/**
* Starts the animation.
*/
void start();
/**
* Releases the animation object.
*/
void release();
/**
* Attaches the animation to UI view.
*/
void attachView(TextureView video, View preview, View playButton);
/**
* Returns the animation Width.
*/
int getVideoWidth();
/**
* Returns the animation Height.
*/
int getVideoHeight();
/**
* Returns the animation duration.
*/
int getDuration();
/**
* Returns if the animation is playing.
*/
boolean isPlaying();
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2018 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.widget;
import android.content.Context;
import androidx.preference.PreferenceScreen;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
public class VideoPreferenceController extends BasePreferenceController implements
LifecycleObserver, OnStart, OnStop {
private VideoPreference mVideoPreference;
public VideoPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
}
@Override
public int getAvailabilityStatus() {
return mVideoPreference != null && mVideoPreference.isAnimationAvailable() ?
AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
mVideoPreference = screen.findPreference(getPreferenceKey());
super.displayPreference(screen);
}
@Override
public void onStart() {
if (mVideoPreference != null) {
mVideoPreference.onViewVisible();
}
}
@Override
public void onStop() {
if (mVideoPreference != null) {
mVideoPreference.onViewInvisible();
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2016 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.widget;
import android.content.Context;
import android.os.UserManager;
import android.util.AttributeSet;
import androidx.preference.PreferenceCategory;
import com.android.settings.SelfAvailablePreference;
import com.android.settings.Utils;
/**
* A PreferenceCategory that is only visible when the device has a work profile.
*/
public class WorkOnlyCategory extends PreferenceCategory implements SelfAvailablePreference {
public WorkOnlyCategory(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean isAvailable(Context context) {
return Utils.getManagedProfile(UserManager.get(context)) != null;
}
}