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