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