OSDN Git Service

RESTRICT AUTOMERGE
[android-x86/frameworks-base.git] / services / companion / java / com / android / server / companion / CompanionDeviceManagerService.java
1 /*
2  * Copyright (C) 2017 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
18 package com.android.server.companion;
19
20 import static com.android.internal.util.CollectionUtils.size;
21 import static com.android.internal.util.Preconditions.checkArgument;
22 import static com.android.internal.util.Preconditions.checkNotNull;
23 import static com.android.internal.util.Preconditions.checkState;
24
25 import android.Manifest;
26 import android.annotation.CheckResult;
27 import android.annotation.Nullable;
28 import android.app.PendingIntent;
29 import android.companion.AssociationRequest;
30 import android.companion.CompanionDeviceManager;
31 import android.companion.ICompanionDeviceDiscoveryService;
32 import android.companion.ICompanionDeviceDiscoveryServiceCallback;
33 import android.companion.ICompanionDeviceManager;
34 import android.companion.IFindDeviceCallback;
35 import android.content.ComponentName;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.ServiceConnection;
39 import android.content.pm.FeatureInfo;
40 import android.content.pm.PackageInfo;
41 import android.content.pm.PackageManager;
42 import android.net.NetworkPolicyManager;
43 import android.os.Binder;
44 import android.os.Environment;
45 import android.os.Handler;
46 import android.os.IBinder;
47 import android.os.IDeviceIdleController;
48 import android.os.IInterface;
49 import android.os.Parcel;
50 import android.os.Process;
51 import android.os.RemoteException;
52 import android.os.ResultReceiver;
53 import android.os.ServiceManager;
54 import android.os.ShellCallback;
55 import android.os.ShellCommand;
56 import android.os.UserHandle;
57 import android.provider.Settings;
58 import android.provider.SettingsStringUtil.ComponentNameSet;
59 import android.text.BidiFormatter;
60 import android.util.ArraySet;
61 import android.util.AtomicFile;
62 import android.util.ExceptionUtils;
63 import android.util.Log;
64 import android.util.Slog;
65 import android.util.Xml;
66
67 import com.android.internal.app.IAppOpsService;
68 import com.android.internal.content.PackageMonitor;
69 import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
70 import com.android.internal.util.ArrayUtils;
71 import com.android.internal.util.CollectionUtils;
72 import com.android.server.FgThread;
73 import com.android.server.SystemService;
74
75 import org.xmlpull.v1.XmlPullParser;
76 import org.xmlpull.v1.XmlPullParserException;
77 import org.xmlpull.v1.XmlSerializer;
78
79 import java.io.File;
80 import java.io.FileDescriptor;
81 import java.io.FileInputStream;
82 import java.io.IOException;
83 import java.nio.charset.StandardCharsets;
84 import java.util.ArrayList;
85 import java.util.List;
86 import java.util.Objects;
87 import java.util.Set;
88 import java.util.concurrent.ConcurrentHashMap;
89 import java.util.concurrent.ConcurrentMap;
90 import java.util.function.Function;
91
92 //TODO onStop schedule unbind in 5 seconds
93 //TODO make sure APIs are only callable from currently focused app
94 //TODO schedule stopScan on activity destroy(except if configuration change)
95 //TODO on associate called again after configuration change -> replace old callback with new
96 //TODO avoid leaking calling activity in IFindDeviceCallback (see PrintManager#print for example)
97 /** @hide */
98 public class CompanionDeviceManagerService extends SystemService implements Binder.DeathRecipient {
99
100     private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative(
101             CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME,
102             ".DeviceDiscoveryService");
103
104     private static final boolean DEBUG = false;
105     private static final String LOG_TAG = "CompanionDeviceManagerService";
106
107     private static final String XML_TAG_ASSOCIATIONS = "associations";
108     private static final String XML_TAG_ASSOCIATION = "association";
109     private static final String XML_ATTR_PACKAGE = "package";
110     private static final String XML_ATTR_DEVICE = "device";
111     private static final String XML_FILE_NAME = "companion_device_manager_associations.xml";
112
113     private final CompanionDeviceManagerImpl mImpl;
114     private final ConcurrentMap<Integer, AtomicFile> mUidToStorage = new ConcurrentHashMap<>();
115     private IDeviceIdleController mIdleController;
116     private ServiceConnection mServiceConnection;
117     private IAppOpsService mAppOpsManager;
118
119     private IFindDeviceCallback mFindDeviceCallback;
120     private AssociationRequest mRequest;
121     private String mCallingPackage;
122
123     private final Object mLock = new Object();
124
125     public CompanionDeviceManagerService(Context context) {
126         super(context);
127         mImpl = new CompanionDeviceManagerImpl();
128         mIdleController = IDeviceIdleController.Stub.asInterface(
129                 ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
130         mAppOpsManager = IAppOpsService.Stub.asInterface(
131                 ServiceManager.getService(Context.APP_OPS_SERVICE));
132         registerPackageMonitor();
133     }
134
135     private void registerPackageMonitor() {
136         new PackageMonitor() {
137             @Override
138             public void onPackageRemoved(String packageName, int uid) {
139                 updateAssociations(
140                         as -> CollectionUtils.filter(as,
141                                 a -> !Objects.equals(a.companionAppPackage, packageName)),
142                         getChangingUserId());
143             }
144
145             @Override
146             public void onPackageModified(String packageName) {
147                 int userId = getChangingUserId();
148                 if (!ArrayUtils.isEmpty(readAllAssociations(userId, packageName))) {
149                     updateSpecialAccessPermissionForAssociatedPackage(packageName, userId);
150                 }
151             }
152
153         }.register(getContext(), FgThread.get().getLooper(), UserHandle.ALL, true);
154     }
155
156     @Override
157     public void onStart() {
158         publishBinderService(Context.COMPANION_DEVICE_SERVICE, mImpl);
159     }
160
161     @Override
162     public void binderDied() {
163         Handler.getMain().post(this::cleanup);
164     }
165
166     private void cleanup() {
167         synchronized (mLock) {
168             mServiceConnection = unbind(mServiceConnection);
169             mFindDeviceCallback = unlinkToDeath(mFindDeviceCallback, this, 0);
170             mRequest = null;
171             mCallingPackage = null;
172         }
173     }
174
175     /**
176      * Usage: {@code a = unlinkToDeath(a, deathRecipient, flags); }
177      */
178     @Nullable
179     @CheckResult
180     private static <T extends IInterface> T unlinkToDeath(T iinterface,
181             IBinder.DeathRecipient deathRecipient, int flags) {
182         if (iinterface != null) {
183             iinterface.asBinder().unlinkToDeath(deathRecipient, flags);
184         }
185         return null;
186     }
187
188     @Nullable
189     @CheckResult
190     private ServiceConnection unbind(@Nullable ServiceConnection conn) {
191         if (conn != null) {
192             getContext().unbindService(conn);
193         }
194         return null;
195     }
196
197     class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub {
198
199         @Override
200         public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
201                 throws RemoteException {
202             try {
203                 return super.onTransact(code, data, reply, flags);
204             } catch (Throwable e) {
205                 Slog.e(LOG_TAG, "Error during IPC", e);
206                 throw ExceptionUtils.propagate(e, RemoteException.class);
207             }
208         }
209
210         @Override
211         public void associate(
212                 AssociationRequest request,
213                 IFindDeviceCallback callback,
214                 String callingPackage) throws RemoteException {
215             if (DEBUG) {
216                 Slog.i(LOG_TAG, "associate(request = " + request + ", callback = " + callback
217                         + ", callingPackage = " + callingPackage + ")");
218             }
219             checkNotNull(request, "Request cannot be null");
220             checkNotNull(callback, "Callback cannot be null");
221             checkCallerIsSystemOr(callingPackage);
222             int userId = getCallingUserId();
223             checkUsesFeature(callingPackage, userId);
224             final long callingIdentity = Binder.clearCallingIdentity();
225             try {
226                 getContext().bindServiceAsUser(
227                         new Intent().setComponent(SERVICE_TO_BIND_TO),
228                         createServiceConnection(request, callback, callingPackage),
229                         Context.BIND_AUTO_CREATE,
230                         UserHandle.of(userId));
231             } finally {
232                 Binder.restoreCallingIdentity(callingIdentity);
233             }
234         }
235
236         @Override
237         public void stopScan(AssociationRequest request,
238                 IFindDeviceCallback callback,
239                 String callingPackage) {
240             if (Objects.equals(request, mRequest)
241                     && Objects.equals(callback, mFindDeviceCallback)
242                     && Objects.equals(callingPackage, mCallingPackage)) {
243                 cleanup();
244             }
245         }
246
247         @Override
248         public List<String> getAssociations(String callingPackage, int userId)
249                 throws RemoteException {
250             checkCallerIsSystemOr(callingPackage, userId);
251             checkUsesFeature(callingPackage, getCallingUserId());
252             return new ArrayList<>(CollectionUtils.map(
253                     readAllAssociations(userId, callingPackage),
254                     a -> a.deviceAddress));
255         }
256
257         //TODO also revoke notification access
258         @Override
259         public void disassociate(String deviceMacAddress, String callingPackage)
260                 throws RemoteException {
261             checkNotNull(deviceMacAddress);
262             checkCallerIsSystemOr(callingPackage);
263             checkUsesFeature(callingPackage, getCallingUserId());
264             removeAssociation(getCallingUserId(), callingPackage, deviceMacAddress);
265         }
266
267         private void checkCallerIsSystemOr(String pkg) throws RemoteException {
268             checkCallerIsSystemOr(pkg, getCallingUserId());
269         }
270
271         private void checkCallerIsSystemOr(String pkg, int userId) throws RemoteException {
272             if (isCallerSystem()) {
273                 return;
274             }
275
276             checkArgument(getCallingUserId() == userId,
277                     "Must be called by either same user or system");
278             mAppOpsManager.checkPackage(Binder.getCallingUid(), pkg);
279         }
280
281         @Override
282         public PendingIntent requestNotificationAccess(ComponentName component)
283                 throws RemoteException {
284             String callingPackage = component.getPackageName();
285             checkCanCallNotificationApi(callingPackage);
286             int userId = getCallingUserId();
287             String packageTitle = BidiFormatter.getInstance().unicodeWrap(
288                     getPackageInfo(callingPackage, userId)
289                             .applicationInfo
290                             .loadSafeLabel(getContext().getPackageManager())
291                             .toString());
292             long identity = Binder.clearCallingIdentity();
293             try {
294                 return PendingIntent.getActivity(getContext(),
295                         0 /* request code */,
296                         NotificationAccessConfirmationActivityContract.launcherIntent(
297                                 userId, component, packageTitle),
298                         PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT
299                                 | PendingIntent.FLAG_CANCEL_CURRENT);
300             } finally {
301                 Binder.restoreCallingIdentity(identity);
302             }
303         }
304
305         @Override
306         public boolean hasNotificationAccess(ComponentName component) throws RemoteException {
307             checkCanCallNotificationApi(component.getPackageName());
308             String setting = Settings.Secure.getString(getContext().getContentResolver(),
309                     Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
310             return new ComponentNameSet(setting).contains(component);
311         }
312
313         private void checkCanCallNotificationApi(String callingPackage) throws RemoteException {
314             checkCallerIsSystemOr(callingPackage);
315             int userId = getCallingUserId();
316             checkState(!ArrayUtils.isEmpty(readAllAssociations(userId, callingPackage)),
317                     "App must have an association before calling this API");
318             checkUsesFeature(callingPackage, userId);
319         }
320
321         private void checkUsesFeature(String pkg, int userId) {
322             if (isCallerSystem()) {
323                 // Drop the requirement for calls from system process
324                 return;
325             }
326
327             FeatureInfo[] reqFeatures = getPackageInfo(pkg, userId).reqFeatures;
328             String requiredFeature = PackageManager.FEATURE_COMPANION_DEVICE_SETUP;
329             int numFeatures = ArrayUtils.size(reqFeatures);
330             for (int i = 0; i < numFeatures; i++) {
331                 if (requiredFeature.equals(reqFeatures[i].name)) return;
332             }
333             throw new IllegalStateException("Must declare uses-feature "
334                     + requiredFeature
335                     + " in manifest to use this API");
336         }
337
338         @Override
339         public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
340                 String[] args, ShellCallback callback, ResultReceiver resultReceiver)
341                 throws RemoteException {
342             new ShellCmd().exec(this, in, out, err, args, callback, resultReceiver);
343         }
344     }
345
346     private static int getCallingUserId() {
347         return UserHandle.getUserId(Binder.getCallingUid());
348     }
349
350     private static boolean isCallerSystem() {
351         return Binder.getCallingUid() == Process.SYSTEM_UID;
352     }
353
354     private ServiceConnection createServiceConnection(
355             final AssociationRequest request,
356             final IFindDeviceCallback findDeviceCallback,
357             final String callingPackage) {
358         mServiceConnection = new ServiceConnection() {
359             @Override
360             public void onServiceConnected(ComponentName name, IBinder service) {
361                 if (DEBUG) {
362                     Slog.i(LOG_TAG,
363                             "onServiceConnected(name = " + name + ", service = "
364                                     + service + ")");
365                 }
366
367                 mFindDeviceCallback = findDeviceCallback;
368                 mRequest = request;
369                 mCallingPackage = callingPackage;
370
371                 try {
372                     mFindDeviceCallback.asBinder().linkToDeath(
373                             CompanionDeviceManagerService.this, 0);
374                 } catch (RemoteException e) {
375                     cleanup();
376                     return;
377                 }
378
379                 try {
380                     ICompanionDeviceDiscoveryService.Stub
381                             .asInterface(service)
382                             .startDiscovery(
383                                     request,
384                                     callingPackage,
385                                     findDeviceCallback,
386                                     getServiceCallback());
387                 } catch (RemoteException e) {
388                     Log.e(LOG_TAG, "Error while initiating device discovery", e);
389                 }
390             }
391
392             @Override
393             public void onServiceDisconnected(ComponentName name) {
394                 if (DEBUG) Slog.i(LOG_TAG, "onServiceDisconnected(name = " + name + ")");
395             }
396         };
397         return mServiceConnection;
398     }
399
400     private ICompanionDeviceDiscoveryServiceCallback.Stub getServiceCallback() {
401         return new ICompanionDeviceDiscoveryServiceCallback.Stub() {
402
403             @Override
404             public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
405                     throws RemoteException {
406                 try {
407                     return super.onTransact(code, data, reply, flags);
408                 } catch (Throwable e) {
409                     Slog.e(LOG_TAG, "Error during IPC", e);
410                     throw ExceptionUtils.propagate(e, RemoteException.class);
411                 }
412             }
413
414             @Override
415             public void onDeviceSelected(String packageName, int userId, String deviceAddress) {
416                 addAssociation(userId, packageName, deviceAddress);
417                 cleanup();
418             }
419
420             @Override
421             public void onDeviceSelectionCancel() {
422                 cleanup();
423             }
424         };
425     }
426
427     void addAssociation(int userId, String packageName, String deviceAddress) {
428         updateSpecialAccessPermissionForAssociatedPackage(packageName, userId);
429         recordAssociation(packageName, deviceAddress);
430     }
431
432     void removeAssociation(int userId, String pkg, String deviceMacAddress) {
433         updateAssociations(associations -> CollectionUtils.remove(associations,
434                 new Association(userId, deviceMacAddress, pkg)));
435     }
436
437     private void updateSpecialAccessPermissionForAssociatedPackage(String packageName, int userId) {
438         PackageInfo packageInfo = getPackageInfo(packageName, userId);
439         if (packageInfo == null) {
440             return;
441         }
442
443         Binder.withCleanCallingIdentity(() -> {
444             try {
445                 if (containsEither(packageInfo.requestedPermissions,
446                         Manifest.permission.RUN_IN_BACKGROUND,
447                         Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
448                     mIdleController.addPowerSaveWhitelistApp(packageInfo.packageName);
449                 } else {
450                     mIdleController.removePowerSaveWhitelistApp(packageInfo.packageName);
451                 }
452             } catch (RemoteException e) {
453                 /* ignore - local call */
454             }
455
456             NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(getContext());
457             if (containsEither(packageInfo.requestedPermissions,
458                     Manifest.permission.USE_DATA_IN_BACKGROUND,
459                     Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)) {
460                 networkPolicyManager.addUidPolicy(
461                         packageInfo.applicationInfo.uid,
462                         NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
463             } else {
464                 networkPolicyManager.removeUidPolicy(
465                         packageInfo.applicationInfo.uid,
466                         NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
467             }
468         });
469     }
470
471     private static <T> boolean containsEither(T[] array, T a, T b) {
472         return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
473     }
474
475     @Nullable
476     private PackageInfo getPackageInfo(String packageName, int userId) {
477         return Binder.withCleanCallingIdentity(() -> {
478             try {
479                 return getContext().getPackageManager().getPackageInfoAsUser(
480                         packageName,
481                         PackageManager.GET_PERMISSIONS | PackageManager.GET_CONFIGURATIONS,
482                         userId);
483             } catch (PackageManager.NameNotFoundException e) {
484                 Slog.e(LOG_TAG, "Failed to get PackageInfo for package " + packageName, e);
485                 return null;
486             }
487         });
488     }
489
490     private void recordAssociation(String priviledgedPackage, String deviceAddress) {
491         if (DEBUG) {
492             Log.i(LOG_TAG, "recordAssociation(priviledgedPackage = " + priviledgedPackage
493                     + ", deviceAddress = " + deviceAddress + ")");
494         }
495         int userId = getCallingUserId();
496         updateAssociations(associations -> CollectionUtils.add(associations,
497                 new Association(userId, deviceAddress, priviledgedPackage)));
498     }
499
500     private void updateAssociations(Function<Set<Association>, Set<Association>> update) {
501         updateAssociations(update, getCallingUserId());
502     }
503
504     private void updateAssociations(Function<Set<Association>, Set<Association>> update,
505             int userId) {
506         final AtomicFile file = getStorageFileForUser(userId);
507         synchronized (file) {
508             Set<Association> associations = readAllAssociations(userId);
509             final Set<Association> old = CollectionUtils.copyOf(associations);
510             associations = update.apply(associations);
511             if (size(old) == size(associations)) return;
512
513             Set<Association> finalAssociations = associations;
514             file.write((out) -> {
515                 XmlSerializer xml = Xml.newSerializer();
516                 try {
517                     xml.setOutput(out, StandardCharsets.UTF_8.name());
518                     xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
519                     xml.startDocument(null, true);
520                     xml.startTag(null, XML_TAG_ASSOCIATIONS);
521
522                     CollectionUtils.forEach(finalAssociations, association -> {
523                         xml.startTag(null, XML_TAG_ASSOCIATION)
524                                 .attribute(null, XML_ATTR_PACKAGE, association.companionAppPackage)
525                                 .attribute(null, XML_ATTR_DEVICE, association.deviceAddress)
526                                 .endTag(null, XML_TAG_ASSOCIATION);
527                     });
528
529                     xml.endTag(null, XML_TAG_ASSOCIATIONS);
530                     xml.endDocument();
531                 } catch (Exception e) {
532                     Slog.e(LOG_TAG, "Error while writing associations file", e);
533                     throw ExceptionUtils.propagate(e);
534                 }
535
536             });
537         }
538     }
539
540     private AtomicFile getStorageFileForUser(int uid) {
541         return mUidToStorage.computeIfAbsent(uid, (u) ->
542                 new AtomicFile(new File(
543                         //TODO deprecated method - what's the right replacement?
544                         Environment.getUserSystemDirectory(u),
545                         XML_FILE_NAME)));
546     }
547
548     @Nullable
549     private Set<Association> readAllAssociations(int userId) {
550         return readAllAssociations(userId, null);
551     }
552
553     @Nullable
554     private Set<Association> readAllAssociations(int userId, @Nullable String packageFilter) {
555         final AtomicFile file = getStorageFileForUser(userId);
556
557         if (!file.getBaseFile().exists()) return null;
558
559         ArraySet<Association> result = null;
560         final XmlPullParser parser = Xml.newPullParser();
561         synchronized (file) {
562             try (FileInputStream in = file.openRead()) {
563                 parser.setInput(in, StandardCharsets.UTF_8.name());
564                 int type;
565                 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
566                     if (type != XmlPullParser.START_TAG
567                             && !XML_TAG_ASSOCIATIONS.equals(parser.getName())) continue;
568
569                     final String appPackage = parser.getAttributeValue(null, XML_ATTR_PACKAGE);
570                     final String deviceAddress = parser.getAttributeValue(null, XML_ATTR_DEVICE);
571
572                     if (appPackage == null || deviceAddress == null) continue;
573                     if (packageFilter != null && !packageFilter.equals(appPackage)) continue;
574
575                     result = ArrayUtils.add(result,
576                             new Association(userId, deviceAddress, appPackage));
577                 }
578                 return result;
579             } catch (XmlPullParserException | IOException e) {
580                 Slog.e(LOG_TAG, "Error while reading associations file", e);
581                 return null;
582             }
583         }
584     }
585
586
587
588     private class Association {
589         public final int uid;
590         public final String deviceAddress;
591         public final String companionAppPackage;
592
593         private Association(int uid, String deviceAddress, String companionAppPackage) {
594             this.uid = uid;
595             this.deviceAddress = checkNotNull(deviceAddress);
596             this.companionAppPackage = checkNotNull(companionAppPackage);
597         }
598
599         @Override
600         public boolean equals(Object o) {
601             if (this == o) return true;
602             if (o == null || getClass() != o.getClass()) return false;
603
604             Association that = (Association) o;
605
606             if (uid != that.uid) return false;
607             if (!deviceAddress.equals(that.deviceAddress)) return false;
608             return companionAppPackage.equals(that.companionAppPackage);
609
610         }
611
612         @Override
613         public int hashCode() {
614             int result = uid;
615             result = 31 * result + deviceAddress.hashCode();
616             result = 31 * result + companionAppPackage.hashCode();
617             return result;
618         }
619     }
620
621     private class ShellCmd extends ShellCommand {
622         public static final String USAGE = "help\n"
623                 + "list USER_ID\n"
624                 + "associate USER_ID PACKAGE MAC_ADDRESS\n"
625                 + "disassociate USER_ID PACKAGE MAC_ADDRESS";
626
627         ShellCmd() {
628             getContext().enforceCallingOrSelfPermission(
629                     android.Manifest.permission.MANAGE_COMPANION_DEVICES, "ShellCmd");
630         }
631
632         @Override
633         public int onCommand(String cmd) {
634             switch (cmd) {
635                 case "list": {
636                     CollectionUtils.forEach(
637                             readAllAssociations(getNextArgInt()),
638                             a -> getOutPrintWriter()
639                                     .println(a.companionAppPackage + " " + a.deviceAddress));
640                 } break;
641
642                 case "associate": {
643                     addAssociation(getNextArgInt(), getNextArgRequired(), getNextArgRequired());
644                 } break;
645
646                 case "disassociate": {
647                     removeAssociation(getNextArgInt(), getNextArgRequired(), getNextArgRequired());
648                 } break;
649
650                 default: return handleDefaultCommands(cmd);
651             }
652             return 0;
653         }
654
655         private int getNextArgInt() {
656             return Integer.parseInt(getNextArgRequired());
657         }
658
659         @Override
660         public void onHelp() {
661             getOutPrintWriter().println(USAGE);
662         }
663     }
664
665 }