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.companion;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.SuppressLint;
22 import android.annotation.UserIdInt;
23 import android.companion.AssociationInfo;
24 import android.companion.CompanionDeviceService;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.os.Handler;
28 import android.util.Log;
29 import android.util.Slog;
30 import android.util.SparseArray;
31 
32 import com.android.internal.annotations.GuardedBy;
33 import com.android.internal.infra.PerUser;
34 import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
35 
36 import java.io.PrintWriter;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.List;
41 import java.util.Map;
42 
43 /**
44  * Manages communication with companion applications via
45  * {@link android.companion.ICompanionDeviceService} interface, including "connecting" (binding) to
46  * the services, maintaining the connection (the binding), and invoking callback methods such as
47  * {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)} and
48  * {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} in the application process.
49  *
50  * <p>
51  * The following is the list of the APIs provided by {@link CompanionApplicationController} (to be
52  * utilized by {@link CompanionDeviceManagerService}):
53  * <ul>
54  * <li> {@link #bindCompanionApplication(int, String, boolean)}
55  * <li> {@link #unbindCompanionApplication(int, String)}
56  * <li> {@link #notifyCompanionApplicationDeviceAppeared(AssociationInfo)}
57  * <li> {@link #notifyCompanionApplicationDeviceDisappeared(AssociationInfo)}
58  * <li> {@link #isCompanionApplicationBound(int, String)}
59  * <li> {@link #isRebindingCompanionApplicationScheduled(int, String)}
60  * </ul>
61  *
62  * @see CompanionDeviceService
63  * @see android.companion.ICompanionDeviceService
64  * @see CompanionDeviceServiceConnector
65  */
66 @SuppressLint("LongLogTag")
67 public class CompanionApplicationController {
68     static final boolean DEBUG = false;
69     private static final String TAG = "CDM_CompanionApplicationController";
70 
71     private static final long REBIND_TIMEOUT = 10 * 1000; // 10 sec
72 
73     private final @NonNull Context mContext;
74     private final @NonNull AssociationStore mAssociationStore;
75     private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor;
76     private final @NonNull CompanionServicesRegister mCompanionServicesRegister;
77 
78     @GuardedBy("mBoundCompanionApplications")
79     private final @NonNull AndroidPackageMap<List<CompanionDeviceServiceConnector>>
80             mBoundCompanionApplications;
81     @GuardedBy("mScheduledForRebindingCompanionApplications")
82     private final @NonNull AndroidPackageMap<Boolean> mScheduledForRebindingCompanionApplications;
83 
CompanionApplicationController(Context context, AssociationStore associationStore, CompanionDevicePresenceMonitor companionDevicePresenceMonitor)84     CompanionApplicationController(Context context, AssociationStore associationStore,
85             CompanionDevicePresenceMonitor companionDevicePresenceMonitor) {
86         mContext = context;
87         mAssociationStore = associationStore;
88         mDevicePresenceMonitor = companionDevicePresenceMonitor;
89         mCompanionServicesRegister = new CompanionServicesRegister();
90         mBoundCompanionApplications = new AndroidPackageMap<>();
91         mScheduledForRebindingCompanionApplications = new AndroidPackageMap<>();
92     }
93 
onPackagesChanged(@serIdInt int userId)94     void onPackagesChanged(@UserIdInt int userId) {
95         mCompanionServicesRegister.invalidate(userId);
96     }
97 
98     /**
99      * CDM binds to the companion app.
100      */
bindCompanionApplication(@serIdInt int userId, @NonNull String packageName, boolean isSelfManaged)101     public void bindCompanionApplication(@UserIdInt int userId, @NonNull String packageName,
102             boolean isSelfManaged) {
103         if (DEBUG) {
104             Log.i(TAG, "bind() u" + userId + "/" + packageName
105                     + " isSelfManaged=" + isSelfManaged);
106         }
107 
108         final List<ComponentName> companionServices =
109                 mCompanionServicesRegister.forPackage(userId, packageName);
110         if (companionServices.isEmpty()) {
111             Slog.w(TAG, "Can not bind companion applications u" + userId + "/" + packageName + ": "
112                     + "eligible CompanionDeviceService not found.\n"
113                     + "A CompanionDeviceService should declare an intent-filter for "
114                     + "\"android.companion.CompanionDeviceService\" action and require "
115                     + "\"android.permission.BIND_COMPANION_DEVICE_SERVICE\" permission.");
116             return;
117         }
118 
119         final List<CompanionDeviceServiceConnector> serviceConnectors = new ArrayList<>();
120         synchronized (mBoundCompanionApplications) {
121             if (mBoundCompanionApplications.containsValueForPackage(userId, packageName)) {
122                 if (DEBUG) Log.e(TAG, "u" + userId + "/" + packageName + " is ALREADY bound.");
123                 return;
124             }
125 
126             for (int i = 0; i < companionServices.size(); i++) {
127                 boolean isPrimary = i == 0;
128                 serviceConnectors.add(CompanionDeviceServiceConnector.newInstance(mContext, userId,
129                         companionServices.get(i), isSelfManaged, isPrimary));
130             }
131 
132             mBoundCompanionApplications.setValueForPackage(userId, packageName, serviceConnectors);
133         }
134 
135         // Set listeners for both Primary and Secondary connectors.
136         for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) {
137             serviceConnector.setListener(this::onBinderDied);
138         }
139 
140         // Now "bind" all the connectors: the primary one and the rest of them.
141         for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) {
142             serviceConnector.connect();
143         }
144     }
145 
146     /**
147      * CDM unbinds the companion app.
148      */
unbindCompanionApplication(@serIdInt int userId, @NonNull String packageName)149     public void unbindCompanionApplication(@UserIdInt int userId, @NonNull String packageName) {
150         if (DEBUG) Log.i(TAG, "unbind() u" + userId + "/" + packageName);
151 
152         final List<CompanionDeviceServiceConnector> serviceConnectors;
153 
154         synchronized (mBoundCompanionApplications) {
155             serviceConnectors = mBoundCompanionApplications.removePackage(userId, packageName);
156         }
157 
158         synchronized (mScheduledForRebindingCompanionApplications) {
159             mScheduledForRebindingCompanionApplications.removePackage(userId, packageName);
160         }
161 
162         if (serviceConnectors == null) {
163             if (DEBUG) {
164                 Log.e(TAG, "unbindCompanionApplication(): "
165                         + "u" + userId + "/" + packageName + " is NOT bound");
166                 Log.d(TAG, "Stacktrace", new Throwable());
167             }
168             return;
169         }
170 
171         for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) {
172             serviceConnector.postUnbind();
173         }
174     }
175 
176     /**
177      * @return whether the companion application is bound now.
178      */
isCompanionApplicationBound(@serIdInt int userId, @NonNull String packageName)179     public boolean isCompanionApplicationBound(@UserIdInt int userId, @NonNull String packageName) {
180         synchronized (mBoundCompanionApplications) {
181             return mBoundCompanionApplications.containsValueForPackage(userId, packageName);
182         }
183     }
184 
scheduleRebinding(@serIdInt int userId, @NonNull String packageName, CompanionDeviceServiceConnector serviceConnector)185     private void scheduleRebinding(@UserIdInt int userId, @NonNull String packageName,
186             CompanionDeviceServiceConnector serviceConnector) {
187         Slog.i(TAG, "scheduleRebinding() " + userId + "/" + packageName);
188 
189         if (isRebindingCompanionApplicationScheduled(userId, packageName)) {
190             if (DEBUG) {
191                 Log.i(TAG, "CompanionApplication rebinding has been scheduled, skipping "
192                         + serviceConnector.getComponentName());
193             }
194             return;
195         }
196 
197         if (serviceConnector.isPrimary()) {
198             synchronized (mScheduledForRebindingCompanionApplications) {
199                 mScheduledForRebindingCompanionApplications.setValueForPackage(
200                         userId, packageName, true);
201             }
202         }
203 
204         // Rebinding in 10 seconds.
205         Handler.getMain().postDelayed(() ->
206                 onRebindingCompanionApplicationTimeout(userId, packageName, serviceConnector),
207                 REBIND_TIMEOUT);
208     }
209 
isRebindingCompanionApplicationScheduled( @serIdInt int userId, @NonNull String packageName)210     private boolean isRebindingCompanionApplicationScheduled(
211             @UserIdInt int userId, @NonNull String packageName) {
212         synchronized (mScheduledForRebindingCompanionApplications) {
213             return mScheduledForRebindingCompanionApplications.containsValueForPackage(
214                     userId, packageName);
215         }
216     }
217 
onRebindingCompanionApplicationTimeout( @serIdInt int userId, @NonNull String packageName, @NonNull CompanionDeviceServiceConnector serviceConnector)218     private void onRebindingCompanionApplicationTimeout(
219             @UserIdInt int userId, @NonNull String packageName,
220             @NonNull CompanionDeviceServiceConnector serviceConnector) {
221         // Re-mark the application is bound.
222         if (serviceConnector.isPrimary()) {
223             synchronized (mBoundCompanionApplications) {
224                 if (!mBoundCompanionApplications.containsValueForPackage(userId, packageName)) {
225                     List<CompanionDeviceServiceConnector> serviceConnectors =
226                             Collections.singletonList(serviceConnector);
227                     mBoundCompanionApplications.setValueForPackage(userId, packageName,
228                             serviceConnectors);
229                 }
230             }
231 
232             synchronized (mScheduledForRebindingCompanionApplications) {
233                 mScheduledForRebindingCompanionApplications.removePackage(userId, packageName);
234             }
235         }
236 
237         serviceConnector.connect();
238     }
239 
notifyCompanionApplicationDeviceAppeared(AssociationInfo association)240     void notifyCompanionApplicationDeviceAppeared(AssociationInfo association) {
241         final int userId = association.getUserId();
242         final String packageName = association.getPackageName();
243         if (DEBUG) {
244             Log.i(TAG, "notifyDevice_Appeared() id=" + association.getId() + " u" + userId
245                     + "/" + packageName);
246         }
247 
248         final CompanionDeviceServiceConnector primaryServiceConnector =
249                 getPrimaryServiceConnector(userId, packageName);
250         if (primaryServiceConnector == null) {
251             if (DEBUG) {
252                 Log.e(TAG, "notify_CompanionApplicationDevice_Appeared(): "
253                         + "u" + userId + "/" + packageName + " is NOT bound.");
254                 Log.d(TAG, "Stacktrace", new Throwable());
255             }
256             return;
257         }
258 
259         Log.i(TAG, "Calling onDeviceAppeared to userId=[" + userId + "] package=["
260                 + packageName + "] associationId=[" + association.getId() + "]");
261 
262         primaryServiceConnector.postOnDeviceAppeared(association);
263     }
264 
notifyCompanionApplicationDeviceDisappeared(AssociationInfo association)265     void notifyCompanionApplicationDeviceDisappeared(AssociationInfo association) {
266         final int userId = association.getUserId();
267         final String packageName = association.getPackageName();
268         if (DEBUG) {
269             Log.i(TAG, "notifyDevice_Disappeared() id=" + association.getId() + " u" + userId
270                     + "/" + packageName);
271         }
272 
273         final CompanionDeviceServiceConnector primaryServiceConnector =
274                 getPrimaryServiceConnector(userId, packageName);
275         if (primaryServiceConnector == null) {
276             if (DEBUG) {
277                 Log.e(TAG, "notify_CompanionApplicationDevice_Disappeared(): "
278                         + "u" + userId + "/" + packageName + " is NOT bound.");
279                 Log.d(TAG, "Stacktrace", new Throwable());
280             }
281             return;
282         }
283 
284         Log.i(TAG, "Calling onDeviceDisappeared to userId=[" + userId + "] package=["
285                 + packageName + "] associationId=[" + association.getId() + "]");
286 
287         primaryServiceConnector.postOnDeviceDisappeared(association);
288     }
289 
dump(@onNull PrintWriter out)290     void dump(@NonNull PrintWriter out) {
291         out.append("Companion Device Application Controller: \n");
292 
293         synchronized (mBoundCompanionApplications) {
294             out.append("  Bound Companion Applications: ");
295             if (mBoundCompanionApplications.size() == 0) {
296                 out.append("<empty>\n");
297             } else {
298                 out.append("\n");
299                 mBoundCompanionApplications.dump(out);
300             }
301         }
302 
303         out.append("  Companion Applications Scheduled For Rebinding: ");
304         if (mScheduledForRebindingCompanionApplications.size() == 0) {
305             out.append("<empty>\n");
306         } else {
307             out.append("\n");
308             mScheduledForRebindingCompanionApplications.dump(out);
309         }
310     }
311 
312     /**
313      * Rebinding for Self-Managed secondary services OR Non-Self-Managed services.
314      */
onBinderDied(@serIdInt int userId, @NonNull String packageName, @NonNull CompanionDeviceServiceConnector serviceConnector)315     private void onBinderDied(@UserIdInt int userId, @NonNull String packageName,
316             @NonNull CompanionDeviceServiceConnector serviceConnector) {
317 
318         boolean isPrimary = serviceConnector.isPrimary();
319         Slog.i(TAG, "onBinderDied() u" + userId + "/" + packageName + " isPrimary: " + isPrimary);
320 
321         // First: Only mark not BOUND for primary service.
322         synchronized (mBoundCompanionApplications) {
323             if (serviceConnector.isPrimary()) {
324                 mBoundCompanionApplications.removePackage(userId, packageName);
325             }
326         }
327 
328         // Second: schedule rebinding if needed.
329         final boolean shouldScheduleRebind = shouldScheduleRebind(userId, packageName, isPrimary);
330 
331         if (shouldScheduleRebind) {
332             scheduleRebinding(userId, packageName, serviceConnector);
333         }
334     }
335 
getPrimaryServiceConnector( @serIdInt int userId, @NonNull String packageName)336     private @Nullable CompanionDeviceServiceConnector getPrimaryServiceConnector(
337             @UserIdInt int userId, @NonNull String packageName) {
338         final List<CompanionDeviceServiceConnector> connectors;
339         synchronized (mBoundCompanionApplications) {
340             connectors = mBoundCompanionApplications.getValueForPackage(userId, packageName);
341         }
342         return connectors != null ? connectors.get(0) : null;
343     }
344 
shouldScheduleRebind(int userId, String packageName, boolean isPrimary)345     private boolean shouldScheduleRebind(int userId, String packageName, boolean isPrimary) {
346         // Make sure do not schedule rebind for the case ServiceConnector still gets callback after
347         // app is uninstalled.
348         boolean stillAssociated = false;
349         // Make sure to clean up the state for all the associations
350         // that associate with this package.
351         boolean shouldScheduleRebind = false;
352 
353         for (AssociationInfo ai :
354                 mAssociationStore.getAssociationsForPackage(userId, packageName)) {
355             final int associationId = ai.getId();
356             stillAssociated = true;
357             if (ai.isSelfManaged()) {
358                 // Do not rebind if primary one is died for selfManaged application.
359                 if (isPrimary
360                         && mDevicePresenceMonitor.isDevicePresent(associationId)) {
361                     mDevicePresenceMonitor.onSelfManagedDeviceReporterBinderDied(associationId);
362                     shouldScheduleRebind = false;
363                 }
364                 // Do not rebind if both primary and secondary services are died for
365                 // selfManaged application.
366                 shouldScheduleRebind = isCompanionApplicationBound(userId, packageName);
367             } else if (ai.isNotifyOnDeviceNearby()) {
368                 // Always rebind for non-selfManaged devices.
369                 shouldScheduleRebind = true;
370             }
371         }
372 
373         return stillAssociated && shouldScheduleRebind;
374     }
375 
376     private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> {
377         @Override
forUser( @serIdInt int userId)378         public synchronized @NonNull Map<String, List<ComponentName>> forUser(
379                 @UserIdInt int userId) {
380             return super.forUser(userId);
381         }
382 
forPackage( @serIdInt int userId, @NonNull String packageName)383         synchronized @NonNull List<ComponentName> forPackage(
384                 @UserIdInt int userId, @NonNull String packageName) {
385             return forUser(userId).getOrDefault(packageName, Collections.emptyList());
386         }
387 
invalidate(@serIdInt int userId)388         synchronized void invalidate(@UserIdInt int userId) {
389             remove(userId);
390         }
391 
392         @Override
create(@serIdInt int userId)393         protected final @NonNull Map<String, List<ComponentName>> create(@UserIdInt int userId) {
394             return PackageUtils.getCompanionServicesForUser(mContext, userId);
395         }
396     }
397 
398     /**
399      * Associates an Android package (defined by userId + packageName) with a value of type T.
400      */
401     private static class AndroidPackageMap<T> extends SparseArray<Map<String, T>> {
402 
setValueForPackage( @serIdInt int userId, @NonNull String packageName, @NonNull T value)403         void setValueForPackage(
404                 @UserIdInt int userId, @NonNull String packageName, @NonNull T value) {
405             Map<String, T> forUser = get(userId);
406             if (forUser == null) {
407                 forUser = /* Map<String, T> */ new HashMap();
408                 put(userId, forUser);
409             }
410 
411             forUser.put(packageName, value);
412         }
413 
containsValueForPackage(@serIdInt int userId, @NonNull String packageName)414         boolean containsValueForPackage(@UserIdInt int userId, @NonNull String packageName) {
415             final Map<String, ?> forUser = get(userId);
416             return forUser != null && forUser.containsKey(packageName);
417         }
418 
getValueForPackage(@serIdInt int userId, @NonNull String packageName)419         T getValueForPackage(@UserIdInt int userId, @NonNull String packageName) {
420             final Map<String, T> forUser = get(userId);
421             return forUser != null ? forUser.get(packageName) : null;
422         }
423 
removePackage(@serIdInt int userId, @NonNull String packageName)424         T removePackage(@UserIdInt int userId, @NonNull String packageName) {
425             final Map<String, T> forUser = get(userId);
426             if (forUser == null) return null;
427             return forUser.remove(packageName);
428         }
429 
dump()430         void dump() {
431             if (size() == 0) {
432                 Log.d(TAG, "<empty>");
433                 return;
434             }
435 
436             for (int i = 0; i < size(); i++) {
437                 final int userId = keyAt(i);
438                 final Map<String, T> forUser = get(userId);
439                 if (forUser.isEmpty()) {
440                     Log.d(TAG, "u" + userId + ": <empty>");
441                 }
442 
443                 for (Map.Entry<String, T> packageValue : forUser.entrySet()) {
444                     final String packageName = packageValue.getKey();
445                     final T value = packageValue.getValue();
446                     Log.d(TAG, "u" + userId + "\\" + packageName + " -> " + value);
447                 }
448             }
449         }
450 
dump(@onNull PrintWriter out)451         private void dump(@NonNull PrintWriter out) {
452             for (int i = 0; i < size(); i++) {
453                 final int userId = keyAt(i);
454                 final Map<String, T> forUser = get(userId);
455                 if (forUser.isEmpty()) {
456                     out.append("    u").append(String.valueOf(userId)).append(": <empty>\n");
457                 }
458 
459                 for (Map.Entry<String, T> packageValue : forUser.entrySet()) {
460                     final String packageName = packageValue.getKey();
461                     final T value = packageValue.getValue();
462                     out.append("    u").append(String.valueOf(userId)).append("\\")
463                             .append(packageName).append(" -> ")
464                             .append(value.toString()).append('\n');
465                 }
466             }
467         }
468     }
469 }
470