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