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