1 /* 2 * Copyright (C) 2019 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.car.developeroptions.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.Rect; 25 import android.graphics.drawable.Drawable; 26 import android.os.Parcel; 27 import android.os.Parcelable; 28 import android.text.SpannableStringBuilder; 29 import android.text.TextUtils; 30 import android.text.style.TextAppearanceSpan; 31 import android.util.AttributeSet; 32 import android.view.LayoutInflater; 33 import android.view.TouchDelegate; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.widget.CompoundButton; 37 import android.widget.ImageView; 38 import android.widget.LinearLayout; 39 import android.widget.Switch; 40 import android.widget.TextView; 41 42 import androidx.annotation.ColorInt; 43 import androidx.annotation.StringRes; 44 import androidx.annotation.VisibleForTesting; 45 46 import com.android.car.developeroptions.R; 47 import com.android.car.developeroptions.overlay.FeatureFactory; 48 import com.android.settingslib.RestrictedLockUtils; 49 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 50 51 import java.util.ArrayList; 52 import java.util.List; 53 54 public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedChangeListener { 55 56 public interface OnSwitchChangeListener { 57 /** 58 * Called when the checked state of the Switch has changed. 59 * 60 * @param switchView The Switch view whose state has changed. 61 * @param isChecked The new checked state of switchView. 62 */ onSwitchChanged(Switch switchView, boolean isChecked)63 void onSwitchChanged(Switch switchView, boolean isChecked); 64 } 65 66 private static final int[] XML_ATTRIBUTES = { 67 R.attr.switchBarMarginStart, 68 R.attr.switchBarMarginEnd, 69 R.attr.switchBarBackgroundColor, 70 R.attr.switchBarBackgroundActivatedColor, 71 R.attr.switchBarRestrictionIcon}; 72 73 private final List<OnSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>(); 74 private final MetricsFeatureProvider mMetricsFeatureProvider; 75 private final TextAppearanceSpan mSummarySpan; 76 77 private ToggleSwitch mSwitch; 78 private ImageView mRestrictedIcon; 79 private TextView mTextView; 80 private String mLabel; 81 private String mSummary; 82 @ColorInt 83 private int mBackgroundColor; 84 @ColorInt 85 private int mBackgroundActivatedColor; 86 @StringRes 87 private int mOnTextId; 88 @StringRes 89 private int mOffTextId; 90 91 private boolean mLoggingIntialized; 92 private boolean mDisabledByAdmin; 93 private EnforcedAdmin mEnforcedAdmin = null; 94 private String mMetricsTag; 95 96 SwitchBar(Context context)97 public SwitchBar(Context context) { 98 this(context, null); 99 } 100 SwitchBar(Context context, AttributeSet attrs)101 public SwitchBar(Context context, AttributeSet attrs) { 102 this(context, attrs, 0); 103 } 104 SwitchBar(Context context, AttributeSet attrs, int defStyleAttr)105 public SwitchBar(Context context, AttributeSet attrs, int defStyleAttr) { 106 this(context, attrs, defStyleAttr, 0); 107 } 108 SwitchBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)109 public SwitchBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 110 super(context, attrs, defStyleAttr, defStyleRes); 111 112 LayoutInflater.from(context).inflate(R.layout.switch_bar, this); 113 114 final TypedArray a = context.obtainStyledAttributes(attrs, XML_ATTRIBUTES); 115 final int switchBarMarginStart = (int) a.getDimension(0, 0); 116 final int switchBarMarginEnd = (int) a.getDimension(1, 0); 117 mBackgroundColor = a.getColor(2, 0); 118 mBackgroundActivatedColor = a.getColor(3, 0); 119 final Drawable restrictedIconDrawable = a.getDrawable(4); 120 a.recycle(); 121 122 mTextView = findViewById(R.id.switch_text); 123 mSummarySpan = new TextAppearanceSpan(mContext, R.style.TextAppearance_Small_SwitchBar); 124 ViewGroup.MarginLayoutParams lp = (MarginLayoutParams) mTextView.getLayoutParams(); 125 lp.setMarginStart(switchBarMarginStart); 126 127 mSwitch = findViewById(R.id.switch_widget); 128 // Prevent onSaveInstanceState() to be called as we are managing the state of the Switch 129 // on our own 130 mSwitch.setSaveEnabled(false); 131 132 lp = (MarginLayoutParams) mSwitch.getLayoutParams(); 133 lp.setMarginEnd(switchBarMarginEnd); 134 setBackgroundColor(mBackgroundColor); 135 136 setSwitchBarText(R.string.switch_on_text, R.string.switch_off_text); 137 138 addOnSwitchChangeListener( 139 (switchView, isChecked) -> setTextViewLabelAndBackground(isChecked)); 140 141 mRestrictedIcon = findViewById(R.id.restricted_icon); 142 mRestrictedIcon.setImageDrawable(restrictedIconDrawable); 143 mRestrictedIcon.setOnClickListener(new View.OnClickListener() { 144 @Override 145 public void onClick(View v) { 146 if (mDisabledByAdmin) { 147 mMetricsFeatureProvider.action( 148 SettingsEnums.PAGE_UNKNOWN, 149 SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE, 150 SettingsEnums.PAGE_UNKNOWN, 151 mMetricsTag + "/switch_bar|restricted", 152 1); 153 154 RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, 155 mEnforcedAdmin); 156 } 157 } 158 }); 159 160 // Default is hide 161 setVisibility(View.GONE); 162 163 mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); 164 } 165 setMetricsTag(String tag)166 public void setMetricsTag(String tag) { 167 mMetricsTag = tag; 168 } 169 setTextViewLabelAndBackground(boolean isChecked)170 public void setTextViewLabelAndBackground(boolean isChecked) { 171 mLabel = getResources().getString(isChecked ? mOnTextId : mOffTextId); 172 setBackgroundColor(isChecked ? mBackgroundActivatedColor : mBackgroundColor); 173 updateText(); 174 } 175 setSwitchBarText(int onText, int offText)176 public void setSwitchBarText(int onText, int offText) { 177 mOnTextId = onText; 178 mOffTextId = offText; 179 setTextViewLabelAndBackground(isChecked()); 180 } 181 setSummary(String summary)182 public void setSummary(String summary) { 183 mSummary = summary; 184 updateText(); 185 } 186 updateText()187 private void updateText() { 188 if (TextUtils.isEmpty(mSummary)) { 189 mTextView.setText(mLabel); 190 return; 191 } 192 final SpannableStringBuilder ssb = new SpannableStringBuilder(mLabel).append('\n'); 193 final int start = ssb.length(); 194 ssb.append(mSummary); 195 ssb.setSpan(mSummarySpan, start, ssb.length(), 0); 196 mTextView.setText(ssb); 197 } 198 setChecked(boolean checked)199 public void setChecked(boolean checked) { 200 setTextViewLabelAndBackground(checked); 201 mSwitch.setChecked(checked); 202 } 203 setCheckedInternal(boolean checked)204 public void setCheckedInternal(boolean checked) { 205 setTextViewLabelAndBackground(checked); 206 mSwitch.setCheckedInternal(checked); 207 } 208 isChecked()209 public boolean isChecked() { 210 return mSwitch.isChecked(); 211 } 212 setEnabled(boolean enabled)213 public void setEnabled(boolean enabled) { 214 if (enabled && mDisabledByAdmin) { 215 setDisabledByAdmin(null); 216 return; 217 } 218 super.setEnabled(enabled); 219 mTextView.setEnabled(enabled); 220 mSwitch.setEnabled(enabled); 221 } 222 223 @VisibleForTesting getDelegatingView()224 View getDelegatingView() { 225 return mDisabledByAdmin ? mRestrictedIcon : mSwitch; 226 } 227 228 /** 229 * If admin is not null, disables the text and switch but keeps the view clickable. 230 * Otherwise, calls setEnabled which will enables the entire view including 231 * the text and switch. 232 */ setDisabledByAdmin(EnforcedAdmin admin)233 public void setDisabledByAdmin(EnforcedAdmin admin) { 234 mEnforcedAdmin = admin; 235 if (admin != null) { 236 super.setEnabled(true); 237 mDisabledByAdmin = true; 238 mTextView.setEnabled(false); 239 mSwitch.setEnabled(false); 240 mSwitch.setVisibility(View.GONE); 241 mRestrictedIcon.setVisibility(View.VISIBLE); 242 } else { 243 mDisabledByAdmin = false; 244 mSwitch.setVisibility(View.VISIBLE); 245 mRestrictedIcon.setVisibility(View.GONE); 246 setEnabled(true); 247 } 248 setTouchDelegate(new TouchDelegate(new Rect(0, 0, getWidth(), getHeight()), 249 getDelegatingView())); 250 } 251 getSwitch()252 public final ToggleSwitch getSwitch() { 253 return mSwitch; 254 } 255 show()256 public void show() { 257 if (!isShowing()) { 258 setVisibility(View.VISIBLE); 259 mSwitch.setOnCheckedChangeListener(this); 260 // Make the entire bar work as a switch 261 post(() -> setTouchDelegate( 262 new TouchDelegate(new Rect(0, 0, getWidth(), getHeight()), 263 getDelegatingView()))); 264 } 265 } 266 hide()267 public void hide() { 268 if (isShowing()) { 269 setVisibility(View.GONE); 270 mSwitch.setOnCheckedChangeListener(null); 271 } 272 } 273 274 @Override onSizeChanged(int w, int h, int oldw, int oldh)275 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 276 if ((w > 0) && (h > 0)) { 277 setTouchDelegate(new TouchDelegate(new Rect(0, 0, w, h), 278 getDelegatingView())); 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