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.TypedArray; 31 import android.graphics.drawable.Drawable; 32 import android.os.Build; 33 import android.os.RemoteException; 34 import android.util.AttributeSet; 35 import android.util.Log; 36 import android.view.Display; 37 import android.view.View; 38 import android.widget.ImageView; 39 import android.widget.LinearLayout; 40 41 import androidx.annotation.Nullable; 42 43 import com.android.internal.annotations.VisibleForTesting; 44 import com.android.systemui.R; 45 import com.android.systemui.car.window.OverlayViewController; 46 import com.android.systemui.settings.UserTracker; 47 import com.android.systemui.statusbar.AlphaOptimizedImageView; 48 49 import java.net.URISyntaxException; 50 51 /** 52 * CarSystemBarButton is an image button that allows for a bit more configuration at the 53 * xml file level. This allows for more control via overlays instead of having to update 54 * code. 55 */ 56 public class CarSystemBarButton extends LinearLayout implements 57 OverlayViewController.OverlayViewStateListener { 58 59 private static final String TAG = "CarSystemBarButton"; 60 private static final String BUTTON_FILTER_DELIMITER = ";"; 61 private static final String EXTRA_BUTTON_CATEGORIES = "categories"; 62 private static final String EXTRA_BUTTON_PACKAGES = "packages"; 63 private static final String EXTRA_DIALOG_CLOSE_REASON = "reason"; 64 private static final String DIALOG_CLOSE_REASON_CAR_SYSTEMBAR_BUTTON = "carsystembarbutton"; 65 private static final float DEFAULT_SELECTED_ALPHA = 1f; 66 private static final float DEFAULT_UNSELECTED_ALPHA = 0.75f; 67 private static final float DISABLED_ALPHA = 0.25f; 68 69 private final Context mContext; 70 private final ActivityManager mActivityManager; 71 @Nullable 72 private UserTracker mUserTracker; 73 private AlphaOptimizedImageView mIcon; 74 private AlphaOptimizedImageView mMoreIcon; 75 private ImageView mUnseenIcon; 76 private String mIntent; 77 private String mLongIntent; 78 private boolean mBroadcastIntent; 79 /** Whether to clear the backstack (i.e. put the home activity directly behind) when pressed */ 80 private boolean mClearBackStack; 81 private boolean mHasUnseen = false; 82 private boolean mSelected = false; 83 private boolean mDisabled = false; 84 private float mSelectedAlpha; 85 private float mUnselectedAlpha; 86 private int mSelectedIconResourceId; 87 private int mIconResourceId; 88 private Drawable mAppIcon; 89 private boolean mIsDefaultAppIconForRoleEnabled; 90 private boolean mToggleSelectedState; 91 private String[] mComponentNames; 92 /** App categories that are to be used with this widget */ 93 private String[] mButtonCategories; 94 /** App packages that are allowed to be used with this widget */ 95 private String[] mButtonPackages; 96 /** Whether to display more icon beneath the primary icon when the button is selected */ 97 private boolean mShowMoreWhenSelected = false; 98 /** Whether to highlight the button if the active application is associated with it */ 99 private boolean mHighlightWhenSelected = false; 100 private Runnable mOnClickWhileDisabledRunnable; 101 CarSystemBarButton(Context context, AttributeSet attrs)102 public CarSystemBarButton(Context context, AttributeSet attrs) { 103 super(context, attrs); 104 105 // Do not move this init call. All logic should be carried out after this. 106 init(); 107 108 mContext = context; 109 mActivityManager = mContext.getSystemService(ActivityManager.class); 110 View.inflate(mContext, R.layout.car_system_bar_button, /* root= */ this); 111 // CarSystemBarButton attrs 112 TypedArray typedArray = context.obtainStyledAttributes(attrs, 113 R.styleable.CarSystemBarButton); 114 115 setUpIntents(typedArray); 116 setUpIcons(typedArray); 117 typedArray.recycle(); 118 } 119 120 /** 121 * Initializer for child classes. 122 */ init()123 protected void init() { 124 } 125 126 /** 127 * @param selected true if should indicate if this is a selected state, false otherwise 128 */ setSelected(boolean selected)129 public void setSelected(boolean selected) { 130 if (mDisabled) { 131 // if the button is disabled, mSelected should not be modified and the button 132 // should be unselectable 133 return; 134 } 135 super.setSelected(selected); 136 mSelected = selected; 137 138 refreshIconAlpha(mIcon); 139 140 if (mShowMoreWhenSelected && mMoreIcon != null) { 141 mMoreIcon.setVisibility(selected ? VISIBLE : GONE); 142 } 143 updateImage(mIcon); 144 } 145 146 /** Gets whether the icon is in a selected state. */ getSelected()147 public boolean getSelected() { 148 return mSelected; 149 } 150 151 /** 152 * @param hasUnseen true if should indicate if this is a Unseen state, false otherwise. 153 */ setUnseen(boolean hasUnseen)154 public void setUnseen(boolean hasUnseen) { 155 mHasUnseen = hasUnseen; 156 updateImage(mIcon); 157 } 158 159 /** 160 * @param disabled true if icon should be isabled, false otherwise. 161 * @param runnable to run when button is clicked while disabled. 162 */ setDisabled(boolean disabled, @Nullable Runnable runnable)163 public void setDisabled(boolean disabled, @Nullable Runnable runnable) { 164 mDisabled = disabled; 165 mOnClickWhileDisabledRunnable = runnable; 166 refreshIconAlpha(mIcon); 167 updateImage(mIcon); 168 } 169 170 /** Gets whether the icon is disabled */ getDisabled()171 public boolean getDisabled() { 172 return mDisabled; 173 } 174 175 /** Runs the Runnable when the button is clicked while disabled */ runOnClickWhileDisabled()176 public void runOnClickWhileDisabled() { 177 if (mOnClickWhileDisabledRunnable == null) { 178 return; 179 } 180 mOnClickWhileDisabledRunnable.run(); 181 } 182 183 /** 184 * Sets the current icon of the default application associated with this button. 185 */ setAppIcon(Drawable appIcon)186 public void setAppIcon(Drawable appIcon) { 187 mAppIcon = appIcon; 188 updateImage(mIcon); 189 } 190 191 /** Gets the icon of the app currently associated to the role of this button. */ 192 @VisibleForTesting getAppIcon()193 protected Drawable getAppIcon() { 194 return mAppIcon; 195 } 196 197 /** Gets whether the icon is in an unseen state. */ getUnseen()198 public boolean getUnseen() { 199 return mHasUnseen; 200 } 201 202 /** 203 * @return The app categories the component represents 204 */ getCategories()205 public String[] getCategories() { 206 if (mButtonCategories == null) { 207 return new String[0]; 208 } 209 return mButtonCategories; 210 } 211 212 /** 213 * @return The valid packages that should be considered. 214 */ getPackages()215 public String[] getPackages() { 216 if (mButtonPackages == null) { 217 return new String[0]; 218 } 219 return mButtonPackages; 220 } 221 222 /** 223 * @return The list of component names. 224 */ getComponentName()225 public String[] getComponentName() { 226 if (mComponentNames == null) { 227 return new String[0]; 228 } 229 return mComponentNames; 230 } 231 232 @Override onVisibilityChanged(boolean isVisible)233 public void onVisibilityChanged(boolean isVisible) { 234 setSelected(isVisible); 235 } 236 237 /** 238 * Subclasses should override this method to return the {@link RoleManager} role associated 239 * with this button. 240 */ getRoleName()241 protected String getRoleName() { 242 return null; 243 } 244 245 /** 246 * @return true if this button should show the icon of the default application for the 247 * role returned by {@link #getRoleName()}. 248 */ isDefaultAppIconForRoleEnabled()249 protected boolean isDefaultAppIconForRoleEnabled() { 250 return mIsDefaultAppIconForRoleEnabled; 251 } 252 253 /** 254 * @return The id of the display the button is on or Display.INVALID_DISPLAY if it's not yet on 255 * a display. 256 */ getDisplayId()257 protected int getDisplayId() { 258 Display display = getDisplay(); 259 if (display == null) { 260 return Display.INVALID_DISPLAY; 261 } 262 return display.getDisplayId(); 263 } 264 hasSelectionState()265 protected boolean hasSelectionState() { 266 return mHighlightWhenSelected || mShowMoreWhenSelected; 267 } 268 getSelectedAlpha()269 protected float getSelectedAlpha() { 270 return mSelectedAlpha; 271 } 272 273 @VisibleForTesting getUnselectedAlpha()274 protected float getUnselectedAlpha() { 275 return mUnselectedAlpha; 276 } 277 278 @VisibleForTesting getDisabledAlpha()279 protected float getDisabledAlpha() { 280 return DISABLED_ALPHA; 281 } 282 283 @VisibleForTesting getIconAlpha()284 protected float getIconAlpha() { return mIcon.getAlpha(); } 285 286 /** 287 * Sets up intents for click, long touch, and broadcast. 288 */ setUpIntents(TypedArray typedArray)289 protected void setUpIntents(TypedArray typedArray) { 290 mIntent = typedArray.getString(R.styleable.CarSystemBarButton_intent); 291 mLongIntent = typedArray.getString(R.styleable.CarSystemBarButton_longIntent); 292 mBroadcastIntent = typedArray.getBoolean(R.styleable.CarSystemBarButton_broadcast, false); 293 294 mClearBackStack = typedArray.getBoolean(R.styleable.CarSystemBarButton_clearBackStack, 295 false); 296 297 String categoryString = typedArray.getString(R.styleable.CarSystemBarButton_categories); 298 String packageString = typedArray.getString(R.styleable.CarSystemBarButton_packages); 299 String componentNameString = 300 typedArray.getString(R.styleable.CarSystemBarButton_componentNames); 301 302 try { 303 if (mIntent != null) { 304 final Intent intent = Intent.parseUri(mIntent, Intent.URI_INTENT_SCHEME); 305 setOnClickListener(getButtonClickListener(intent)); 306 if (packageString != null) { 307 mButtonPackages = packageString.split(BUTTON_FILTER_DELIMITER); 308 intent.putExtra(EXTRA_BUTTON_PACKAGES, mButtonPackages); 309 } 310 if (categoryString != null) { 311 mButtonCategories = categoryString.split(BUTTON_FILTER_DELIMITER); 312 intent.putExtra(EXTRA_BUTTON_CATEGORIES, mButtonCategories); 313 } 314 if (componentNameString != null) { 315 mComponentNames = componentNameString.split(BUTTON_FILTER_DELIMITER); 316 } 317 } 318 } catch (URISyntaxException e) { 319 throw new RuntimeException("Failed to attach intent", e); 320 } 321 322 try { 323 if (mLongIntent != null && !mLongIntent.isEmpty() 324 && (Build.IS_ENG || Build.IS_USERDEBUG)) { 325 final Intent intent = Intent.parseUri(mLongIntent, Intent.URI_INTENT_SCHEME); 326 setOnLongClickListener(getButtonLongClickListener(intent)); 327 } 328 } catch (URISyntaxException e) { 329 throw new RuntimeException("Failed to attach long press intent", e); 330 } 331 } 332 333 /** Defines the behavior of a button click. */ getButtonClickListener(Intent toSend)334 protected OnClickListener getButtonClickListener(Intent toSend) { 335 return v -> { 336 if (mDisabled) { 337 runOnClickWhileDisabled(); 338 return; 339 } 340 boolean startState = mSelected; 341 Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); 342 intent.putExtra(EXTRA_DIALOG_CLOSE_REASON, DIALOG_CLOSE_REASON_CAR_SYSTEMBAR_BUTTON); 343 mContext.sendBroadcastAsUser(intent, getCurrentUserHandle(mContext, mUserTracker)); 344 345 boolean intentLaunched = false; 346 try { 347 if (mBroadcastIntent) { 348 mContext.sendBroadcastAsUser(toSend, 349 getCurrentUserHandle(mContext, mUserTracker)); 350 return; 351 } 352 ActivityOptions options = ActivityOptions.makeBasic(); 353 options.setLaunchDisplayId(mContext.getDisplayId()); 354 mContext.startActivityAsUser(toSend, options.toBundle(), 355 getCurrentUserHandle(mContext, mUserTracker)); 356 intentLaunched = true; 357 } catch (Exception e) { 358 Log.e(TAG, "Failed to launch intent", e); 359 } 360 361 if (intentLaunched && mClearBackStack) { 362 try { 363 ActivityTaskManager.RootTaskInfo rootTaskInfo = 364 ActivityTaskManager.getService().getRootTaskInfoOnDisplay( 365 WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_UNDEFINED, 366 mContext.getDisplayId()); 367 if (rootTaskInfo != null) { 368 mActivityManager.moveTaskToFront(rootTaskInfo.taskId, 369 ActivityManager.MOVE_TASK_WITH_HOME); 370 } 371 } catch (RemoteException e) { 372 Log.e(TAG, "Failed getting root task info", e); 373 } 374 } 375 376 if (mToggleSelectedState && (startState == mSelected)) { 377 setSelected(!mSelected); 378 } 379 }; 380 } 381 382 /** Defines the behavior of a long click. */ 383 protected OnLongClickListener getButtonLongClickListener(Intent toSend) { 384 return v -> { 385 Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); 386 intent.putExtra(EXTRA_DIALOG_CLOSE_REASON, DIALOG_CLOSE_REASON_CAR_SYSTEMBAR_BUTTON); 387 mContext.sendBroadcastAsUser(intent, getCurrentUserHandle(mContext, mUserTracker)); 388 try { 389 ActivityOptions options = ActivityOptions.makeBasic(); 390 options.setLaunchDisplayId(mContext.getDisplayId()); 391 mContext.startActivityAsUser(toSend, options.toBundle(), 392 getCurrentUserHandle(mContext, mUserTracker)); 393 } catch (Exception e) { 394 Log.e(TAG, "Failed to launch intent", e); 395 } 396 // consume event either way 397 return true; 398 }; 399 } 400 401 void setUserTracker(UserTracker userTracker) { 402 mUserTracker = userTracker; 403 } 404 405 /** 406 * Initializes view-related aspects of the button. 407 */ 408 private void setUpIcons(TypedArray typedArray) { 409 mSelectedAlpha = typedArray.getFloat( 410 R.styleable.CarSystemBarButton_selectedAlpha, DEFAULT_SELECTED_ALPHA); 411 mUnselectedAlpha = typedArray.getFloat( 412 R.styleable.CarSystemBarButton_unselectedAlpha, DEFAULT_UNSELECTED_ALPHA); 413 mHighlightWhenSelected = typedArray.getBoolean( 414 R.styleable.CarSystemBarButton_highlightWhenSelected, 415 mHighlightWhenSelected); 416 mShowMoreWhenSelected = typedArray.getBoolean( 417 R.styleable.CarSystemBarButton_showMoreWhenSelected, 418 mShowMoreWhenSelected); 419 420 mIconResourceId = typedArray.getResourceId( 421 R.styleable.CarSystemBarButton_icon, 0); 422 mSelectedIconResourceId = typedArray.getResourceId( 423 R.styleable.CarSystemBarButton_selectedIcon, mIconResourceId); 424 mIsDefaultAppIconForRoleEnabled = typedArray.getBoolean( 425 R.styleable.CarSystemBarButton_useDefaultAppIconForRole, false); 426 mToggleSelectedState = typedArray.getBoolean( 427 R.styleable.CarSystemBarButton_toggleSelected, false); 428 mIcon = findViewById(R.id.car_nav_button_icon_image); 429 refreshIconAlpha(mIcon); 430 mMoreIcon = findViewById(R.id.car_nav_button_more_icon); 431 mUnseenIcon = findViewById(R.id.car_nav_button_unseen_icon); 432 updateImage(mIcon); 433 } 434 435 protected void updateImage(AlphaOptimizedImageView icon) { 436 if (mIsDefaultAppIconForRoleEnabled && mAppIcon != null) { 437 icon.setImageDrawable(mAppIcon); 438 } else { 439 icon.setImageResource(mSelected ? mSelectedIconResourceId : mIconResourceId); 440 } 441 mUnseenIcon.setVisibility(mHasUnseen ? VISIBLE : GONE); 442 } 443 444 protected void refreshIconAlpha(AlphaOptimizedImageView icon) { 445 if (mDisabled) { 446 icon.setAlpha(DISABLED_ALPHA); 447 } else { 448 icon.setAlpha(mHighlightWhenSelected && mSelected ? mSelectedAlpha : mUnselectedAlpha); 449 } 450 } 451 452 @Nullable 453 protected UserTracker getUserTracker() { 454 return mUserTracker; 455 } 456 } 457