1 /*
2  * Copyright (C) 2024 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.companion.association;
18 
19 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
20 import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
21 
22 import static com.android.internal.util.CollectionUtils.any;
23 import static com.android.server.companion.utils.RolesUtils.removeRoleHolderForAssociation;
24 
25 import static java.util.concurrent.TimeUnit.DAYS;
26 
27 import android.annotation.NonNull;
28 import android.annotation.SuppressLint;
29 import android.annotation.UserIdInt;
30 import android.app.ActivityManager;
31 import android.companion.AssociationInfo;
32 import android.content.Context;
33 import android.content.pm.PackageManagerInternal;
34 import android.os.Binder;
35 import android.os.SystemProperties;
36 import android.os.UserHandle;
37 import android.util.Slog;
38 
39 import com.android.server.companion.datatransfer.SystemDataTransferRequestStore;
40 import com.android.server.companion.devicepresence.CompanionAppBinder;
41 import com.android.server.companion.devicepresence.DevicePresenceProcessor;
42 import com.android.server.companion.transport.CompanionTransportManager;
43 
44 /**
45  * This class responsible for disassociation.
46  */
47 @SuppressLint("LongLogTag")
48 public class DisassociationProcessor {
49 
50     private static final String TAG = "CDM_DisassociationProcessor";
51 
52     private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW =
53             "debug.cdm.cdmservice.removal_time_window";
54     private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90);
55 
56     @NonNull
57     private final Context mContext;
58     @NonNull
59     private final AssociationStore mAssociationStore;
60     @NonNull
61     private final PackageManagerInternal mPackageManagerInternal;
62     @NonNull
63     private final DevicePresenceProcessor mDevicePresenceMonitor;
64     @NonNull
65     private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
66     @NonNull
67     private final CompanionAppBinder mCompanionAppController;
68     @NonNull
69     private final CompanionTransportManager mTransportManager;
70     private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener;
71     private final ActivityManager mActivityManager;
72 
DisassociationProcessor(@onNull Context context, @NonNull ActivityManager activityManager, @NonNull AssociationStore associationStore, @NonNull PackageManagerInternal packageManager, @NonNull DevicePresenceProcessor devicePresenceMonitor, @NonNull CompanionAppBinder applicationController, @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore, @NonNull CompanionTransportManager companionTransportManager)73     public DisassociationProcessor(@NonNull Context context,
74             @NonNull ActivityManager activityManager,
75             @NonNull AssociationStore associationStore,
76             @NonNull PackageManagerInternal packageManager,
77             @NonNull DevicePresenceProcessor devicePresenceMonitor,
78             @NonNull CompanionAppBinder applicationController,
79             @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore,
80             @NonNull CompanionTransportManager companionTransportManager) {
81         mContext = context;
82         mActivityManager = activityManager;
83         mAssociationStore = associationStore;
84         mPackageManagerInternal = packageManager;
85         mOnPackageVisibilityChangeListener =
86                 new OnPackageVisibilityChangeListener();
87         mDevicePresenceMonitor = devicePresenceMonitor;
88         mCompanionAppController = applicationController;
89         mSystemDataTransferRequestStore = systemDataTransferRequestStore;
90         mTransportManager = companionTransportManager;
91     }
92 
93     /**
94      * Disassociate an association by id.
95      */
96     // TODO: also revoke notification access
disassociate(int id)97     public void disassociate(int id) {
98         Slog.i(TAG, "Disassociating id=[" + id + "]...");
99 
100         final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(id);
101         final int userId = association.getUserId();
102         final String packageName = association.getPackageName();
103         final String deviceProfile = association.getDeviceProfile();
104 
105         final boolean isRoleInUseByOtherAssociations = deviceProfile != null
106                 && any(mAssociationStore.getActiveAssociationsByPackage(userId, packageName),
107                     it -> deviceProfile.equals(it.getDeviceProfile()) && id != it.getId());
108 
109         final int packageProcessImportance = getPackageProcessImportance(userId, packageName);
110         if (packageProcessImportance <= IMPORTANCE_VISIBLE && deviceProfile != null
111                 && !isRoleInUseByOtherAssociations) {
112             // Need to remove the app from the list of role holders, but the process is visible
113             // to the user at the moment, so we'll need to do it later.
114             Slog.i(TAG, "Cannot disassociate id=[" + id + "] now - process is visible. "
115                     + "Start listening to package importance...");
116 
117             AssociationInfo revokedAssociation = (new AssociationInfo.Builder(
118                     association)).setRevoked(true).build();
119             mAssociationStore.updateAssociation(revokedAssociation);
120             startListening();
121             return;
122         }
123 
124         // Detach transport if exists
125         mTransportManager.detachSystemDataTransport(id);
126 
127         // Association cleanup.
128         mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, id);
129         mAssociationStore.removeAssociation(association.getId());
130 
131         // If role is not in use by other associations, revoke the role.
132         // Do not need to remove the system role since it was pre-granted by the system.
133         if (!isRoleInUseByOtherAssociations && deviceProfile != null && !deviceProfile.equals(
134                 DEVICE_PROFILE_AUTOMOTIVE_PROJECTION)) {
135             removeRoleHolderForAssociation(mContext, association.getUserId(),
136                     association.getPackageName(), association.getDeviceProfile());
137         }
138 
139         // Unbind the app if needed.
140         final boolean wasPresent = mDevicePresenceMonitor.isDevicePresent(id);
141         if (!wasPresent || !association.isNotifyOnDeviceNearby()) {
142             return;
143         }
144         final boolean shouldStayBound = any(
145                 mAssociationStore.getActiveAssociationsByPackage(userId, packageName),
146                 it -> it.isNotifyOnDeviceNearby()
147                         && mDevicePresenceMonitor.isDevicePresent(it.getId()));
148         if (!shouldStayBound) {
149             mCompanionAppController.unbindCompanionApp(userId, packageName);
150         }
151     }
152 
153     /**
154      * @deprecated Use {@link #disassociate(int)} instead.
155      */
156     @Deprecated
disassociate(int userId, String packageName, String macAddress)157     public void disassociate(int userId, String packageName, String macAddress) {
158         AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId,
159                 packageName, macAddress);
160 
161         if (association == null) {
162             throw new IllegalArgumentException(
163                     "Association for mac address=[" + macAddress + "] doesn't exist");
164         }
165 
166         mAssociationStore.getAssociationWithCallerChecks(association.getId());
167 
168         disassociate(association.getId());
169     }
170 
171     @SuppressLint("MissingPermission")
getPackageProcessImportance(@serIdInt int userId, @NonNull String packageName)172     private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) {
173         return Binder.withCleanCallingIdentity(() -> {
174             final int uid =
175                     mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId);
176             return mActivityManager.getUidImportance(uid);
177         });
178     }
179 
startListening()180     private void startListening() {
181         Slog.i(TAG, "Start listening to uid importance changes...");
182         try {
183             Binder.withCleanCallingIdentity(
184                     () -> mActivityManager.addOnUidImportanceListener(
185                             mOnPackageVisibilityChangeListener,
186                             ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE));
187         } catch (IllegalArgumentException e) {
188             Slog.e(TAG, "Failed to start listening to uid importance changes.");
189         }
190     }
191 
stopListening()192     private void stopListening() {
193         Slog.i(TAG, "Stop listening to uid importance changes.");
194         try {
195             Binder.withCleanCallingIdentity(() -> mActivityManager.removeOnUidImportanceListener(
196                     mOnPackageVisibilityChangeListener));
197         } catch (IllegalArgumentException e) {
198             Slog.e(TAG, "Failed to stop listening to uid importance changes.");
199         }
200     }
201 
202     /**
203      * Remove idle self-managed associations.
204      */
removeIdleSelfManagedAssociations()205     public void removeIdleSelfManagedAssociations() {
206         Slog.i(TAG, "Removing idle self-managed associations.");
207 
208         final long currentTime = System.currentTimeMillis();
209         long removalWindow = SystemProperties.getLong(SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW, -1);
210         if (removalWindow <= 0) {
211             // 0 or negative values indicate that the sysprop was never set or should be ignored.
212             removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT;
213         }
214 
215         for (AssociationInfo association : mAssociationStore.getAssociations()) {
216             if (!association.isSelfManaged()) continue;
217 
218             final boolean isInactive =
219                     currentTime - association.getLastTimeConnectedMs() >= removalWindow;
220             if (!isInactive) continue;
221 
222             final int id = association.getId();
223 
224             Slog.i(TAG, "Removing inactive self-managed association=[" + association.toShortString()
225                     + "].");
226             disassociate(id);
227         }
228     }
229 
230     /**
231      * An OnUidImportanceListener class which watches the importance of the packages.
232      * In this class, we ONLY interested in the importance of the running process is greater than
233      * {@link ActivityManager.RunningAppProcessInfo#IMPORTANCE_VISIBLE}.
234      *
235      * Lastly remove the role holder for the revoked associations for the same packages.
236      *
237      * @see #disassociate(int)
238      */
239     private class OnPackageVisibilityChangeListener implements
240             ActivityManager.OnUidImportanceListener {
241 
242         @Override
onUidImportance(int uid, int importance)243         public void onUidImportance(int uid, int importance) {
244             if (importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE) {
245                 // The lower the importance value the more "important" the process is.
246                 // We are only interested when the process ceases to be visible.
247                 return;
248             }
249 
250             final String packageName = mPackageManagerInternal.getNameForUid(uid);
251             if (packageName == null) {
252                 // Not interested in this uid.
253                 return;
254             }
255 
256             int userId = UserHandle.getUserId(uid);
257             for (AssociationInfo association : mAssociationStore.getRevokedAssociations(userId,
258                     packageName)) {
259                 disassociate(association.getId());
260             }
261 
262             if (mAssociationStore.getRevokedAssociations().isEmpty()) {
263                 stopListening();
264             }
265         }
266     }
267 }
268