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
17package com.android.settings.widget;
18
19import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
20
21import android.content.Context;
22import android.content.res.TypedArray;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.text.SpannableStringBuilder;
26import android.text.TextUtils;
27import android.text.style.TextAppearanceSpan;
28import android.util.AttributeSet;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.accessibility.AccessibilityEvent;
33import android.view.accessibility.AccessibilityNodeInfo;
34import android.widget.CompoundButton;
35import android.widget.LinearLayout;
36import android.widget.Switch;
37import android.widget.TextView;
38
39import com.android.settings.R;
40import com.android.settings.core.instrumentation.MetricsFeatureProvider;
41import com.android.settings.overlay.FeatureFactory;
42import com.android.settingslib.RestrictedLockUtils;
43
44import java.util.ArrayList;
45
46public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedChangeListener,
47        View.OnClickListener {
48
49    public interface OnSwitchChangeListener {
50        /**
51         * Called when the checked state of the Switch has changed.
52         *
53         * @param switchView The Switch view whose state has changed.
54         * @param isChecked  The new checked state of switchView.
55         */
56        void onSwitchChanged(Switch switchView, boolean isChecked);
57    }
58
59    private MetricsFeatureProvider mMetricsFeatureProvider;
60    private final TextAppearanceSpan mSummarySpan;
61
62    private ToggleSwitch mSwitch;
63    private View mRestrictedIcon;
64    private TextView mTextView;
65    private String mLabel;
66    private String mSummary;
67
68    private boolean mLoggingIntialized;
69    private boolean mDisabledByAdmin;
70    private EnforcedAdmin mEnforcedAdmin = null;
71
72    private String mMetricsTag;
73
74    private final ArrayList<OnSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>();
75
76    private static int[] XML_ATTRIBUTES = {
77            R.attr.switchBarMarginStart, R.attr.switchBarMarginEnd,
78            R.attr.switchBarBackgroundColor};
79
80    public SwitchBar(Context context) {
81        this(context, null);
82    }
83
84    public SwitchBar(Context context, AttributeSet attrs) {
85        this(context, attrs, 0);
86    }
87
88    public SwitchBar(Context context, AttributeSet attrs, int defStyleAttr) {
89        this(context, attrs, defStyleAttr, 0);
90    }
91
92    public SwitchBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
93        super(context, attrs, defStyleAttr, defStyleRes);
94
95        LayoutInflater.from(context).inflate(R.layout.switch_bar, this);
96
97        final TypedArray a = context.obtainStyledAttributes(attrs, XML_ATTRIBUTES);
98        int switchBarMarginStart = (int) a.getDimension(0, 0);
99        int switchBarMarginEnd = (int) a.getDimension(1, 0);
100        int switchBarBackgroundColor = (int) a.getColor(2, 0);
101        a.recycle();
102
103        mTextView = (TextView) findViewById(R.id.switch_text);
104        mTextView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
105        mLabel = getResources().getString(R.string.switch_off_text);
106        mSummarySpan = new TextAppearanceSpan(mContext, R.style.TextAppearance_Small_SwitchBar);
107        updateText();
108        ViewGroup.MarginLayoutParams lp = (MarginLayoutParams) mTextView.getLayoutParams();
109        lp.setMarginStart(switchBarMarginStart);
110
111        mSwitch = (ToggleSwitch) findViewById(R.id.switch_widget);
112        // Prevent onSaveInstanceState() to be called as we are managing the state of the Switch
113        // on our own
114        mSwitch.setSaveEnabled(false);
115        mSwitch.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
116        lp = (MarginLayoutParams) mSwitch.getLayoutParams();
117        lp.setMarginEnd(switchBarMarginEnd);
118        setBackgroundColor(switchBarBackgroundColor);
119
120        addOnSwitchChangeListener(new OnSwitchChangeListener() {
121            @Override
122            public void onSwitchChanged(Switch switchView, boolean isChecked) {
123                setTextViewLabel(isChecked);
124            }
125        });
126
127        mRestrictedIcon = findViewById(R.id.restricted_icon);
128
129        setOnClickListener(this);
130
131        // Default is hide
132        setVisibility(View.GONE);
133
134        mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
135    }
136
137    public void setMetricsTag(String tag) {
138        mMetricsTag = tag;
139    }
140
141    public void setTextViewLabel(boolean isChecked) {
142        mLabel = getResources()
143                .getString(isChecked ? R.string.switch_on_text : R.string.switch_off_text);
144        updateText();
145    }
146
147    public void setSummary(String summary) {
148        mSummary = summary;
149        updateText();
150    }
151
152    private void updateText() {
153        if (TextUtils.isEmpty(mSummary)) {
154            mTextView.setText(mLabel);
155            return;
156        }
157        final SpannableStringBuilder ssb = new SpannableStringBuilder(mLabel).append('\n');
158        final int start = ssb.length();
159        ssb.append(mSummary);
160        ssb.setSpan(mSummarySpan, start, ssb.length(), 0);
161        mTextView.setText(ssb);
162    }
163
164    public void setChecked(boolean checked) {
165        setTextViewLabel(checked);
166        mSwitch.setChecked(checked);
167    }
168
169    public void setCheckedInternal(boolean checked) {
170        setTextViewLabel(checked);
171        mSwitch.setCheckedInternal(checked);
172    }
173
174    public boolean isChecked() {
175        return mSwitch.isChecked();
176    }
177
178    public void setEnabled(boolean enabled) {
179        if (enabled && mDisabledByAdmin) {
180            setDisabledByAdmin(null);
181            return;
182        }
183        super.setEnabled(enabled);
184        mTextView.setEnabled(enabled);
185        mSwitch.setEnabled(enabled);
186    }
187
188    /**
189     * If admin is not null, disables the text and switch but keeps the view clickable.
190     * Otherwise, calls setEnabled which will enables the entire view including
191     * the text and switch.
192     */
193    public void setDisabledByAdmin(EnforcedAdmin admin) {
194        mEnforcedAdmin = admin;
195        if (admin != null) {
196            super.setEnabled(true);
197            mDisabledByAdmin = true;
198            mTextView.setEnabled(false);
199            mSwitch.setEnabled(false);
200            mSwitch.setVisibility(View.GONE);
201            mRestrictedIcon.setVisibility(View.VISIBLE);
202        } else {
203            mDisabledByAdmin = false;
204            mSwitch.setVisibility(View.VISIBLE);
205            mRestrictedIcon.setVisibility(View.GONE);
206            setEnabled(true);
207        }
208    }
209
210    public final ToggleSwitch getSwitch() {
211        return mSwitch;
212    }
213
214    public void show() {
215        if (!isShowing()) {
216            setVisibility(View.VISIBLE);
217            mSwitch.setOnCheckedChangeListener(this);
218        }
219    }
220
221    public void hide() {
222        if (isShowing()) {
223            setVisibility(View.GONE);
224            mSwitch.setOnCheckedChangeListener(null);
225        }
226    }
227
228    public boolean isShowing() {
229        return (getVisibility() == View.VISIBLE);
230    }
231
232    @Override
233    public void onClick(View v) {
234        if (mDisabledByAdmin) {
235            mMetricsFeatureProvider.count(mContext, mMetricsTag + "/switch_bar|restricted", 1);
236            RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mContext, mEnforcedAdmin);
237        } else {
238            final boolean isChecked = !mSwitch.isChecked();
239            setChecked(isChecked);
240        }
241    }
242
243    public void propagateChecked(boolean isChecked) {
244        final int count = mSwitchChangeListeners.size();
245        for (int n = 0; n < count; n++) {
246            mSwitchChangeListeners.get(n).onSwitchChanged(mSwitch, isChecked);
247        }
248    }
249
250    @Override
251    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
252        if (mLoggingIntialized) {
253            mMetricsFeatureProvider.count(mContext, mMetricsTag + "/switch_bar|" + isChecked, 1);
254        }
255        mLoggingIntialized = true;
256        propagateChecked(isChecked);
257    }
258
259    public void addOnSwitchChangeListener(OnSwitchChangeListener listener) {
260        if (mSwitchChangeListeners.contains(listener)) {
261            throw new IllegalStateException("Cannot add twice the same OnSwitchChangeListener");
262        }
263        mSwitchChangeListeners.add(listener);
264    }
265
266    public void removeOnSwitchChangeListener(OnSwitchChangeListener listener) {
267        if (!mSwitchChangeListeners.contains(listener)) {
268            throw new IllegalStateException("Cannot remove OnSwitchChangeListener");
269        }
270        mSwitchChangeListeners.remove(listener);
271    }
272
273    static class SavedState extends BaseSavedState {
274        boolean checked;
275        boolean visible;
276
277        SavedState(Parcelable superState) {
278            super(superState);
279        }
280
281        /**
282         * Constructor called from {@link #CREATOR}
283         */
284        private SavedState(Parcel in) {
285            super(in);
286            checked = (Boolean)in.readValue(null);
287            visible = (Boolean)in.readValue(null);
288        }
289
290        @Override
291        public void writeToParcel(Parcel out, int flags) {
292            super.writeToParcel(out, flags);
293            out.writeValue(checked);
294            out.writeValue(visible);
295        }
296
297        @Override
298        public String toString() {
299            return "SwitchBar.SavedState{"
300                    + Integer.toHexString(System.identityHashCode(this))
301                    + " checked=" + checked
302                    + " visible=" + visible + "}";
303        }
304
305        public static final Parcelable.Creator<SavedState> CREATOR
306                = new Parcelable.Creator<SavedState>() {
307            public SavedState createFromParcel(Parcel in) {
308                return new SavedState(in);
309            }
310
311            public SavedState[] newArray(int size) {
312                return new SavedState[size];
313            }
314        };
315    }
316
317    @Override
318    public Parcelable onSaveInstanceState() {
319        Parcelable superState = super.onSaveInstanceState();
320
321        SavedState ss = new SavedState(superState);
322        ss.checked = mSwitch.isChecked();
323        ss.visible = isShowing();
324        return ss;
325    }
326
327    @Override
328    public void onRestoreInstanceState(Parcelable state) {
329        SavedState ss = (SavedState) state;
330
331        super.onRestoreInstanceState(ss.getSuperState());
332
333        mSwitch.setCheckedInternal(ss.checked);
334        setTextViewLabel(ss.checked);
335        setVisibility(ss.visible ? View.VISIBLE : View.GONE);
336        mSwitch.setOnCheckedChangeListener(ss.visible ? this : null);
337
338        requestLayout();
339    }
340
341    @Override
342    public CharSequence getAccessibilityClassName() {
343        return Switch.class.getName();
344    }
345
346    @Override
347    public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
348        // Since the children are marked as not important for accessibility, re-dispatch all
349        // of their events as if they came from this view
350        event.setSource(this);
351        return true;
352    }
353
354    /** @hide */
355    @Override
356    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
357        super.onInitializeAccessibilityNodeInfoInternal(info);
358        info.setText(mTextView.getText());
359        info.setCheckable(true);
360        info.setChecked(mSwitch.isChecked());
361    }
362
363    /** @hide */
364    @Override
365    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
366        super.onInitializeAccessibilityEventInternal(event);
367        // Don't say "on on" or "off off" - rather, speak the state only once. We need to specify
368        // this explicitly as each of our children (the textview and the checkbox) contribute to
369        // the state once, giving us duplicate text by default.
370        event.setContentDescription(mTextView.getText());
371        event.setChecked(mSwitch.isChecked());
372    }
373}
374