1 /* 2 * Copyright (C) 2019 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.server.telecom; 18 19 import android.annotation.IntRange; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.app.UiModeManager; 23 import android.telecom.Log; 24 import android.util.LocalLog; 25 26 import com.android.internal.util.IndentingPrintWriter; 27 28 import java.util.Comparator; 29 import java.util.List; 30 import java.util.Objects; 31 import java.util.Optional; 32 import java.util.PriorityQueue; 33 import java.util.function.Function; 34 import java.util.stream.Collectors; 35 36 /** 37 * Tracks the package names of apps which enter end exit car mode. 38 */ 39 public class CarModeTracker { 40 /** 41 * Data class holding information about apps which have requested to enter car mode. 42 */ 43 private class CarModeApp { 44 private final boolean mAutomotiveProjection; 45 private final @IntRange(from = 0) int mPriority; 46 private @NonNull String mPackageName; 47 CarModeApp(@onNull String packageName)48 public CarModeApp(@NonNull String packageName) { 49 this(true, 0, packageName); 50 } 51 CarModeApp(int priority, @NonNull String packageName)52 public CarModeApp(int priority, @NonNull String packageName) { 53 this(false, priority, packageName); 54 } 55 CarModeApp(boolean automotiveProjection, int priority, @NonNull String packageName)56 private CarModeApp(boolean automotiveProjection, int priority, @NonNull String packageName) { 57 mAutomotiveProjection = automotiveProjection; 58 mPriority = priority; 59 mPackageName = Objects.requireNonNull(packageName); 60 } 61 hasSetAutomotiveProjection()62 public boolean hasSetAutomotiveProjection() { 63 return mAutomotiveProjection; 64 } 65 66 /** 67 * The priority at which the app requested to enter car mode. 68 * Will be the same as the one specified when {@link UiModeManager#enableCarMode(int, int)} 69 * was called, or {@link UiModeManager#DEFAULT_PRIORITY} if no priority was specified. 70 * @return The priority. 71 */ getPriority()72 public int getPriority() { 73 return mPriority; 74 } 75 76 /** 77 * @return The package name of the app which requested to enter car mode/set projection. 78 */ getPackageName()79 public String getPackageName() { 80 return mPackageName; 81 } 82 setPackageName(String packageName)83 public void setPackageName(String packageName) { 84 mPackageName = packageName; 85 } 86 toString()87 public String toString() { 88 return String.format("[%s, %s]", 89 mAutomotiveProjection ? "PROJECTION SET" : mPriority, 90 mPackageName); 91 } 92 } 93 94 /** 95 * Priority list of apps which have entered or exited car mode, ordered first by whether the app 96 * has set automotive projection, and then by highest priority. Where items have the same 97 * priority, order is arbitrary, but we only allow one item in the queue per priority. 98 */ 99 private PriorityQueue<CarModeApp> mCarModeApps = new PriorityQueue<>(2, 100 // Natural ordering of booleans is False, True. Natural ordering of ints is increasing. 101 Comparator.comparing(CarModeApp::hasSetAutomotiveProjection) 102 .thenComparing(CarModeApp::getPriority) 103 .reversed()); 104 105 private final LocalLog mCarModeChangeLog = new LocalLog(20); 106 107 /** 108 * Handles a request to enter car mode by a package name. 109 * @param priority The priority at which car mode is entered. 110 * @param packageName The package name of the app entering car mode. 111 */ handleEnterCarMode(@ntRangefrom = 0) int priority, @NonNull String packageName)112 public void handleEnterCarMode(@IntRange(from = 0) int priority, @NonNull String packageName) { 113 if (mCarModeApps.stream().anyMatch(c -> c.getPriority() == priority)) { 114 Log.w(this, "handleEnterCarMode: already in car mode at priority %d (apps: %s)", 115 priority, getCarModePriorityString()); 116 return; 117 } 118 119 if (mCarModeApps.stream().anyMatch(c -> c.getPackageName().equals(packageName))) { 120 Log.w(this, "handleEnterCarMode: %s is already in car mode (apps: %s)", 121 packageName, getCarModePriorityString()); 122 return; 123 } 124 125 Log.i(this, "handleEnterCarMode: packageName=%s, priority=%d", packageName, priority); 126 mCarModeChangeLog.log("enterCarMode: packageName=" + packageName + ", priority=" 127 + priority); 128 mCarModeApps.add(new CarModeApp(priority, packageName)); 129 } 130 131 /** 132 * Handles a request to exist car mode at a priority level. 133 * @param priority The priority level. 134 * @param packageName The packagename of the app requesting the change. 135 */ handleExitCarMode(@ntRangefrom = 0) int priority, @NonNull String packageName)136 public void handleExitCarMode(@IntRange(from = 0) int priority, @NonNull String packageName) { 137 if (!mCarModeApps.stream().anyMatch(c -> c.getPriority() == priority)) { 138 Log.w(this, "handleExitCarMode: not in car mode at priority %d (apps=%s)", 139 priority, getCarModePriorityString()); 140 return; 141 } 142 143 if (priority != UiModeManager.DEFAULT_PRIORITY && !mCarModeApps.stream().anyMatch( 144 c -> c.getPackageName().equals(packageName) && c.getPriority() == priority)) { 145 Log.w(this, "handleExitCarMode: %s didn't enter car mode at priority %d (apps=%s)", 146 packageName, priority, getCarModePriorityString()); 147 return; 148 } 149 150 Log.i(this, "handleExitCarMode: packageName=%s, priority=%d", packageName, priority); 151 mCarModeChangeLog.log("exitCarMode: packageName=" + packageName + ", priority=" 152 + priority); 153 154 //Remove the car mode app with specified priority without clearing out the projection entry. 155 mCarModeApps.removeIf(c -> c.getPriority() == priority && !c.hasSetAutomotiveProjection()); 156 } 157 handleSetAutomotiveProjection(@onNull String packageName)158 public void handleSetAutomotiveProjection(@NonNull String packageName) { 159 Optional<CarModeApp> projectingApp = mCarModeApps.stream() 160 .filter(CarModeApp::hasSetAutomotiveProjection) 161 .findAny(); 162 // No app with automotive projection? Easy peasy, just add it. 163 if (!projectingApp.isPresent()) { 164 Log.i(this, "handleSetAutomotiveProjection: %s", packageName); 165 mCarModeChangeLog.log("setAutomotiveProjection: packageName=" + packageName); 166 mCarModeApps.add(new CarModeApp(packageName)); 167 return; 168 } 169 // Otherwise an app already has automotive projection set. Is it the same app? 170 if (packageName.equals(projectingApp.get().getPackageName())) { 171 Log.w(this, "handleSetAutomotiveProjection: %s already the automotive projection app", 172 packageName); 173 return; 174 } 175 // We have a new app for automotive projection. As a shortcut just reuse the same object by 176 // overwriting the package name. 177 Log.i(this, "handleSetAutomotiveProjection: %s replacing %s as automotive projection app", 178 packageName, projectingApp.get().getPackageName()); 179 mCarModeChangeLog.log("setAutomotiveProjection: " + packageName + " replaces " 180 + projectingApp.get().getPackageName()); 181 projectingApp.get().setPackageName(packageName); 182 } 183 handleReleaseAutomotiveProjection()184 public void handleReleaseAutomotiveProjection() { 185 Optional<String> projectingPackage = mCarModeApps.stream() 186 .filter(CarModeApp::hasSetAutomotiveProjection) 187 .map(CarModeApp::getPackageName) 188 .findAny(); 189 if (!projectingPackage.isPresent()) { 190 Log.w(this, "handleReleaseAutomotiveProjection: no current automotive projection app"); 191 return; 192 } 193 Log.i(this, "handleReleaseAutomotiveProjection: %s", projectingPackage.get()); 194 mCarModeChangeLog.log("releaseAutomotiveProjection: packageName=" 195 + projectingPackage.get()); 196 mCarModeApps.removeIf(CarModeApp::hasSetAutomotiveProjection); 197 } 198 199 /** 200 * Force-removes a package from the car mode tracking list, no matter at which priority. 201 * 202 * This handles the case where packages are disabled or uninstalled. In those case, remove them 203 * from the tracking list so they don't cause a leak. 204 * @param packageName Package name of the app to force-remove 205 */ forceRemove(@onNull String packageName)206 public void forceRemove(@NonNull String packageName) { 207 // We must account for the possibility that the app has set both car mode AND projection. 208 List<CarModeApp> forcedApp = mCarModeApps.stream() 209 .filter(c -> c.getPackageName().equals(packageName)) 210 .collect(Collectors.toList()); 211 if (forcedApp.isEmpty()) { 212 Log.i(this, "Package %s is not tracked.", packageName); 213 return; 214 } 215 for (CarModeApp app : forcedApp) { 216 String logString = "forceRemove: " + app; 217 Log.i(this, logString); 218 mCarModeChangeLog.log(logString); 219 } 220 mCarModeApps.removeIf(c -> c.getPackageName().equals(packageName)); 221 } 222 223 /** 224 * Retrieves a list of the apps which are currently in car mode, ordered by priority such that 225 * the highest priority app is first. 226 * @return List of apps in car mode. 227 */ getCarModeApps()228 public @NonNull List<String> getCarModeApps() { 229 return mCarModeApps 230 .stream() 231 .sorted(mCarModeApps.comparator()) 232 .map(CarModeApp::getPackageName) 233 .collect(Collectors.toList()); 234 } 235 getCarModePriorityString()236 private @NonNull String getCarModePriorityString() { 237 return mCarModeApps 238 .stream() 239 .sorted(mCarModeApps.comparator()) 240 .map(CarModeApp::toString) 241 .collect(Collectors.joining(", ")); 242 } 243 244 /** 245 * Gets the app which is currently in car mode. This is the highest priority app which has 246 * entered car mode. 247 * @return The app which is in car mode. 248 */ getCurrentCarModePackage()249 public @Nullable String getCurrentCarModePackage() { 250 CarModeApp app = mCarModeApps.peek(); 251 return app == null ? null : app.getPackageName(); 252 } 253 254 /** 255 * @return {@code true} if the device is in car mode, {@code false} otherwise. 256 */ isInCarMode()257 public boolean isInCarMode() { 258 return !mCarModeApps.isEmpty(); 259 } 260 261 /** 262 * Dumps the state of the car mode tracker to the specified print writer. 263 * @param pw 264 */ dump(IndentingPrintWriter pw)265 public void dump(IndentingPrintWriter pw) { 266 pw.println("CarModeTracker:"); 267 pw.increaseIndent(); 268 269 pw.println("Current car mode apps:"); 270 pw.increaseIndent(); 271 for (CarModeApp app : mCarModeApps) { 272 pw.print("["); 273 pw.print(app.hasSetAutomotiveProjection() ? "PROJECTION SET" : app.getPriority()); 274 pw.print("] "); 275 pw.println(app.getPackageName()); 276 } 277 pw.decreaseIndent(); 278 279 pw.println("Car mode history:"); 280 pw.increaseIndent(); 281 mCarModeChangeLog.dump(pw); 282 pw.decreaseIndent(); 283 284 pw.decreaseIndent(); 285 } 286 } 287