1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.widget;
18 
19 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
20 
21 import android.app.settings.SettingsEnums;
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.graphics.drawable.Drawable;
25 import android.os.Parcel;
26 import android.os.Parcelable;
27 import android.text.SpannableStringBuilder;
28 import android.text.TextUtils;
29 import android.text.style.TextAppearanceSpan;
30 import android.util.AttributeSet;
31 import android.view.LayoutInflater;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.widget.CompoundButton;
35 import android.widget.ImageView;
36 import android.widget.LinearLayout;
37 import android.widget.Switch;
38 import android.widget.TextView;
39 
40 import androidx.annotation.ColorInt;
41 import androidx.annotation.StringRes;
42 import androidx.annotation.VisibleForTesting;
43 
44 import com.android.settings.R;
45 import com.android.settings.overlay.FeatureFactory;
46 import com.android.settingslib.RestrictedLockUtils;
47 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
48 
49 import java.util.ArrayList;
50 import java.util.List;
51 
52 public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedChangeListener {
53 
54     public interface OnSwitchChangeListener {
55         /**
56          * Called when the checked state of the Switch has changed.
57          *
58          * @param switchView The Switch view whose state has changed.
59          * @param isChecked  The new checked state of switchView.
60          */
onSwitchChanged(Switch switchView, boolean isChecked)61         void onSwitchChanged(Switch switchView, boolean isChecked);
62     }
63 
64     private static final int[] XML_ATTRIBUTES = {
65             R.attr.switchBarMarginStart,
66             R.attr.switchBarMarginEnd,
67             R.attr.switchBarBackgroundColor,
68             R.attr.switchBarBackgroundActivatedColor,
69             R.attr.switchBarRestrictionIcon};
70 
71     private final List<OnSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>();
72     private final MetricsFeatureProvider mMetricsFeatureProvider;
73     private final TextAppearanceSpan mSummarySpan;
74 
75     private ToggleSwitch mSwitch;
76     private ImageView mRestrictedIcon;
77     private TextView mTextView;
78     private String mLabel;
79     private String mSummary;
80     @ColorInt
81     private int mBackgroundColor;
82     @ColorInt
83     private int mBackgroundActivatedColor;
84     @StringRes
85     private int mOnTextId;
86     @StringRes
87     private int mOffTextId;
88 
89     private boolean mLoggingIntialized;
90     private boolean mDisabledByAdmin;
91     private EnforcedAdmin mEnforcedAdmin = null;
92     private String mMetricsTag;
93 
94 
SwitchBar(Context context)95     public SwitchBar(Context context) {
96         this(context, null);
97     }
98 
SwitchBar(Context context, AttributeSet attrs)99     public SwitchBar(Context context, AttributeSet attrs) {
100         this(context, attrs, 0);
101     }
102 
SwitchBar(Context context, AttributeSet attrs, int defStyleAttr)103     public SwitchBar(Context context, AttributeSet attrs, int defStyleAttr) {
104         this(context, attrs, defStyleAttr, 0);
105     }
106 
SwitchBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)107     public SwitchBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
108         super(context, attrs, defStyleAttr, defStyleRes);
109 
110         LayoutInflater.from(context).inflate(R.layout.switch_bar, this);
111         // Set the whole SwitchBar focusable and clickable.
112         setFocusable(true);
113         setClickable(true);
114 
115         final TypedArray a = context.obtainStyledAttributes(attrs, XML_ATTRIBUTES);
116         final int switchBarMarginStart = (int) a.getDimension(0, 0);
117         final int switchBarMarginEnd = (int) a.getDimension(1, 0);
118         mBackgroundColor = a.getColor(2, 0);
119         mBackgroundActivatedColor = a.getColor(3, 0);
120         final Drawable restrictedIconDrawable = a.getDrawable(4);
121         a.recycle();
122 
123         mTextView = findViewById(R.id.switch_text);
124         mSummarySpan = new TextAppearanceSpan(mContext, R.style.TextAppearance_Small_SwitchBar);
125         ViewGroup.MarginLayoutParams lp = (MarginLayoutParams) mTextView.getLayoutParams();
126         lp.setMarginStart(switchBarMarginStart);
127 
128         mSwitch = findViewById(R.id.switch_widget);
129         // Prevent onSaveInstanceState() to be called as we are managing the state of the Switch
130         // on our own
131         mSwitch.setSaveEnabled(false);
132         // Set the ToggleSwitch non-focusable and non-clickable to avoid multiple focus.
133         mSwitch.setFocusable(false);
134         mSwitch.setClickable(false);
135 
136         lp = (MarginLayoutParams) mSwitch.getLayoutParams();
137         lp.setMarginEnd(switchBarMarginEnd);
138         setBackgroundColor(mBackgroundColor);
139 
140         setSwitchBarText(R.string.switch_on_text, R.string.switch_off_text);
141 
142         addOnSwitchChangeListener(
143                 (switchView, isChecked) -> setTextViewLabelAndBackground(isChecked));
144 
145         mRestrictedIcon = findViewById(R.id.restricted_icon);
146         mRestrictedIcon.setImageDrawable(restrictedIconDrawable);
147         mRestrictedIcon.setOnClickListener(new View.OnClickListener() {
148             @Override
149             public void onClick(View v) {
150                 if (mDisabledByAdmin) {
151                     mMetricsFeatureProvider.action(
152                             SettingsEnums.PAGE_UNKNOWN,
153                             SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE,
154                             SettingsEnums.PAGE_UNKNOWN,
155                             mMetricsTag + "/switch_bar|restricted",
156                             1);
157 
158                     RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context,
159                             mEnforcedAdmin);
160                 }
161             }
162         });
163 
164         // Default is hide
165         setVisibility(View.GONE);
166 
167         mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
168     }
169 
170     // Override the performClick method to eliminate redundant click.
171     @Override
performClick()172     public boolean performClick() {
173         return getDelegatingView().performClick();
174     }
175 
setMetricsTag(String tag)176     public void setMetricsTag(String tag) {
177         mMetricsTag = tag;
178     }
179 
setTextViewLabelAndBackground(boolean isChecked)180     public void setTextViewLabelAndBackground(boolean isChecked) {
181         mLabel = getResources().getString(isChecked ? mOnTextId : mOffTextId);
182         setBackgroundColor(isChecked ? mBackgroundActivatedColor : mBackgroundColor);
183         updateText();
184     }
185 
setSwitchBarText(int onText, int offText)186     public void setSwitchBarText(int onText, int offText) {
187         mOnTextId = onText;
188         mOffTextId = offText;
189         setTextViewLabelAndBackground(isChecked());
190     }
191 
setSummary(String summary)192     public void setSummary(String summary) {
193         mSummary = summary;
194         updateText();
195     }
196 
updateText()197     private void updateText() {
198         if (TextUtils.isEmpty(mSummary)) {
199             mTextView.setText(mLabel);
200             return;
201         }
202         final SpannableStringBuilder ssb = new SpannableStringBuilder(mLabel).append('\n');
203         final int start = ssb.length();
204         ssb.append(mSummary);
205         ssb.setSpan(mSummarySpan, start, ssb.length(), 0);
206         mTextView.setText(ssb);
207     }
208 
setChecked(boolean checked)209     public void setChecked(boolean checked) {
210         setTextViewLabelAndBackground(checked);
211         mSwitch.setChecked(checked);
212     }
213 
setCheckedInternal(boolean checked)214     public void setCheckedInternal(boolean checked) {
215         setTextViewLabelAndBackground(checked);
216         mSwitch.setCheckedInternal(checked);
217     }
218 
isChecked()219     public boolean isChecked() {
220         return mSwitch.isChecked();
221     }
222 
setEnabled(boolean enabled)223     public void setEnabled(boolean enabled) {
224         if (enabled && mDisabledByAdmin) {
225             setDisabledByAdmin(null);
226             return;
227         }
228         super.setEnabled(enabled);
229         mTextView.setEnabled(enabled);
230         mSwitch.setEnabled(enabled);
231     }
232 
233     @VisibleForTesting
getDelegatingView()234     View getDelegatingView() {
235         return mDisabledByAdmin ? mRestrictedIcon : mSwitch;
236     }
237 
238     /**
239      * If admin is not null, disables the text and switch but keeps the view clickable.
240      * Otherwise, calls setEnabled which will enables the entire view including
241      * the text and switch.
242      */
setDisabledByAdmin(EnforcedAdmin admin)243     public void setDisabledByAdmin(EnforcedAdmin admin) {
244         mEnforcedAdmin = admin;
245         if (admin != null) {
246             super.setEnabled(true);
247             mDisabledByAdmin = true;
248             mTextView.setEnabled(false);
249             mSwitch.setEnabled(false);
250             mSwitch.setVisibility(View.GONE);
251             mRestrictedIcon.setVisibility(View.VISIBLE);
252             mRestrictedIcon.setFocusable(false);
253             mRestrictedIcon.setClickable(false);
254         } else {
255             mDisabledByAdmin = false;
256             mSwitch.setVisibility(View.VISIBLE);
257             mRestrictedIcon.setVisibility(View.GONE);
258             setEnabled(true);
259         }
260     }
261 
getSwitch()262     public final ToggleSwitch getSwitch() {
263         return mSwitch;
264     }
265 
show()266     public void show() {
267         if (!isShowing()) {
268             setVisibility(View.VISIBLE);
269             mSwitch.setOnCheckedChangeListener(this);
270         }
271     }
272 
hide()273     public void hide() {
274         if (isShowing()) {
275             setVisibility(View.GONE);
276             mSwitch.setOnCheckedChangeListener(null);
277         }
278     }
279 
isShowing()280     public boolean isShowing() {
281         return (getVisibility() == View.VISIBLE);
282     }
283 
propagateChecked(boolean isChecked)284     public void propagateChecked(boolean isChecked) {
285         final int count = mSwitchChangeListeners.size();
286         for (int n = 0; n < count; n++) {
287             mSwitchChangeListeners.get(n).onSwitchChanged(mSwitch, isChecked);
288         }
289     }
290 
291     @Override
onCheckedChanged(CompoundButton buttonView, boolean isChecked)292     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
293         if (mLoggingIntialized) {
294             mMetricsFeatureProvider.action(
295                     SettingsEnums.PAGE_UNKNOWN,
296                     SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE,
297                     SettingsEnums.PAGE_UNKNOWN,
298                     mMetricsTag + "/switch_bar",
299                     isChecked ? 1 : 0);
300         }
301         mLoggingIntialized = true;
302         propagateChecked(isChecked);
303     }
304 
addOnSwitchChangeListener(OnSwitchChangeListener listener)305     public void addOnSwitchChangeListener(OnSwitchChangeListener listener) {
306         if (mSwitchChangeListeners.contains(listener)) {
307             throw new IllegalStateException("Cannot add twice the same OnSwitchChangeListener");
308         }
309         mSwitchChangeListeners.add(listener);
310     }
311 
removeOnSwitchChangeListener(OnSwitchChangeListener listener)312     public void removeOnSwitchChangeListener(OnSwitchChangeListener listener) {
313         if (!mSwitchChangeListeners.contains(listener)) {
314             throw new IllegalStateException("Cannot remove OnSwitchChangeListener");
315         }
316         mSwitchChangeListeners.remove(listener);
317     }
318 
319     static class SavedState extends BaseSavedState {
320         boolean checked;
321         boolean visible;
322 
SavedState(Parcelable superState)323         SavedState(Parcelable superState) {
324             super(superState);
325         }
326 
327         /**
328          * Constructor called from {@link #CREATOR}
329          */
SavedState(Parcel in)330         private SavedState(Parcel in) {
331             super(in);
332             checked = (Boolean) in.readValue(null);
333             visible = (Boolean) in.readValue(null);
334         }
335 
336         @Override
writeToParcel(Parcel out, int flags)337         public void writeToParcel(Parcel out, int flags) {
338             super.writeToParcel(out, flags);
339             out.writeValue(checked);
340             out.writeValue(visible);
341         }
342 
343         @Override
toString()344         public String toString() {
345             return "SwitchBar.SavedState{"
346                     + Integer.toHexString(System.identityHashCode(this))
347                     + " checked=" + checked
348                     + " visible=" + visible + "}";
349         }
350 
351         public static final Parcelable.Creator<SavedState> CREATOR
352                 = new Parcelable.Creator<SavedState>() {
353             public SavedState createFromParcel(Parcel in) {
354                 return new SavedState(in);
355             }
356 
357             public SavedState[] newArray(int size) {
358                 return new SavedState[size];
359             }
360         };
361     }
362 
363     @Override
onSaveInstanceState()364     public Parcelable onSaveInstanceState() {
365         Parcelable superState = super.onSaveInstanceState();
366 
367         SavedState ss = new SavedState(superState);
368         ss.checked = mSwitch.isChecked();
369         ss.visible = isShowing();
370         return ss;
371     }
372 
373     @Override
onRestoreInstanceState(Parcelable state)374     public void onRestoreInstanceState(Parcelable state) {
375         SavedState ss = (SavedState) state;
376 
377         super.onRestoreInstanceState(ss.getSuperState());
378 
379         mSwitch.setCheckedInternal(ss.checked);
380         setTextViewLabelAndBackground(ss.checked);
381         setVisibility(ss.visible ? View.VISIBLE : View.GONE);
382         mSwitch.setOnCheckedChangeListener(ss.visible ? this : null);
383 
384         requestLayout();
385     }
386 }
387