blob: 737ce5a82f6e162217a484948eb72b63f4416109 [file] [log] [blame]
/*
* 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();
}
}