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