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