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