/* * 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 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 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 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 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 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(); } }