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.content.Context; 22 import android.content.res.TypedArray; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 import android.text.SpannableStringBuilder; 26 import android.text.TextUtils; 27 import android.text.style.TextAppearanceSpan; 28 import android.util.AttributeSet; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.accessibility.AccessibilityEvent; 33 import android.view.accessibility.AccessibilityNodeInfo; 34 import android.widget.CompoundButton; 35 import android.widget.LinearLayout; 36 import android.widget.Switch; 37 import android.widget.TextView; 38 39 import com.android.settings.R; 40 import com.android.settings.core.instrumentation.MetricsFeatureProvider; 41 import com.android.settings.overlay.FeatureFactory; 42 import com.android.settingslib.RestrictedLockUtils; 43 44 import java.util.ArrayList; 45 46 public 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