1 /*
2  * Copyright (C) 2021 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.companion.AssociationInfo;
20 import android.companion.ContextSyncMessage;
21 import android.companion.Telecom;
22 import android.companion.datatransfer.PermissionSyncRequest;
23 import android.net.MacAddress;
24 import android.os.Binder;
25 import android.os.ShellCommand;
26 import android.util.proto.ProtoOutputStream;
27 
28 import com.android.server.companion.datatransfer.SystemDataTransferProcessor;
29 import com.android.server.companion.datatransfer.contextsync.BitmapUtils;
30 import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController;
31 import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
32 import com.android.server.companion.transport.CompanionTransportManager;
33 import com.android.server.companion.transport.Transport;
34 
35 import java.io.PrintWriter;
36 import java.util.List;
37 
38 class CompanionDeviceShellCommand extends ShellCommand {
39     private static final String TAG = "CDM_CompanionDeviceShellCommand";
40 
41     private final CompanionDeviceManagerService mService;
42     private final AssociationStoreImpl mAssociationStore;
43     private final CompanionDevicePresenceMonitor mDevicePresenceMonitor;
44     private final CompanionTransportManager mTransportManager;
45 
46     private final SystemDataTransferProcessor mSystemDataTransferProcessor;
47     private final AssociationRequestsProcessor mAssociationRequestsProcessor;
48 
CompanionDeviceShellCommand(CompanionDeviceManagerService service, AssociationStoreImpl associationStore, CompanionDevicePresenceMonitor devicePresenceMonitor, CompanionTransportManager transportManager, SystemDataTransferProcessor systemDataTransferProcessor, AssociationRequestsProcessor associationRequestsProcessor)49     CompanionDeviceShellCommand(CompanionDeviceManagerService service,
50             AssociationStoreImpl associationStore,
51             CompanionDevicePresenceMonitor devicePresenceMonitor,
52             CompanionTransportManager transportManager,
53             SystemDataTransferProcessor systemDataTransferProcessor,
54             AssociationRequestsProcessor associationRequestsProcessor) {
55         mService = service;
56         mAssociationStore = associationStore;
57         mDevicePresenceMonitor = devicePresenceMonitor;
58         mTransportManager = transportManager;
59         mSystemDataTransferProcessor = systemDataTransferProcessor;
60         mAssociationRequestsProcessor = associationRequestsProcessor;
61     }
62 
63     @Override
onCommand(String cmd)64     public int onCommand(String cmd) {
65         final PrintWriter out = getOutPrintWriter();
66         final int associationId;
67         try {
68             switch (cmd) {
69                 case "list": {
70                     final int userId = getNextIntArgRequired();
71                     final List<AssociationInfo> associationsForUser =
72                             mAssociationStore.getAssociationsForUser(userId);
73                     for (AssociationInfo association : associationsForUser) {
74                         // TODO(b/212535524): use AssociationInfo.toShortString(), once it's not
75                         //  longer referenced in tests.
76                         out.println(association.getPackageName() + " "
77                                 + association.getDeviceMacAddress() + " " + association.getId());
78                     }
79                 }
80                 break;
81 
82                 case "associate": {
83                     int userId = getNextIntArgRequired();
84                     String packageName = getNextArgRequired();
85                     String address = getNextArgRequired();
86                     final MacAddress macAddress = MacAddress.fromString(address);
87                     mService.createNewAssociation(userId, packageName, macAddress,
88                             null, null, false);
89                 }
90                 break;
91 
92                 case "disassociate": {
93                     final int userId = getNextIntArgRequired();
94                     final String packageName = getNextArgRequired();
95                     final String address = getNextArgRequired();
96                     final AssociationInfo association =
97                             mService.getAssociationWithCallerChecks(userId, packageName, address);
98                     if (association != null) {
99                         mService.disassociateInternal(association.getId());
100                     }
101                 }
102                 break;
103 
104                 case "clear-association-memory-cache":
105                     mService.persistState();
106                     mService.loadAssociationsFromDisk();
107                     break;
108 
109                 case "simulate-device-appeared":
110                     associationId = getNextIntArgRequired();
111                     mDevicePresenceMonitor.simulateDeviceAppeared(associationId);
112                     break;
113 
114                 case "simulate-device-disappeared":
115                     associationId = getNextIntArgRequired();
116                     mDevicePresenceMonitor.simulateDeviceDisappeared(associationId);
117                     break;
118 
119                 case "remove-inactive-associations": {
120                     // This command should trigger the same "clean-up" job as performed by the
121                     // InactiveAssociationsRemovalService JobService. However, since the
122                     // InactiveAssociationsRemovalService run as system, we want to run this
123                     // as system (not as shell/root) as well.
124                     Binder.withCleanCallingIdentity(
125                             mService::removeInactiveSelfManagedAssociations);
126                 }
127                 break;
128 
129                 case "create-emulated-transport":
130                     // This command creates a RawTransport in order to test Transport listeners
131                     associationId = getNextIntArgRequired();
132                     mTransportManager.createEmulatedTransport(associationId);
133                     break;
134 
135                 case "send-context-sync-empty-message": {
136                     associationId = getNextIntArgRequired();
137                     mTransportManager.createEmulatedTransport(associationId)
138                             .processMessage(Transport.MESSAGE_REQUEST_CONTEXT_SYNC,
139                                     /* sequence= */ 0,
140                                     CrossDeviceSyncController.createEmptyMessage());
141                     break;
142                 }
143 
144                 case "send-context-sync-call-create-message": {
145                     associationId = getNextIntArgRequired();
146                     String callId = getNextArgRequired();
147                     String address = getNextArgRequired();
148                     String facilitator = getNextArgRequired();
149                     mTransportManager.createEmulatedTransport(associationId)
150                             .processMessage(Transport.MESSAGE_REQUEST_CONTEXT_SYNC,
151                                     /* sequence= */ 0,
152                                     CrossDeviceSyncController.createCallCreateMessage(callId,
153                                             address, facilitator));
154                     break;
155                 }
156 
157                 case "send-context-sync-call-control-message": {
158                     associationId = getNextIntArgRequired();
159                     String callId = getNextArgRequired();
160                     int control = getNextIntArgRequired();
161                     mTransportManager.createEmulatedTransport(associationId)
162                             .processMessage(Transport.MESSAGE_REQUEST_CONTEXT_SYNC,
163                                     /* sequence= */ 0,
164                                     CrossDeviceSyncController.createCallControlMessage(callId,
165                                             control));
166                     break;
167                 }
168 
169                 case "send-context-sync-call-facilitators-message": {
170                     associationId = getNextIntArgRequired();
171                     int numberOfFacilitators = getNextIntArgRequired();
172                     String facilitatorName = getNextArgRequired();
173                     String facilitatorId = getNextArgRequired();
174                     final ProtoOutputStream pos = new ProtoOutputStream();
175                     pos.write(ContextSyncMessage.VERSION, 1);
176                     final long telecomToken = pos.start(ContextSyncMessage.TELECOM);
177                     for (int i = 0; i < numberOfFacilitators; i++) {
178                         final long facilitatorsToken = pos.start(Telecom.FACILITATORS);
179                         pos.write(Telecom.CallFacilitator.NAME,
180                                 numberOfFacilitators == 1 ? facilitatorName : facilitatorName + i);
181                         pos.write(Telecom.CallFacilitator.IDENTIFIER,
182                                 numberOfFacilitators == 1 ? facilitatorId : facilitatorId + i);
183                         pos.end(facilitatorsToken);
184                     }
185                     pos.end(telecomToken);
186                     mTransportManager.createEmulatedTransport(associationId)
187                             .processMessage(Transport.MESSAGE_REQUEST_CONTEXT_SYNC,
188                                     /* sequence= */ 0, pos.getBytes());
189                     break;
190                 }
191 
192                 case "send-context-sync-call-message": {
193                     associationId = getNextIntArgRequired();
194                     String callId = getNextArgRequired();
195                     String facilitatorId = getNextArgRequired();
196                     int status = getNextIntArgRequired();
197                     boolean acceptControl = getNextBooleanArgRequired();
198                     boolean rejectControl = getNextBooleanArgRequired();
199                     boolean silenceControl = getNextBooleanArgRequired();
200                     boolean muteControl = getNextBooleanArgRequired();
201                     boolean unmuteControl = getNextBooleanArgRequired();
202                     boolean endControl = getNextBooleanArgRequired();
203                     boolean holdControl = getNextBooleanArgRequired();
204                     boolean unholdControl = getNextBooleanArgRequired();
205                     final ProtoOutputStream pos = new ProtoOutputStream();
206                     pos.write(ContextSyncMessage.VERSION, 1);
207                     final long telecomToken = pos.start(ContextSyncMessage.TELECOM);
208                     final long callsToken = pos.start(Telecom.CALLS);
209                     pos.write(Telecom.Call.ID, callId);
210                     final long originToken = pos.start(Telecom.Call.ORIGIN);
211                     pos.write(Telecom.Call.Origin.CALLER_ID, "Caller Name");
212                     pos.write(Telecom.Call.Origin.APP_ICON, BitmapUtils.renderDrawableToByteArray(
213                             mService.getContext().getPackageManager().getApplicationIcon(
214                                     facilitatorId)));
215                     final long facilitatorToken = pos.start(
216                             Telecom.Request.CreateAction.FACILITATOR);
217                     pos.write(Telecom.CallFacilitator.NAME, "Test App Name");
218                     pos.write(Telecom.CallFacilitator.IDENTIFIER, facilitatorId);
219                     pos.end(facilitatorToken);
220                     pos.end(originToken);
221                     pos.write(Telecom.Call.STATUS, status);
222                     if (acceptControl) {
223                         pos.write(Telecom.Call.CONTROLS, Telecom.ACCEPT);
224                     }
225                     if (rejectControl) {
226                         pos.write(Telecom.Call.CONTROLS, Telecom.REJECT);
227                     }
228                     if (silenceControl) {
229                         pos.write(Telecom.Call.CONTROLS, Telecom.SILENCE);
230                     }
231                     if (muteControl) {
232                         pos.write(Telecom.Call.CONTROLS, Telecom.MUTE);
233                     }
234                     if (unmuteControl) {
235                         pos.write(Telecom.Call.CONTROLS, Telecom.UNMUTE);
236                     }
237                     if (endControl) {
238                         pos.write(Telecom.Call.CONTROLS, Telecom.END);
239                     }
240                     if (holdControl) {
241                         pos.write(Telecom.Call.CONTROLS, Telecom.PUT_ON_HOLD);
242                     }
243                     if (unholdControl) {
244                         pos.write(Telecom.Call.CONTROLS, Telecom.TAKE_OFF_HOLD);
245                     }
246                     pos.end(callsToken);
247                     pos.end(telecomToken);
248                     mTransportManager.createEmulatedTransport(associationId)
249                             .processMessage(Transport.MESSAGE_REQUEST_CONTEXT_SYNC,
250                                     /* sequence= */ 0, pos.getBytes());
251                     break;
252                 }
253 
254                 case "disable-context-sync": {
255                     associationId = getNextIntArgRequired();
256                     int flag = getNextIntArgRequired();
257                     mAssociationRequestsProcessor.disableSystemDataSync(associationId, flag);
258                     break;
259                 }
260 
261                 case "enable-context-sync": {
262                     associationId = getNextIntArgRequired();
263                     int flag = getNextIntArgRequired();
264                     mAssociationRequestsProcessor.enableSystemDataSync(associationId, flag);
265                     break;
266                 }
267 
268                 case "get-perm-sync-state": {
269                     associationId = getNextIntArgRequired();
270                     PermissionSyncRequest request =
271                             mSystemDataTransferProcessor.getPermissionSyncRequest(associationId);
272                     out.println((request == null ? "null" : request.isUserConsented()));
273                     break;
274                 }
275 
276                 case "remove-perm-sync-state": {
277                     associationId = getNextIntArgRequired();
278                     PermissionSyncRequest request =
279                             mSystemDataTransferProcessor.getPermissionSyncRequest(associationId);
280                     out.print((request == null ? "null" : request.isUserConsented()));
281                     mSystemDataTransferProcessor.removePermissionSyncRequest(associationId);
282                     request = mSystemDataTransferProcessor.getPermissionSyncRequest(associationId);
283                     // should print " -> null"
284                     out.println(" -> " + (request == null ? "null" : request.isUserConsented()));
285                     break;
286                 }
287 
288                 case "enable-perm-sync": {
289                     associationId = getNextIntArgRequired();
290                     PermissionSyncRequest request =
291                             mSystemDataTransferProcessor.getPermissionSyncRequest(associationId);
292                     out.print((request == null ? "null" : request.isUserConsented()));
293                     mSystemDataTransferProcessor.enablePermissionsSync(associationId);
294                     request = mSystemDataTransferProcessor.getPermissionSyncRequest(associationId);
295                     out.println(" -> " + request.isUserConsented()); // should print " -> true"
296                     break;
297                 }
298 
299                 case "disable-perm-sync": {
300                     associationId = getNextIntArgRequired();
301                     PermissionSyncRequest request =
302                             mSystemDataTransferProcessor.getPermissionSyncRequest(associationId);
303                     out.print((request == null ? "null" : request.isUserConsented()));
304                     mSystemDataTransferProcessor.disablePermissionsSync(associationId);
305                     request = mSystemDataTransferProcessor.getPermissionSyncRequest(associationId);
306                     out.println(" -> " + request.isUserConsented()); // should print " -> false"
307                     break;
308                 }
309 
310                 default:
311                     return handleDefaultCommands(cmd);
312             }
313         } catch (Throwable e) {
314             final PrintWriter errOut = getErrPrintWriter();
315             errOut.println();
316             errOut.println("Exception occurred while executing '" + cmd + "':");
317             e.printStackTrace(errOut);
318             return 1;
319         }
320         return 0;
321     }
322 
getNextIntArgRequired()323     private int getNextIntArgRequired() {
324         return Integer.parseInt(getNextArgRequired());
325     }
326 
getNextBooleanArgRequired()327     private boolean getNextBooleanArgRequired() {
328         String arg = getNextArgRequired();
329         if ("true".equalsIgnoreCase(arg) || "false".equalsIgnoreCase(arg)) {
330             return Boolean.parseBoolean(arg);
331         } else {
332             throw new IllegalArgumentException("Expected a boolean argument but was: " + arg);
333         }
334     }
335 
336     @Override
onHelp()337     public void onHelp() {
338         PrintWriter pw = getOutPrintWriter();
339         pw.println("Companion Device Manager (companiondevice) commands:");
340         pw.println("  help");
341         pw.println("      Print this help text.");
342         pw.println("  list USER_ID");
343         pw.println("      List all Associations for a user.");
344         pw.println("  associate USER_ID PACKAGE MAC_ADDRESS");
345         pw.println("      Create a new Association.");
346         pw.println("  disassociate USER_ID PACKAGE MAC_ADDRESS");
347         pw.println("      Remove an existing Association.");
348         pw.println("  clear-association-memory-cache");
349         pw.println("      Clear the in-memory association cache and reload all association ");
350         pw.println("      information from persistent storage. USE FOR DEBUGGING PURPOSES ONLY.");
351         pw.println("      USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
352 
353         pw.println("  simulate-device-appeared ASSOCIATION_ID");
354         pw.println("      Make CDM act as if the given companion device has appeared.");
355         pw.println("      I.e. bind the associated companion application's");
356         pw.println("      CompanionDeviceService(s) and trigger onDeviceAppeared() callback.");
357         pw.println("      The CDM will consider the devices as present for 60 seconds and then");
358         pw.println("      will act as if device disappeared, unless 'simulate-device-disappeared'");
359         pw.println("      or 'simulate-device-appeared' is called again before 60 seconds run out"
360                 + ".");
361         pw.println("      USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
362 
363         pw.println("  simulate-device-disappeared ASSOCIATION_ID");
364         pw.println("      Make CDM act as if the given companion device has disappeared.");
365         pw.println("      I.e. unbind the associated companion application's");
366         pw.println("      CompanionDeviceService(s) and trigger onDeviceDisappeared() callback.");
367         pw.println("      NOTE: This will only have effect if 'simulate-device-appeared' was");
368         pw.println("      invoked for the same device (same ASSOCIATION_ID) no longer than");
369         pw.println("      60 seconds ago.");
370         pw.println("      USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
371 
372         pw.println("  remove-inactive-associations");
373         pw.println("      Remove self-managed associations that have not been active ");
374         pw.println("      for a long time (90 days or as configured via ");
375         pw.println("      \"debug.cdm.cdmservice.cleanup_time_window\" system property). ");
376         pw.println("      USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
377 
378         pw.println("  create-emulated-transport <ASSOCIATION_ID>");
379         pw.println("      Create an EmulatedTransport for testing purposes only");
380 
381         pw.println("  enable-perm-sync <ASSOCIATION_ID>");
382         pw.println("      Enable perm sync for the association.");
383         pw.println("  disable-perm-sync <ASSOCIATION_ID>");
384         pw.println("      Disable perm sync for the association.");
385         pw.println("  get-perm-sync-state <ASSOCIATION_ID>");
386         pw.println("      Get perm sync state for the association.");
387         pw.println("  remove-perm-sync-state <ASSOCIATION_ID>");
388         pw.println("      Remove perm sync state for the association.");
389     }
390 }
391