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