| /* |
| * Copyright (C) 2019 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.server.telecom; |
| |
| import android.annotation.IntRange; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.UiModeManager; |
| import android.telecom.Log; |
| import android.util.LocalLog; |
| |
| import com.android.internal.util.IndentingPrintWriter; |
| |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.PriorityQueue; |
| import java.util.function.Function; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Tracks the package names of apps which enter end exit car mode. |
| */ |
| public class CarModeTracker { |
| /** |
| * Data class holding information about apps which have requested to enter car mode. |
| */ |
| private class CarModeApp { |
| private final boolean mAutomotiveProjection; |
| private final @IntRange(from = 0) int mPriority; |
| private @NonNull String mPackageName; |
| |
| public CarModeApp(@NonNull String packageName) { |
| this(true, 0, packageName); |
| } |
| |
| public CarModeApp(int priority, @NonNull String packageName) { |
| this(false, priority, packageName); |
| } |
| |
| private CarModeApp(boolean automotiveProjection, int priority, @NonNull String packageName) { |
| mAutomotiveProjection = automotiveProjection; |
| mPriority = priority; |
| mPackageName = Objects.requireNonNull(packageName); |
| } |
| |
| public boolean hasSetAutomotiveProjection() { |
| return mAutomotiveProjection; |
| } |
| |
| /** |
| * The priority at which the app requested to enter car mode. |
| * Will be the same as the one specified when {@link UiModeManager#enableCarMode(int, int)} |
| * was called, or {@link UiModeManager#DEFAULT_PRIORITY} if no priority was specified. |
| * @return The priority. |
| */ |
| public int getPriority() { |
| return mPriority; |
| } |
| |
| /** |
| * @return The package name of the app which requested to enter car mode/set projection. |
| */ |
| public String getPackageName() { |
| return mPackageName; |
| } |
| |
| public void setPackageName(String packageName) { |
| mPackageName = packageName; |
| } |
| |
| public String toString() { |
| return String.format("[%s, %s]", |
| mAutomotiveProjection ? "PROJECTION SET" : mPriority, |
| mPackageName); |
| } |
| } |
| |
| /** |
| * Priority list of apps which have entered or exited car mode, ordered first by whether the app |
| * has set automotive projection, and then by highest priority. Where items have the same |
| * priority, order is arbitrary, but we only allow one item in the queue per priority. |
| */ |
| private PriorityQueue<CarModeApp> mCarModeApps = new PriorityQueue<>(2, |
| // Natural ordering of booleans is False, True. Natural ordering of ints is increasing. |
| Comparator.comparing(CarModeApp::hasSetAutomotiveProjection) |
| .thenComparing(CarModeApp::getPriority) |
| .reversed()); |
| |
| private final LocalLog mCarModeChangeLog = new LocalLog(20); |
| |
| /** |
| * Handles a request to enter car mode by a package name. |
| * @param priority The priority at which car mode is entered. |
| * @param packageName The package name of the app entering car mode. |
| */ |
| public void handleEnterCarMode(@IntRange(from = 0) int priority, @NonNull String packageName) { |
| if (mCarModeApps.stream().anyMatch(c -> c.getPriority() == priority)) { |
| Log.w(this, "handleEnterCarMode: already in car mode at priority %d (apps: %s)", |
| priority, getCarModePriorityString()); |
| return; |
| } |
| |
| if (mCarModeApps.stream().anyMatch(c -> c.getPackageName().equals(packageName))) { |
| Log.w(this, "handleEnterCarMode: %s is already in car mode (apps: %s)", |
| packageName, getCarModePriorityString()); |
| return; |
| } |
| |
| Log.i(this, "handleEnterCarMode: packageName=%s, priority=%d", packageName, priority); |
| mCarModeChangeLog.log("enterCarMode: packageName=" + packageName + ", priority=" |
| + priority); |
| mCarModeApps.add(new CarModeApp(priority, packageName)); |
| } |
| |
| /** |
| * Handles a request to exist car mode at a priority level. |
| * @param priority The priority level. |
| * @param packageName The packagename of the app requesting the change. |
| */ |
| public void handleExitCarMode(@IntRange(from = 0) int priority, @NonNull String packageName) { |
| if (!mCarModeApps.stream().anyMatch(c -> c.getPriority() == priority)) { |
| Log.w(this, "handleExitCarMode: not in car mode at priority %d (apps=%s)", |
| priority, getCarModePriorityString()); |
| return; |
| } |
| |
| if (priority != UiModeManager.DEFAULT_PRIORITY && !mCarModeApps.stream().anyMatch( |
| c -> c.getPackageName().equals(packageName) && c.getPriority() == priority)) { |
| Log.w(this, "handleExitCarMode: %s didn't enter car mode at priority %d (apps=%s)", |
| packageName, priority, getCarModePriorityString()); |
| return; |
| } |
| |
| Log.i(this, "handleExitCarMode: packageName=%s, priority=%d", packageName, priority); |
| mCarModeChangeLog.log("exitCarMode: packageName=" + packageName + ", priority=" |
| + priority); |
| mCarModeApps.removeIf(c -> c.getPriority() == priority); |
| } |
| |
| public void handleSetAutomotiveProjection(@NonNull String packageName) { |
| Optional<CarModeApp> projectingApp = mCarModeApps.stream() |
| .filter(CarModeApp::hasSetAutomotiveProjection) |
| .findAny(); |
| // No app with automotive projection? Easy peasy, just add it. |
| if (!projectingApp.isPresent()) { |
| Log.i(this, "handleSetAutomotiveProjection: %s", packageName); |
| mCarModeChangeLog.log("setAutomotiveProjection: packageName=" + packageName); |
| mCarModeApps.add(new CarModeApp(packageName)); |
| return; |
| } |
| // Otherwise an app already has automotive projection set. Is it the same app? |
| if (packageName.equals(projectingApp.get().getPackageName())) { |
| Log.w(this, "handleSetAutomotiveProjection: %s already the automotive projection app", |
| packageName); |
| return; |
| } |
| // We have a new app for automotive projection. As a shortcut just reuse the same object by |
| // overwriting the package name. |
| Log.i(this, "handleSetAutomotiveProjection: %s replacing %s as automotive projection app", |
| packageName, projectingApp.get().getPackageName()); |
| mCarModeChangeLog.log("setAutomotiveProjection: " + packageName + " replaces " |
| + projectingApp.get().getPackageName()); |
| projectingApp.get().setPackageName(packageName); |
| } |
| |
| public void handleReleaseAutomotiveProjection() { |
| Optional<String> projectingPackage = mCarModeApps.stream() |
| .filter(CarModeApp::hasSetAutomotiveProjection) |
| .map(CarModeApp::getPackageName) |
| .findAny(); |
| if (!projectingPackage.isPresent()) { |
| Log.w(this, "handleReleaseAutomotiveProjection: no current automotive projection app"); |
| return; |
| } |
| Log.i(this, "handleReleaseAutomotiveProjection: %s", projectingPackage.get()); |
| mCarModeChangeLog.log("releaseAutomotiveProjection: packageName=" |
| + projectingPackage.get()); |
| mCarModeApps.removeIf(CarModeApp::hasSetAutomotiveProjection); |
| } |
| |
| /** |
| * Force-removes a package from the car mode tracking list, no matter at which priority. |
| * |
| * This handles the case where packages are disabled or uninstalled. In those case, remove them |
| * from the tracking list so they don't cause a leak. |
| * @param packageName Package name of the app to force-remove |
| */ |
| public void forceRemove(@NonNull String packageName) { |
| // We must account for the possibility that the app has set both car mode AND projection. |
| List<CarModeApp> forcedApp = mCarModeApps.stream() |
| .filter(c -> c.getPackageName().equals(packageName)) |
| .collect(Collectors.toList()); |
| if (forcedApp.isEmpty()) { |
| Log.i(this, "Package %s is not tracked.", packageName); |
| return; |
| } |
| for (CarModeApp app : forcedApp) { |
| String logString = "forceRemove: " + app; |
| Log.i(this, logString); |
| mCarModeChangeLog.log(logString); |
| } |
| mCarModeApps.removeIf(c -> c.getPackageName().equals(packageName)); |
| } |
| |
| /** |
| * Retrieves a list of the apps which are currently in car mode, ordered by priority such that |
| * the highest priority app is first. |
| * @return List of apps in car mode. |
| */ |
| public @NonNull List<String> getCarModeApps() { |
| return mCarModeApps |
| .stream() |
| .sorted(mCarModeApps.comparator()) |
| .map(CarModeApp::getPackageName) |
| .collect(Collectors.toList()); |
| } |
| |
| private @NonNull String getCarModePriorityString() { |
| return mCarModeApps |
| .stream() |
| .sorted(mCarModeApps.comparator()) |
| .map(CarModeApp::toString) |
| .collect(Collectors.joining(", ")); |
| } |
| |
| /** |
| * Gets the app which is currently in car mode. This is the highest priority app which has |
| * entered car mode. |
| * @return The app which is in car mode. |
| */ |
| public @Nullable String getCurrentCarModePackage() { |
| CarModeApp app = mCarModeApps.peek(); |
| return app == null ? null : app.getPackageName(); |
| } |
| |
| /** |
| * @return {@code true} if the device is in car mode, {@code false} otherwise. |
| */ |
| public boolean isInCarMode() { |
| return !mCarModeApps.isEmpty(); |
| } |
| |
| /** |
| * Dumps the state of the car mode tracker to the specified print writer. |
| * @param pw |
| */ |
| public void dump(IndentingPrintWriter pw) { |
| pw.println("CarModeTracker:"); |
| pw.increaseIndent(); |
| |
| pw.println("Current car mode apps:"); |
| pw.increaseIndent(); |
| for (CarModeApp app : mCarModeApps) { |
| pw.print("["); |
| pw.print(app.hasSetAutomotiveProjection() ? "PROJECTION SET" : app.getPriority()); |
| pw.print("] "); |
| pw.println(app.getPackageName()); |
| } |
| pw.decreaseIndent(); |
| |
| pw.println("Car mode history:"); |
| pw.increaseIndent(); |
| mCarModeChangeLog.dump(pw); |
| pw.decreaseIndent(); |
| |
| pw.decreaseIndent(); |
| } |
| } |