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.PriorityQueue;
32 import java.util.stream.Collectors;
33 
34 /**
35  * Tracks the package names of apps which enter end exit car mode.
36  */
37 public class CarModeTracker {
38     /**
39      * Data class holding information about apps which have requested to enter car mode.
40      */
41     private class CarModeApp {
42         private @IntRange(from = 0) int mPriority;
43         private @NonNull String mPackageName;
44 
CarModeApp(int priority, @NonNull String packageName)45         public CarModeApp(int priority, @NonNull String packageName) {
46             mPriority = priority;
47             mPackageName = Objects.requireNonNull(packageName);
48         }
49 
50         /**
51          * The priority at which the app requested to enter car mode.
52          * Will be the same as the one specified when {@link UiModeManager#enableCarMode(int, int)}
53          * was called, or {@link UiModeManager#DEFAULT_PRIORITY} if no priority was specifeid.
54          * @return The priority.
55          */
getPriority()56         public int getPriority() {
57             return mPriority;
58         }
59 
setPriority(int priority)60         public void setPriority(int priority) {
61             mPriority = priority;
62         }
63 
64         /**
65          * @return The package name of the app which requested to enter car mode.
66          */
getPackageName()67         public String getPackageName() {
68             return mPackageName;
69         }
70 
setPackageName(String packageName)71         public void setPackageName(String packageName) {
72             mPackageName = packageName;
73         }
74     }
75 
76     /**
77      * Comparator used to maintain the car mode priority queue ordering.
78      */
79     private class CarModeAppComparator implements Comparator<CarModeApp> {
80         @Override
compare(CarModeApp o1, CarModeApp o2)81         public int compare(CarModeApp o1, CarModeApp o2) {
82             // highest priority takes precedence.
83             return Integer.compare(o2.getPriority(), o1.getPriority());
84         }
85     }
86 
87     /**
88      * Priority list of apps which have entered or exited car mode, ordered with the highest
89      * priority app at the top of the queue.  Where items have the same priority, they are ordered
90      * by insertion time.
91      */
92     private PriorityQueue<CarModeApp> mCarModeApps = new PriorityQueue<>(2,
93             new CarModeAppComparator());
94 
95     private final LocalLog mCarModeChangeLog = new LocalLog(20);
96 
97     /**
98      * Handles a request to enter car mode by a package name.
99      * @param priority The priority at which car mode is entered.
100      * @param packageName The package name of the app entering car mode.
101      */
handleEnterCarMode(@ntRangefrom = 0) int priority, @NonNull String packageName)102     public void handleEnterCarMode(@IntRange(from = 0) int priority, @NonNull String packageName) {
103         if (mCarModeApps.stream().anyMatch(c -> c.getPriority() == priority)) {
104             Log.w(this, "handleEnterCarMode: already in car mode at priority %d (apps: %s)",
105                     priority, getCarModePriorityString());
106             return;
107         }
108 
109         if (mCarModeApps.stream().anyMatch(c -> c.getPackageName().equals(packageName))) {
110             Log.w(this, "handleEnterCarMode: %s is already in car mode (apps: %s)",
111                     packageName, getCarModePriorityString());
112             return;
113         }
114 
115         Log.i(this, "handleEnterCarMode: packageName=%s, priority=%d", packageName, priority);
116         mCarModeChangeLog.log("enterCarMode: packageName=" + packageName + ", priority="
117                 + priority);
118         mCarModeApps.add(new CarModeApp(priority, packageName));
119     }
120 
121     /**
122      * Handles a request to exist car mode at a priority level.
123      * @param priority The priority level.
124      * @param packageName The packagename of the app requesting the change.
125      */
handleExitCarMode(@ntRangefrom = 0) int priority, @NonNull String packageName)126     public void handleExitCarMode(@IntRange(from = 0) int priority, @NonNull String packageName) {
127         if (!mCarModeApps.stream().anyMatch(c -> c.getPriority() == priority)) {
128             Log.w(this, "handleExitCarMode: not in car mode at priority %d (apps=%s)",
129                     priority, getCarModePriorityString());
130             return;
131         }
132 
133         if (priority != UiModeManager.DEFAULT_PRIORITY && !mCarModeApps.stream().anyMatch(
134                 c -> c.getPackageName().equals(packageName) && c.getPriority() == priority)) {
135             Log.w(this, "handleExitCarMode: %s didn't enter car mode at priority %d (apps=%s)",
136                     packageName, priority, getCarModePriorityString());
137             return;
138         }
139 
140         Log.i(this, "handleExitCarMode: packageName=%s, priority=%d", packageName, priority);
141         mCarModeChangeLog.log("exitCarMode: packageName=" + packageName + ", priority="
142                 + priority);
143         mCarModeApps.removeIf(c -> c.getPriority() == priority);
144     }
145 
146     /**
147      * Retrieves a list of the apps which are currently in car mode, ordered by priority such that
148      * the highest priority app is first.
149      * @return List of apps in car mode.
150      */
getCarModeApps()151     public @NonNull List<String> getCarModeApps() {
152         return mCarModeApps
153                 .stream()
154                 .sorted(mCarModeApps.comparator())
155                 .map(cma -> cma.getPackageName())
156                 .collect(Collectors.toList());
157     }
158 
getCarModePriorityString()159     private @NonNull String getCarModePriorityString() {
160         return mCarModeApps
161                 .stream()
162                 .sorted(mCarModeApps.comparator())
163                 .map(cma -> "[" + cma.getPriority() + ", " + cma.getPackageName() + "]")
164                 .collect(Collectors.joining(", "));
165     }
166 
167     /**
168      * Gets the app which is currently in car mode.  This is the highest priority app which has
169      * entered car mode.
170      * @return The app which is in car mode.
171      */
getCurrentCarModePackage()172     public @Nullable String getCurrentCarModePackage() {
173         CarModeApp app = mCarModeApps.peek();
174         return app == null ? null : app.getPackageName();
175     }
176 
177     /**
178      * @return {@code true} if the device is in car mode, {@code false} otherwise.
179      */
isInCarMode()180     public boolean isInCarMode() {
181         return !mCarModeApps.isEmpty();
182     }
183 
184     /**
185      * Dumps the state of the car mode tracker to the specified print writer.
186      * @param pw
187      */
dump(IndentingPrintWriter pw)188     public void dump(IndentingPrintWriter pw) {
189         pw.println("CarModeTracker:");
190         pw.increaseIndent();
191 
192         pw.println("Current car mode apps:");
193         pw.increaseIndent();
194         for (CarModeApp app : mCarModeApps) {
195             pw.print("[");
196             pw.print(app.getPriority());
197             pw.print("] ");
198             pw.println(app.getPackageName());
199         }
200         pw.decreaseIndent();
201 
202         pw.println("Car mode history:");
203         pw.increaseIndent();
204         mCarModeChangeLog.dump(pw);
205         pw.decreaseIndent();
206 
207         pw.decreaseIndent();
208     }
209 }
210