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