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