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