1 /*
2  * Copyright (C) 2022 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.pm;
18 
19 import android.app.ActivityManager;
20 import android.app.ActivityManager.RunningAppProcessInfo;
21 import android.app.ActivityManagerInternal;
22 import android.app.ActivityThread;
23 import android.app.usage.NetworkStats;
24 import android.app.usage.NetworkStatsManager;
25 import android.content.Context;
26 import android.content.pm.PackageManagerInternal;
27 import android.media.AudioManager;
28 import android.media.IAudioService;
29 import android.net.ConnectivityManager;
30 import android.os.ServiceManager;
31 import android.os.SystemProperties;
32 import android.telecom.TelecomManager;
33 import android.text.TextUtils;
34 import android.util.ArraySet;
35 
36 import com.android.internal.util.ArrayUtils;
37 import com.android.server.LocalServices;
38 
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.List;
42 import java.util.concurrent.TimeUnit;
43 
44 /**
45  * A helper class to provide queries for app states concerning gentle-update.
46  */
47 public class AppStateHelper {
48     // The duration to monitor network usage to determine if network is active or not
49     private static final long ACTIVE_NETWORK_DURATION_MILLIS = TimeUnit.MINUTES.toMillis(10);
50 
51     private final Context mContext;
52 
AppStateHelper(Context context)53     public AppStateHelper(Context context) {
54         mContext = context;
55     }
56 
57     /**
58      * True if the package is loaded into the process.
59      */
isPackageLoaded(RunningAppProcessInfo info, String packageName)60     private static boolean isPackageLoaded(RunningAppProcessInfo info, String packageName) {
61         return ArrayUtils.contains(info.pkgList, packageName)
62                 || ArrayUtils.contains(info.pkgDeps, packageName);
63     }
64 
65     /**
66      * Returns the importance of the given package.
67      */
getImportance(String packageName)68     private int getImportance(String packageName) {
69         var am = mContext.getSystemService(ActivityManager.class);
70         return am.getPackageImportance(packageName);
71     }
72 
73     /**
74      * True if the app owns the audio focus.
75      */
hasAudioFocus(String packageName)76     private boolean hasAudioFocus(String packageName) {
77         var audioService = IAudioService.Stub.asInterface(
78                 ServiceManager.getService(Context.AUDIO_SERVICE));
79         try {
80             var focusInfos = audioService.getFocusStack();
81             int size = focusInfos.size();
82             var audioFocusPackage = (size > 0) ? focusInfos.get(size - 1).getPackageName() : null;
83             return TextUtils.equals(packageName, audioFocusPackage);
84         } catch (Exception ignore) {
85         }
86         return false;
87     }
88 
89     /**
90      * True if any app is using voice communication.
91      */
hasVoiceCall()92     private boolean hasVoiceCall() {
93         var am = mContext.getSystemService(AudioManager.class);
94         try {
95             int audioMode = am.getMode();
96             return audioMode == AudioManager.MODE_IN_CALL
97                     || audioMode == AudioManager.MODE_IN_COMMUNICATION;
98         } catch (Exception ignore) {
99             return false;
100         }
101     }
102 
103     /**
104      * True if the app is recording audio.
105      */
isRecordingAudio(String packageName)106     private boolean isRecordingAudio(String packageName) {
107         var am = mContext.getSystemService(AudioManager.class);
108         try {
109             for (var arc : am.getActiveRecordingConfigurations()) {
110                 if (TextUtils.equals(arc.getClientPackageName(), packageName)) {
111                     return true;
112                 }
113             }
114         } catch (Exception ignore) {
115         }
116         return false;
117     }
118 
119     /**
120      * True if the app is in the foreground.
121      */
isAppForeground(String packageName)122     private boolean isAppForeground(String packageName) {
123         return getImportance(packageName) <= RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
124     }
125 
126     /**
127      * True if the app is currently at the top of the screen that the user is interacting with.
128      */
isAppTopVisible(String packageName)129     public boolean isAppTopVisible(String packageName) {
130         return getImportance(packageName) <= RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
131     }
132 
133     /**
134      * True if the app is playing/recording audio.
135      */
hasActiveAudio(String packageName)136     private boolean hasActiveAudio(String packageName) {
137         return hasAudioFocus(packageName) || isRecordingAudio(packageName);
138     }
139 
hasActiveNetwork(List<String> packageNames, int networkType)140     private boolean hasActiveNetwork(List<String> packageNames, int networkType) {
141         var pm = ActivityThread.getPackageManager();
142         var nsm = mContext.getSystemService(NetworkStatsManager.class);
143         var endTime = System.currentTimeMillis();
144         var startTime = endTime - ACTIVE_NETWORK_DURATION_MILLIS;
145         try (var stats = nsm.querySummary(networkType, null, startTime, endTime)) {
146             var bucket = new NetworkStats.Bucket();
147             while (stats.hasNextBucket()) {
148                 stats.getNextBucket(bucket);
149                 var packageName = pm.getNameForUid(bucket.getUid());
150                 if (!packageNames.contains(packageName)) {
151                     continue;
152                 }
153                 if (bucket.getRxPackets() > 0 || bucket.getTxPackets() > 0) {
154                     return true;
155                 }
156             }
157         } catch (Exception ignore) {
158         }
159         return false;
160     }
161 
162     /**
163      * True if {@code arr} contains any element in {@code which}.
164      * Both {@code arr} and {@code which} must be sorted in advance.
165      */
containsAny(String[] arr, List<String> which)166     private static boolean containsAny(String[] arr, List<String> which) {
167         int s1 = arr.length;
168         int s2 = which.size();
169         for (int i = 0, j = 0; i < s1 && j < s2; ) {
170             int val = arr[i].compareTo(which.get(j));
171             if (val == 0) {
172                 return true;
173             } else if (val < 0) {
174                 ++i;
175             } else {
176                 ++j;
177             }
178         }
179         return false;
180     }
181 
addLibraryDependency(ArraySet<String> results, List<String> libPackageNames)182     private void addLibraryDependency(ArraySet<String> results, List<String> libPackageNames) {
183         var pmInternal = LocalServices.getService(PackageManagerInternal.class);
184 
185         var libraryNames = new ArrayList<String>();
186         var staticSharedLibraryNames = new ArrayList<String>();
187         var sdkLibraryNames = new ArrayList<String>();
188         for (var packageName : libPackageNames) {
189             var pkg = pmInternal.getAndroidPackage(packageName);
190             if (pkg == null) {
191                 continue;
192             }
193             libraryNames.addAll(pkg.getLibraryNames());
194             var libraryName = pkg.getStaticSharedLibraryName();
195             if (libraryName != null) {
196                 staticSharedLibraryNames.add(libraryName);
197             }
198             libraryName = pkg.getSdkLibraryName();
199             if (libraryName != null) {
200                 sdkLibraryNames.add(libraryName);
201             }
202         }
203 
204         if (libraryNames.isEmpty()
205                 && staticSharedLibraryNames.isEmpty()
206                 && sdkLibraryNames.isEmpty()) {
207             return;
208         }
209 
210         Collections.sort(libraryNames);
211         Collections.sort(sdkLibraryNames);
212         Collections.sort(staticSharedLibraryNames);
213 
214         pmInternal.forEachPackageState(pkgState -> {
215             var pkg = pkgState.getPkg();
216             if (pkg == null) {
217                 return;
218             }
219             if (containsAny(pkg.getUsesLibrariesSorted(), libraryNames)
220                     || containsAny(pkg.getUsesOptionalLibrariesSorted(), libraryNames)
221                     || containsAny(pkg.getUsesStaticLibrariesSorted(), staticSharedLibraryNames)
222                     || containsAny(pkg.getUsesSdkLibrariesSorted(), sdkLibraryNames)) {
223                 results.add(pkg.getPackageName());
224             }
225         });
226     }
227 
228     /**
229      * True if any app has sent or received network data over the past
230      * {@link #ACTIVE_NETWORK_DURATION_MILLIS} milliseconds.
231      */
hasActiveNetwork(List<String> packageNames)232     private boolean hasActiveNetwork(List<String> packageNames) {
233         return hasActiveNetwork(packageNames, ConnectivityManager.TYPE_WIFI)
234                 || hasActiveNetwork(packageNames, ConnectivityManager.TYPE_MOBILE);
235     }
236 
237     /**
238      * True if any app is interacting with the user.
239      */
hasInteractingApp(List<String> packageNames)240     public boolean hasInteractingApp(List<String> packageNames) {
241         for (var packageName : packageNames) {
242             if (hasActiveAudio(packageName)
243                     || isAppTopVisible(packageName)) {
244                 return true;
245             }
246         }
247         return hasActiveNetwork(packageNames);
248     }
249 
250     /**
251      * True if any app is in the foreground.
252      */
hasForegroundApp(List<String> packageNames)253     public boolean hasForegroundApp(List<String> packageNames) {
254         for (var packageName : packageNames) {
255             if (isAppForeground(packageName)) {
256                 return true;
257             }
258         }
259         return false;
260     }
261 
262     /**
263      * True if any app is top visible.
264      */
hasTopVisibleApp(List<String> packageNames)265     public boolean hasTopVisibleApp(List<String> packageNames) {
266         for (var packageName : packageNames) {
267             if (isAppTopVisible(packageName)) {
268                 return true;
269             }
270         }
271         return false;
272     }
273 
274     /**
275      * True if there is an ongoing phone call.
276      */
isInCall()277     public boolean isInCall() {
278         // Simulate in-call during test
279         if (SystemProperties.getBoolean(
280                 "debug.pm.gentle_update_test.is_in_call", false)) {
281             return true;
282         }
283         // TelecomManager doesn't handle the case where some apps don't implement ConnectionService.
284         // We check apps using voice communication to detect if the device is in call.
285         var tm = mContext.getSystemService(TelecomManager.class);
286         return tm.isInCall() || hasVoiceCall();
287     }
288 
289     /**
290      * Returns a list of packages which depend on {@code packageNames}. These are the packages
291      * that will be affected when updating {@code packageNames} and should participate in
292      * the evaluation of install constraints.
293      */
getDependencyPackages(List<String> packageNames)294     public List<String> getDependencyPackages(List<String> packageNames) {
295         var results = new ArraySet<String>();
296         // Include packages sharing the same process
297         var am = mContext.getSystemService(ActivityManager.class);
298         for (var info : am.getRunningAppProcesses()) {
299             for (var packageName : packageNames) {
300                 if (!isPackageLoaded(info, packageName)) {
301                     continue;
302                 }
303                 for (var pkg : info.pkgList) {
304                     results.add(pkg);
305                 }
306             }
307         }
308         // Include packages using bounded services
309         var amInternal = LocalServices.getService(ActivityManagerInternal.class);
310         for (var packageName : packageNames) {
311             results.addAll(amInternal.getClientPackages(packageName));
312         }
313         // Include packages using libraries
314         addLibraryDependency(results, packageNames);
315 
316         return new ArrayList<>(results);
317     }
318 }
319