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