1 /* 2 * Copyright (C) 2020 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.systemui.car.systembar; 18 19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; 20 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 21 22 import static com.android.systemui.car.users.CarSystemUIUserUtil.getCurrentUserHandle; 23 24 import android.app.ActivityManager; 25 import android.app.ActivityOptions; 26 import android.app.ActivityTaskManager; 27 import android.app.role.RoleManager; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.res.Resources; 31 import android.content.res.TypedArray; 32 import android.graphics.drawable.Drawable; 33 import android.os.Build; 34 import android.os.RemoteException; 35 import android.util.AttributeSet; 36 import android.util.Log; 37 import android.view.Display; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.widget.ImageView; 41 import android.widget.LinearLayout; 42 43 import androidx.annotation.Nullable; 44 45 import com.android.internal.annotations.VisibleForTesting; 46 import com.android.systemui.R; 47 import com.android.systemui.car.systembar.element.CarSystemBarElement; 48 import com.android.systemui.car.systembar.element.CarSystemBarElementFlags; 49 import com.android.systemui.car.systembar.element.CarSystemBarElementResolver; 50 import com.android.systemui.car.window.OverlayViewController; 51 import com.android.systemui.car.wm.scalableui.EventDispatcher; 52 import com.android.systemui.settings.UserTracker; 53 import com.android.systemui.statusbar.AlphaOptimizedImageView; 54 55 import java.net.URISyntaxException; 56 57 /** 58 * CarSystemBarButton is an image button that allows for a bit more configuration at the 59 * xml file level. This allows for more control via overlays instead of having to update 60 * code. 61 */ 62 public class CarSystemBarButton extends LinearLayout implements 63 OverlayViewController.OverlayViewStateListener, CarSystemBarElement { 64 65 private static final String TAG = "CarSystemBarButton"; 66 private static final String BUTTON_FILTER_DELIMITER = ";"; 67 private static final String EXTRA_BUTTON_CATEGORIES = "categories"; 68 private static final String EXTRA_BUTTON_PACKAGES = "packages"; 69 private static final String EXTRA_DIALOG_CLOSE_REASON = "reason"; 70 private static final String DIALOG_CLOSE_REASON_CAR_SYSTEMBAR_BUTTON = "carsystembarbutton"; 71 private static final float DEFAULT_SELECTED_ALPHA = 1f; 72 private static final float DEFAULT_UNSELECTED_ALPHA = 0.75f; 73 private static final float DISABLED_ALPHA = 0.25f; 74 75 private final Context mContext; 76 private final ActivityManager mActivityManager; 77 private final Class<?> mElementControllerClassAttr; 78 private final int mSystemBarDisableFlags; 79 private final int mSystemBarDisable2Flags; 80 private final boolean mDisableForLockTaskModeLocked; 81 @Nullable 82 private UserTracker mUserTracker; 83 @Nullable 84 private EventDispatcher mEventDispatcher; 85 private ViewGroup mIconContainer; 86 private AlphaOptimizedImageView mIcon; 87 private AlphaOptimizedImageView mMoreIcon; 88 private ImageView mUnseenIcon; 89 /** The intent to be used while the button is selected. */ 90 private Intent mSelectedIntent; 91 /** The intent to be used while the button is unselected. */ 92 private Intent mUnselectedIntent; 93 private String mLongIntent; 94 /** The event to be used while the button is selected. */ 95 private String mSelectedEvent; 96 /** The event to be used while the button is unselected. */ 97 private String mUnselectedEvent; 98 private boolean mBroadcastIntent; 99 /** Whether to clear the backstack (i.e. put the home activity directly behind) when pressed */ 100 private boolean mClearBackStack; 101 private boolean mHasUnseen = false; 102 private boolean mSelected = false; 103 private boolean mDisabled = false; 104 private float mSelectedAlpha; 105 private float mUnselectedAlpha; 106 private int mSelectedIconResourceId; 107 private int mIconResourceId; 108 private Drawable mAppIcon; 109 private boolean mIsDefaultAppIconForRoleEnabled; 110 private boolean mToggleSelectedState; 111 private String[] mPanelNames; 112 private String[] mComponentNames; 113 /** App categories that are to be used with this widget */ 114 private String[] mButtonCategories; 115 /** App packages that are allowed to be used with this widget */ 116 private String[] mButtonPackages; 117 /** Whether to display more icon beneath the primary icon when the button is selected */ 118 private boolean mShowMoreWhenSelected = false; 119 /** Whether to highlight the button if the active application is associated with it */ 120 private boolean mHighlightWhenSelected = false; 121 private Runnable mOnClickWhileDisabledRunnable; 122 CarSystemBarButton(Context context, AttributeSet attrs)123 public CarSystemBarButton(Context context, AttributeSet attrs) { 124 super(context, attrs); 125 126 // Do not move this init call. All logic should be carried out after this. 127 init(); 128 129 mContext = context; 130 mActivityManager = mContext.getSystemService(ActivityManager.class); 131 View.inflate(mContext, R.layout.car_system_bar_button, /* root= */ this); 132 // CarSystemBarButton attrs 133 TypedArray typedArray = context.obtainStyledAttributes(attrs, 134 R.styleable.CarSystemBarButton); 135 136 mElementControllerClassAttr = 137 CarSystemBarElementResolver.getElementControllerClassFromAttributes(context, attrs); 138 mSystemBarDisableFlags = 139 CarSystemBarElementFlags.getStatusBarManagerDisableFlagsFromAttributes(context, 140 attrs); 141 mSystemBarDisable2Flags = 142 CarSystemBarElementFlags.getStatusBarManagerDisable2FlagsFromAttributes(context, 143 attrs); 144 mDisableForLockTaskModeLocked = 145 CarSystemBarElementFlags.getDisableForLockTaskModeLockedFromAttributes(context, 146 attrs); 147 148 setUpCategories(typedArray); 149 setUpIntents(typedArray); 150 setUpIcons(typedArray); 151 typedArray.recycle(); 152 } 153 154 /** 155 * Initializer for child classes. 156 */ init()157 protected void init() { 158 } 159 160 /** 161 * @param selected true if should indicate if this is a selected state, false otherwise 162 */ setSelected(boolean selected)163 public void setSelected(boolean selected) { 164 if (mDisabled) { 165 // if the button is disabled, mSelected should not be modified and the button 166 // should be unselectable 167 return; 168 } 169 super.setSelected(selected); 170 mSelected = selected; 171 172 refreshIconAlpha(mIcon); 173 174 if (mShowMoreWhenSelected && mMoreIcon != null) { 175 mMoreIcon.setVisibility(selected ? VISIBLE : GONE); 176 } 177 updateImage(mIcon); 178 } 179 180 /** Gets whether the icon is in a selected state. */ getSelected()181 public boolean getSelected() { 182 return mSelected; 183 } 184 185 /** 186 * @param hasUnseen true if should indicate if this is a Unseen state, false otherwise. 187 */ setUnseen(boolean hasUnseen)188 public void setUnseen(boolean hasUnseen) { 189 mHasUnseen = hasUnseen; 190 updateImage(mIcon); 191 } 192 193 /** 194 * @param disabled true if icon should be isabled, false otherwise. 195 * @param runnable to run when button is clicked while disabled. 196 */ setDisabled(boolean disabled, @Nullable Runnable runnable)197 public void setDisabled(boolean disabled, @Nullable Runnable runnable) { 198 mDisabled = disabled; 199 mOnClickWhileDisabledRunnable = runnable; 200 refreshIconAlpha(mIcon); 201 updateImage(mIcon); 202 } 203 204 /** Gets whether the icon is disabled */ getDisabled()205 public boolean getDisabled() { 206 return mDisabled; 207 } 208 209 /** Runs the Runnable when the button is clicked while disabled */ runOnClickWhileDisabled()210 public void runOnClickWhileDisabled() { 211 if (mOnClickWhileDisabledRunnable == null) { 212 return; 213 } 214 mOnClickWhileDisabledRunnable.run(); 215 } 216 217 /** 218 * Sets the current icon of the default application associated with this button. 219 */ setAppIcon(Drawable appIcon)220 public void setAppIcon(Drawable appIcon) { 221 mAppIcon = appIcon; 222 updateImage(mIcon); 223 } 224 225 /** Gets the icon of the app currently associated to the role of this button. */ 226 @VisibleForTesting getAppIcon()227 protected Drawable getAppIcon() { 228 return mAppIcon; 229 } 230 231 /** Gets whether the icon is in an unseen state. */ getUnseen()232 public boolean getUnseen() { 233 return mHasUnseen; 234 } 235 236 /** 237 * @return The app categories the component represents 238 */ getCategories()239 public String[] getCategories() { 240 if (mButtonCategories == null) { 241 return new String[0]; 242 } 243 return mButtonCategories; 244 } 245 246 /** 247 * @return The valid packages that should be considered. 248 */ getPackages()249 public String[] getPackages() { 250 if (mButtonPackages == null) { 251 return new String[0]; 252 } 253 return mButtonPackages; 254 } 255 256 /** 257 * @return The list of panel names that should be used for selection 258 */ getPanelNames()259 public String[] getPanelNames() { 260 if (mPanelNames == null) { 261 return new String[0]; 262 } 263 return mPanelNames; 264 } 265 266 /** 267 * @return The list of component names. 268 */ getComponentName()269 public String[] getComponentName() { 270 if (mComponentNames == null) { 271 return new String[0]; 272 } 273 return mComponentNames; 274 } 275 276 @Override onVisibilityChanged(boolean isVisible)277 public void onVisibilityChanged(boolean isVisible) { 278 setSelected(isVisible); 279 } 280 281 /** 282 * Subclasses should override this method to return the {@link RoleManager} role associated 283 * with this button. 284 */ getRoleName()285 protected String getRoleName() { 286 return null; 287 } 288 289 /** 290 * @return true if this button should show the icon of the default application for the 291 * role returned by {@link #getRoleName()}. 292 */ isDefaultAppIconForRoleEnabled()293 protected boolean isDefaultAppIconForRoleEnabled() { 294 return mIsDefaultAppIconForRoleEnabled; 295 } 296 297 /** 298 * @return The id of the display the button is on or Display.INVALID_DISPLAY if it's not yet on 299 * a display. 300 */ getDisplayId()301 protected int getDisplayId() { 302 Display display = getDisplay(); 303 if (display == null) { 304 return Display.INVALID_DISPLAY; 305 } 306 return display.getDisplayId(); 307 } 308 hasSelectionState()309 protected boolean hasSelectionState() { 310 return mHighlightWhenSelected || mShowMoreWhenSelected; 311 } 312 getSelectedAlpha()313 protected float getSelectedAlpha() { 314 return mSelectedAlpha; 315 } 316 317 @VisibleForTesting getUnselectedAlpha()318 protected float getUnselectedAlpha() { 319 return mUnselectedAlpha; 320 } 321 322 @VisibleForTesting getDisabledAlpha()323 protected float getDisabledAlpha() { 324 return DISABLED_ALPHA; 325 } 326 327 @VisibleForTesting getIconAlpha()328 protected float getIconAlpha() { return mIcon.getAlpha(); } 329 getIntent()330 protected Intent getIntent() { 331 if (mSelected) { 332 return mSelectedIntent; 333 } else { 334 return mUnselectedIntent; 335 } 336 } 337 getEvent()338 protected String getEvent() { 339 if (mSelected) { 340 return mSelectedEvent; 341 } else { 342 return mUnselectedEvent; 343 } 344 } 345 346 /** 347 * Sets up package, category and component names for the buttons. 348 * These properties can be used to control the selected state of buttons as a group. 349 */ setUpCategories(TypedArray typedArray)350 protected void setUpCategories(TypedArray typedArray) { 351 String categoryString = typedArray.getString(R.styleable.CarSystemBarButton_categories); 352 String packageString = typedArray.getString(R.styleable.CarSystemBarButton_packages); 353 String componentNameString = 354 typedArray.getString(R.styleable.CarSystemBarButton_componentNames); 355 String panelNamesString = 356 typedArray.getString(R.styleable.CarSystemBarButton_panelNames); 357 if (packageString != null) { 358 mButtonPackages = packageString.split(BUTTON_FILTER_DELIMITER); 359 } 360 if (categoryString != null) { 361 mButtonCategories = categoryString.split(BUTTON_FILTER_DELIMITER); 362 } 363 if (componentNameString != null) { 364 mComponentNames = componentNameString.split(BUTTON_FILTER_DELIMITER); 365 } 366 if (panelNamesString != null) { 367 mPanelNames = panelNamesString.split(BUTTON_FILTER_DELIMITER); 368 } 369 } 370 371 /** 372 * Sets up intents for click, long touch, and broadcast. 373 */ setUpIntents(TypedArray typedArray)374 protected void setUpIntents(TypedArray typedArray) { 375 String intentString = typedArray.getString(R.styleable.CarSystemBarButton_intent); 376 String selectedIntentString = 377 typedArray.getString(R.styleable.CarSystemBarButton_selectedIntent); 378 selectedIntentString = selectedIntentString != null ? selectedIntentString : intentString; 379 String unselectedIntentString = 380 typedArray.getString(R.styleable.CarSystemBarButton_unselectedIntent); 381 unselectedIntentString = 382 unselectedIntentString != null ? unselectedIntentString : intentString; 383 mLongIntent = typedArray.getString(R.styleable.CarSystemBarButton_longIntent); 384 mBroadcastIntent = typedArray.getBoolean(R.styleable.CarSystemBarButton_broadcast, false); 385 386 String eventString = typedArray.getString(R.styleable.CarSystemBarButton_event); 387 String selectedEventString = 388 typedArray.getString(R.styleable.CarSystemBarButton_selectedEvent); 389 mSelectedEvent = selectedEventString != null ? selectedEventString : eventString; 390 String unselectedEventString = 391 typedArray.getString(R.styleable.CarSystemBarButton_unselectedEvent); 392 mUnselectedEvent = 393 unselectedEventString != null ? unselectedEventString : eventString; 394 395 mClearBackStack = typedArray.getBoolean(R.styleable.CarSystemBarButton_clearBackStack, 396 false); 397 398 try { 399 if (selectedIntentString != null) { 400 mSelectedIntent = Intent.parseUri(selectedIntentString, Intent.URI_INTENT_SCHEME); 401 if (mButtonPackages != null) { 402 mSelectedIntent.putExtra(EXTRA_BUTTON_PACKAGES, mButtonPackages); 403 } 404 if (mButtonCategories != null) { 405 mSelectedIntent.putExtra(EXTRA_BUTTON_CATEGORIES, mButtonCategories); 406 } 407 } 408 409 if (unselectedIntentString != null) { 410 mUnselectedIntent = 411 Intent.parseUri(unselectedIntentString, Intent.URI_INTENT_SCHEME); 412 if (mButtonPackages != null) { 413 mUnselectedIntent.putExtra(EXTRA_BUTTON_PACKAGES, mButtonPackages); 414 } 415 if (mButtonCategories != null) { 416 mUnselectedIntent.putExtra(EXTRA_BUTTON_CATEGORIES, mButtonCategories); 417 } 418 } 419 420 setOnClickListener(getButtonClickListener()); 421 422 } catch (URISyntaxException e) { 423 throw new RuntimeException("Failed to attach intent", e); 424 } 425 426 try { 427 if (mLongIntent != null && !mLongIntent.isEmpty() 428 && (Build.IS_ENG || Build.IS_USERDEBUG)) { 429 final Intent intent = Intent.parseUri(mLongIntent, Intent.URI_INTENT_SCHEME); 430 setOnLongClickListener(getButtonLongClickListener(intent)); 431 } 432 } catch (URISyntaxException e) { 433 throw new RuntimeException("Failed to attach long press intent", e); 434 } 435 } 436 437 /** Defines the behavior of a button click. */ getButtonClickListener()438 protected OnClickListener getButtonClickListener() { 439 return v -> { 440 if (mDisabled) { 441 runOnClickWhileDisabled(); 442 return; 443 } 444 boolean startState = mSelected; 445 Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); 446 intent.putExtra(EXTRA_DIALOG_CLOSE_REASON, DIALOG_CLOSE_REASON_CAR_SYSTEMBAR_BUTTON); 447 mContext.sendBroadcastAsUser(intent, getCurrentUserHandle(mContext, mUserTracker)); 448 449 if (getEvent() != null && mEventDispatcher != null) { 450 mEventDispatcher.executeTransaction(getEvent()); 451 } 452 453 if (getIntent() == null) { 454 return; 455 } 456 457 boolean intentLaunched = false; 458 try { 459 if (mBroadcastIntent) { 460 mContext.sendBroadcastAsUser(getIntent(), 461 getCurrentUserHandle(mContext, mUserTracker)); 462 return; 463 } 464 ActivityOptions options = ActivityOptions.makeBasic(); 465 options.setLaunchDisplayId(mContext.getDisplayId()); 466 mContext.startActivityAsUser(getIntent(), options.toBundle(), 467 getCurrentUserHandle(mContext, mUserTracker)); 468 intentLaunched = true; 469 } catch (Exception e) { 470 Log.e(TAG, "Failed to launch intent", e); 471 } 472 473 if (intentLaunched && mClearBackStack) { 474 try { 475 ActivityTaskManager.RootTaskInfo rootTaskInfo = 476 ActivityTaskManager.getService().getRootTaskInfoOnDisplay( 477 WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_UNDEFINED, 478 mContext.getDisplayId()); 479 if (rootTaskInfo != null) { 480 mActivityManager.moveTaskToFront(rootTaskInfo.taskId, 481 ActivityManager.MOVE_TASK_WITH_HOME); 482 } 483 } catch (RemoteException e) { 484 Log.e(TAG, "Failed getting root task info", e); 485 } 486 } 487 488 if (mToggleSelectedState && (startState == mSelected)) { 489 setSelected(!mSelected); 490 } 491 }; 492 } 493 494 /** Defines the behavior of a long click. */ 495 protected OnLongClickListener getButtonLongClickListener(Intent toSend) { 496 return v -> { 497 Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); 498 intent.putExtra(EXTRA_DIALOG_CLOSE_REASON, DIALOG_CLOSE_REASON_CAR_SYSTEMBAR_BUTTON); 499 mContext.sendBroadcastAsUser(intent, getCurrentUserHandle(mContext, mUserTracker)); 500 try { 501 ActivityOptions options = ActivityOptions.makeBasic(); 502 options.setLaunchDisplayId(mContext.getDisplayId()); 503 mContext.startActivityAsUser(toSend, options.toBundle(), 504 getCurrentUserHandle(mContext, mUserTracker)); 505 } catch (Exception e) { 506 Log.e(TAG, "Failed to launch intent", e); 507 } 508 // consume event either way 509 return true; 510 }; 511 } 512 513 public void setUserTracker(UserTracker userTracker) { 514 mUserTracker = userTracker; 515 } 516 517 /** 518 * Set the EventDispatcher instance. 519 */ 520 public void setEventDispatcher(EventDispatcher eventDispatcher) { 521 mEventDispatcher = eventDispatcher; 522 } 523 524 /** 525 * Initializes view-related aspects of the button. 526 */ 527 private void setUpIcons(TypedArray typedArray) { 528 mSelectedAlpha = typedArray.getFloat( 529 R.styleable.CarSystemBarButton_selectedAlpha, DEFAULT_SELECTED_ALPHA); 530 mUnselectedAlpha = typedArray.getFloat( 531 R.styleable.CarSystemBarButton_unselectedAlpha, DEFAULT_UNSELECTED_ALPHA); 532 mHighlightWhenSelected = typedArray.getBoolean( 533 R.styleable.CarSystemBarButton_highlightWhenSelected, 534 mHighlightWhenSelected); 535 mShowMoreWhenSelected = typedArray.getBoolean( 536 R.styleable.CarSystemBarButton_showMoreWhenSelected, 537 mShowMoreWhenSelected); 538 539 mIconResourceId = typedArray.getResourceId( 540 R.styleable.CarSystemBarButton_icon, Resources.ID_NULL); 541 mSelectedIconResourceId = typedArray.getResourceId( 542 R.styleable.CarSystemBarButton_selectedIcon, mIconResourceId); 543 mIsDefaultAppIconForRoleEnabled = typedArray.getBoolean( 544 R.styleable.CarSystemBarButton_useDefaultAppIconForRole, false); 545 mToggleSelectedState = typedArray.getBoolean( 546 R.styleable.CarSystemBarButton_toggleSelected, false); 547 mIconContainer = findViewById(R.id.car_nav_button_icon); 548 mIcon = findViewById(R.id.car_nav_button_icon_image); 549 mMoreIcon = findViewById(R.id.car_nav_button_more_icon); 550 mUnseenIcon = findViewById(R.id.car_nav_button_unseen_icon); 551 refreshIconAlpha(mIcon); 552 updateImage(mIcon); 553 } 554 555 private void updateIconContainerVisibility() { 556 boolean visible = mIcon.getVisibility() == VISIBLE 557 || mUnseenIcon.getVisibility() == VISIBLE 558 || mMoreIcon.getVisibility() == VISIBLE; 559 mIconContainer.setVisibility(visible ? VISIBLE : GONE); 560 } 561 562 protected void updateImage(AlphaOptimizedImageView icon) { 563 if (mIsDefaultAppIconForRoleEnabled && mAppIcon != null) { 564 icon.setImageDrawable(mAppIcon); 565 icon.setVisibility(VISIBLE); 566 } else { 567 int resId = mSelected ? mSelectedIconResourceId : mIconResourceId; 568 icon.setImageResource(resId); 569 icon.setVisibility(resId != Resources.ID_NULL ? VISIBLE : GONE); 570 } 571 mUnseenIcon.setVisibility(mHasUnseen ? VISIBLE : GONE); 572 updateIconContainerVisibility(); 573 } 574 575 protected void refreshIconAlpha(AlphaOptimizedImageView icon) { 576 if (mDisabled) { 577 icon.setAlpha(DISABLED_ALPHA); 578 } else { 579 icon.setAlpha(mHighlightWhenSelected && mSelected ? mSelectedAlpha : mUnselectedAlpha); 580 } 581 } 582 583 @Nullable 584 protected UserTracker getUserTracker() { 585 return mUserTracker; 586 } 587 588 @Override 589 public Class<?> getElementControllerClass() { 590 if (mElementControllerClassAttr != null) { 591 return mElementControllerClassAttr; 592 } 593 return CarSystemBarButtonController.class; 594 } 595 596 @Override 597 public int getSystemBarDisableFlags() { 598 return mSystemBarDisableFlags; 599 } 600 601 @Override 602 public int getSystemBarDisable2Flags() { 603 return mSystemBarDisable2Flags; 604 } 605 606 @Override 607 public boolean disableForLockTaskModeLocked() { 608 return mDisableForLockTaskModeLocked; 609 } 610 } 611