blob: 19e5846f66331911745bbdcdbf6c029a62ce3478 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.car.systembar;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
import android.app.ActivityTaskManager;
import android.app.ActivityTaskManager.RootTaskInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import com.android.systemui.dagger.SysUISingleton;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
/**
* CarSystemBarButtons can optionally have selection state that toggles certain visual indications
* based on whether the active application on screen is associated with it. This is basically a
* similar concept to a radio button group.
*
* This class controls the selection state of CarSystemBarButtons that have opted in to have such
* selection state-dependent visual indications.
*/
@SysUISingleton
public class ButtonSelectionStateController {
private static final String TAG = ButtonSelectionStateController.class.getSimpleName();
private final Set<CarSystemBarButton> mRegisteredViews = new HashSet<>();
protected ButtonMap mButtonsByCategory = new ButtonMap();
protected ButtonMap mButtonsByPackage = new ButtonMap();
protected ButtonMap mButtonsByComponentName = new ButtonMap();
protected HashSet<CarSystemBarButton> mSelectedButtons;
protected Context mContext;
@Inject
public ButtonSelectionStateController(Context context) {
mContext = context;
mSelectedButtons = new HashSet<>();
}
/**
* Iterate through a view looking for CarSystemBarButton and add it to the controller if it
* opted in to be highlighted when the active application is associated with it.
*
* @param v the View that may contain CarFacetButtons
*/
protected void addAllButtonsWithSelectionState(View v) {
if (v instanceof CarSystemBarButton) {
if (((CarSystemBarButton) v).hasSelectionState()) {
addButtonWithSelectionState((CarSystemBarButton) v);
}
} else if (v instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) v;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
addAllButtonsWithSelectionState(viewGroup.getChildAt(i));
}
}
}
/** Removes all buttons from the button maps. */
protected void removeAll() {
mButtonsByCategory.clear();
mButtonsByPackage.clear();
mButtonsByComponentName.clear();
mSelectedButtons.clear();
mRegisteredViews.clear();
}
/**
* This will unselect the currently selected CarSystemBarButton and determine which one should
* be selected next. It does this by reading the properties on the CarSystemBarButton and
* seeing if they are a match with the supplied StackInfo list.
* The order of selection detection is ComponentName, PackageName then Category
* They will then be compared with the supplied StackInfo list.
* The StackInfo is expected to be supplied in order of recency and StackInfo will only be used
* for consideration if it has the same displayId as the CarSystemBarButton.
*
* @param taskInfoList of the currently running application
* @param validDisplay index of the valid display
*/
protected void taskChanged(List<RootTaskInfo> taskInfoList, int validDisplay) {
RootTaskInfo validTaskInfo = null;
for (RootTaskInfo taskInfo : taskInfoList) {
// Find the first stack info with a topActivity in the primary display.
// TODO: We assume that CarFacetButton will launch an app only in the primary display.
// We need to extend the functionality to handle the multiple display properly.
if (taskInfo.topActivity != null && taskInfo.displayAreaFeatureId == validDisplay) {
validTaskInfo = taskInfo;
break;
}
}
if (validTaskInfo == null) {
// No stack was found that was on the same display as the buttons thus return
return;
}
int displayId = validTaskInfo.displayId;
// Clear all registered views
mRegisteredViews.forEach(carSystemBarButton -> {
if (carSystemBarButton.getDisplayId() == displayId) {
carSystemBarButton.setSelected(false);
}
});
mSelectedButtons.clear();
HashSet<CarSystemBarButton> selectedButtons = findSelectedButtons(validTaskInfo);
if (selectedButtons != null) {
selectedButtons.forEach(carSystemBarButton -> {
if (carSystemBarButton.getDisplayId() == displayId) {
carSystemBarButton.setSelected(true);
mSelectedButtons.add(carSystemBarButton);
}
});
}
}
/**
* Defaults to Display.DEFAULT_DISPLAY when no parameter is provided for the validDisplay.
*
* @param taskInfoList
*/
protected void taskChanged(List<RootTaskInfo> taskInfoList) {
taskChanged(taskInfoList, FEATURE_DEFAULT_TASK_CONTAINER);
}
/**
* Add navigation button to this controller if it uses selection state.
*/
private void addButtonWithSelectionState(CarSystemBarButton carSystemBarButton) {
if (mRegisteredViews.contains(carSystemBarButton)) {
return;
}
String[] categories = carSystemBarButton.getCategories();
for (int i = 0; i < categories.length; i++) {
mButtonsByCategory.add(categories[i], carSystemBarButton);
}
String[] packages = carSystemBarButton.getPackages();
for (int i = 0; i < packages.length; i++) {
mButtonsByPackage.add(packages[i], carSystemBarButton);
}
String[] componentNames = carSystemBarButton.getComponentName();
for (int i = 0; i < componentNames.length; i++) {
mButtonsByComponentName.add(componentNames[i], carSystemBarButton);
}
mRegisteredViews.add(carSystemBarButton);
}
private HashSet<CarSystemBarButton> findSelectedButtons(RootTaskInfo validTaskInfo) {
ComponentName topActivity = null;
// Window mode being WINDOW_MODE_MULTI_WINDOW implies TaskView might be visible on the
// display. In such cases, topActivity reported by validTaskInfo will be the one hosted in
// TaskView and not necessarily the main activity visible on display. Thus we should get
// rootTaskInfo instead.
if (validTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
try {
RootTaskInfo rootTaskInfo =
ActivityTaskManager.getService().getRootTaskInfoOnDisplay(
WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_UNDEFINED,
validTaskInfo.displayId);
topActivity = rootTaskInfo.topActivity;
} catch (RemoteException e) {
Log.e(TAG, "findSelectedButtons: Failed getting root task info", e);
}
} else {
topActivity = validTaskInfo.topActivity;
}
if (topActivity == null) return null;
String packageName = topActivity.getPackageName();
HashSet<CarSystemBarButton> selectedButtons =
findButtonsByComponentName(topActivity);
if (selectedButtons == null) {
selectedButtons = mButtonsByPackage.get(packageName);
}
if (selectedButtons == null) {
String category = getPackageCategory(packageName);
if (category != null) {
selectedButtons = mButtonsByCategory.get(category);
}
}
return selectedButtons;
}
private HashSet<CarSystemBarButton> findButtonsByComponentName(
ComponentName componentName) {
HashSet<CarSystemBarButton> buttons =
mButtonsByComponentName.get(componentName.flattenToShortString());
return (buttons != null) ? buttons :
mButtonsByComponentName.get(componentName.flattenToString());
}
private String getPackageCategory(String packageName) {
PackageManager pm = mContext.getPackageManager();
Set<String> supportedCategories = mButtonsByCategory.keySet();
for (String category : supportedCategories) {
Intent intent = new Intent();
intent.setPackage(packageName);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(category);
List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
if (list.size() > 0) {
// Cache this package name into ButtonsByPackage map, so we won't have to query
// all categories next time this package name shows up.
mButtonsByPackage.put(packageName, mButtonsByCategory.get(category));
return category;
}
}
return null;
}
// simple multi-map
private static class ButtonMap extends HashMap<String, HashSet<CarSystemBarButton>> {
public boolean add(String key, CarSystemBarButton value) {
if (containsKey(key)) {
return get(key).add(value);
}
HashSet<CarSystemBarButton> set = new HashSet<>();
set.add(value);
put(key, set);
return true;
}
}
}