• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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