1 /*
2  * Copyright (C) 2018 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.statusbar.car;
18 
19 import android.app.ActivityManager;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.PackageManager;
24 import android.content.pm.ResolveInfo;
25 import android.view.Display;
26 import android.view.View;
27 import android.view.ViewGroup;
28 
29 import java.util.HashMap;
30 import java.util.HashSet;
31 import java.util.Iterator;
32 import java.util.List;
33 import java.util.Set;
34 
35 import javax.inject.Inject;
36 import javax.inject.Singleton;
37 
38 /**
39  * CarFacetButtons placed on the nav bar are designed to have visual indication that the active
40  * application on screen is associated with it. This is basically a similar concept to a radio
41  * button group.
42  */
43 @Singleton
44 public class CarFacetButtonController {
45 
46     protected ButtonMap mButtonsByCategory = new ButtonMap();
47     protected ButtonMap mButtonsByPackage = new ButtonMap();
48     protected ButtonMap mButtonsByComponentName = new ButtonMap();
49     protected HashSet<CarFacetButton> mSelectedFacetButtons;
50     protected Context mContext;
51 
52     @Inject
CarFacetButtonController(Context context)53     public CarFacetButtonController(Context context) {
54         mContext = context;
55         mSelectedFacetButtons = new HashSet<>();
56     }
57 
58     /**
59      * Add facet button to this controller. The expected use is for the facet button
60      * to get a reference to this controller via {@link com.android.systemui.Dependency}
61      * and self add.
62      */
addFacetButton(CarFacetButton facetButton)63     public void addFacetButton(CarFacetButton facetButton) {
64         String[] categories = facetButton.getCategories();
65         for (int i = 0; i < categories.length; i++) {
66             mButtonsByCategory.add(categories[i], facetButton);
67         }
68 
69         String[] facetPackages = facetButton.getFacetPackages();
70         for (int i = 0; i < facetPackages.length; i++) {
71             mButtonsByPackage.add(facetPackages[i], facetButton);
72         }
73         String[] componentNames = facetButton.getComponentName();
74         for (int i = 0; i < componentNames.length; i++) {
75             mButtonsByComponentName.add(componentNames[i], facetButton);
76         }
77     }
78 
removeAll()79     public void removeAll() {
80         mButtonsByCategory.clear();
81         mButtonsByPackage.clear();
82         mButtonsByComponentName.clear();
83         mSelectedFacetButtons.clear();
84     }
85 
86     /**
87      * Iterate through a view looking for CarFacetButtons and adding them to the controller if found
88      *
89      * @param v the View that may contain CarFacetButtons
90      */
addAllFacetButtons(View v)91     public void addAllFacetButtons(View v) {
92         if (v instanceof CarFacetButton) {
93             addFacetButton((CarFacetButton) v);
94         } else if (v instanceof ViewGroup) {
95             ViewGroup viewGroup = (ViewGroup) v;
96             for (int i = 0; i < viewGroup.getChildCount(); i++) {
97                 addAllFacetButtons(viewGroup.getChildAt(i));
98             }
99         }
100     }
101 
102     /**
103      * This will unselect the currently selected CarFacetButton and determine which one should be
104      * selected next. It does this by reading the properties on the CarFacetButton and seeing if
105      * they are a match with the supplied StackInfo list.
106      * The order of selection detection is ComponentName, PackageName then Category
107      * They will then be compared with the supplied StackInfo list.
108      * The StackInfo is expected to be supplied in order of recency and StackInfo will only be used
109      * for consideration if it has the same displayId as the CarFacetButtons.
110      *
111      * @param stackInfoList of the currently running application
112      */
taskChanged(List<ActivityManager.StackInfo> stackInfoList)113     public void taskChanged(List<ActivityManager.StackInfo> stackInfoList) {
114         ActivityManager.StackInfo validStackInfo = null;
115         for (ActivityManager.StackInfo stackInfo : stackInfoList) {
116             // Find the first stack info with a topActivity in the primary display.
117             // TODO: We assume that CarFacetButton will launch an app only in the primary display.
118             // We need to extend the functionality to handle the mutliple display properly.
119             if (stackInfo.topActivity != null && stackInfo.displayId == Display.DEFAULT_DISPLAY) {
120                 validStackInfo = stackInfo;
121                 break;
122             }
123         }
124 
125         if (validStackInfo == null) {
126             // No stack was found that was on the same display as the facet buttons thus return
127             return;
128         }
129 
130         if (mSelectedFacetButtons != null) {
131             Iterator<CarFacetButton> iterator = mSelectedFacetButtons.iterator();
132             while(iterator.hasNext()) {
133                 CarFacetButton carFacetButton = iterator.next();
134                 if (carFacetButton.getDisplayId() == validStackInfo.displayId) {
135                     carFacetButton.setSelected(false);
136                     iterator.remove();
137                 }
138             }
139         }
140 
141         String packageName = validStackInfo.topActivity.getPackageName();
142         HashSet<CarFacetButton> facetButton =
143                 findFacetButtonByComponentName(validStackInfo.topActivity);
144         if (facetButton == null) {
145             facetButton = mButtonsByPackage.get(packageName);
146         }
147 
148         if (facetButton == null) {
149             String category = getPackageCategory(packageName);
150             if (category != null) {
151                 facetButton = mButtonsByCategory.get(category);
152             }
153         }
154 
155         if (facetButton != null) {
156             for (CarFacetButton carFacetButton : facetButton) {
157                 if (carFacetButton.getDisplayId() == validStackInfo.displayId) {
158                     carFacetButton.setSelected(true);
159                     mSelectedFacetButtons.add(carFacetButton);
160                 }
161             }
162         }
163 
164     }
165 
findFacetButtonByComponentName(ComponentName componentName)166     private HashSet<CarFacetButton> findFacetButtonByComponentName(ComponentName componentName) {
167         HashSet<CarFacetButton> buttons =
168                 mButtonsByComponentName.get(componentName.flattenToShortString());
169         return (buttons != null) ? buttons :
170                 mButtonsByComponentName.get(componentName.flattenToString());
171     }
172 
getPackageCategory(String packageName)173     protected String getPackageCategory(String packageName) {
174         PackageManager pm = mContext.getPackageManager();
175         Set<String> supportedCategories = mButtonsByCategory.keySet();
176         for (String category : supportedCategories) {
177             Intent intent = new Intent();
178             intent.setPackage(packageName);
179             intent.setAction(Intent.ACTION_MAIN);
180             intent.addCategory(category);
181             List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
182             if (list.size() > 0) {
183                 // Cache this package name into facetPackageMap, so we won't have to query
184                 // all categories next time this package name shows up.
185                 mButtonsByPackage.put(packageName, mButtonsByCategory.get(category));
186                 return category;
187             }
188         }
189         return null;
190     }
191 
192     // simple multi-map
193     private static class ButtonMap extends HashMap<String, HashSet<CarFacetButton>> {
194 
add(String key, CarFacetButton value)195         public boolean add(String key, CarFacetButton value) {
196             if (containsKey(key)) {
197                 return get(key).add(value);
198             }
199             HashSet<CarFacetButton> set = new HashSet<>();
200             set.add(value);
201             put(key, set);
202             return true;
203         }
204     }
205 }
206