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