• 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.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