Merge "Replace CDM profiles hardcoded strings with real values"
This commit is contained in:
committed by
Android (Google) Code Review
commit
3462bcb4a5
@@ -96,11 +96,25 @@ public final class AssociationRequest implements Parcelable {
|
||||
*/
|
||||
private @Nullable String mCallingPackage = null;
|
||||
|
||||
/**
|
||||
* The user-readable description of the device profile's privileges.
|
||||
*
|
||||
* Populated by the system.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
private @Nullable String mDeviceProfilePrivilegesDescription = null;
|
||||
|
||||
/** @hide */
|
||||
public void setCallingPackage(@NonNull String pkg) {
|
||||
mCallingPackage = pkg;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public void setDeviceProfilePrivilegesDescription(@NonNull String desc) {
|
||||
mDeviceProfilePrivilegesDescription = desc;
|
||||
}
|
||||
|
||||
private void onConstructed() {
|
||||
if (mDeviceProfile != null
|
||||
&& !Objects.equals(mDeviceProfile, DEVICE_PROFILE_WATCH)) {
|
||||
@@ -178,14 +192,14 @@ public final class AssociationRequest implements Parcelable {
|
||||
markUsed();
|
||||
return new AssociationRequest(
|
||||
mSingleDevice, emptyIfNull(mDeviceFilters),
|
||||
mDeviceProfile, null);
|
||||
mDeviceProfile, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Code below generated by codegen v1.0.20.
|
||||
// Code below generated by codegen v1.0.22.
|
||||
//
|
||||
// DO NOT MODIFY!
|
||||
// CHECKSTYLE:OFF Generated code
|
||||
@@ -215,6 +229,10 @@ public final class AssociationRequest implements Parcelable {
|
||||
* The app package making the request.
|
||||
*
|
||||
* Populated by the system.
|
||||
* @param deviceProfilePrivilegesDescription
|
||||
* The user-readable description of the device profile's privileges.
|
||||
*
|
||||
* Populated by the system.
|
||||
* @hide
|
||||
*/
|
||||
@DataClass.Generated.Member
|
||||
@@ -222,7 +240,8 @@ public final class AssociationRequest implements Parcelable {
|
||||
boolean singleDevice,
|
||||
@NonNull List<DeviceFilter<?>> deviceFilters,
|
||||
@Nullable @DeviceProfile String deviceProfile,
|
||||
@Nullable String callingPackage) {
|
||||
@Nullable String callingPackage,
|
||||
@Nullable String deviceProfilePrivilegesDescription) {
|
||||
this.mSingleDevice = singleDevice;
|
||||
this.mDeviceFilters = deviceFilters;
|
||||
com.android.internal.util.AnnotationValidations.validate(
|
||||
@@ -231,6 +250,7 @@ public final class AssociationRequest implements Parcelable {
|
||||
com.android.internal.util.AnnotationValidations.validate(
|
||||
DeviceProfile.class, null, mDeviceProfile);
|
||||
this.mCallingPackage = callingPackage;
|
||||
this.mDeviceProfilePrivilegesDescription = deviceProfilePrivilegesDescription;
|
||||
|
||||
onConstructed();
|
||||
}
|
||||
@@ -257,6 +277,18 @@ public final class AssociationRequest implements Parcelable {
|
||||
return mCallingPackage;
|
||||
}
|
||||
|
||||
/**
|
||||
* The user-readable description of the device profile's privileges.
|
||||
*
|
||||
* Populated by the system.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@DataClass.Generated.Member
|
||||
public @Nullable String getDeviceProfilePrivilegesDescription() {
|
||||
return mDeviceProfilePrivilegesDescription;
|
||||
}
|
||||
|
||||
@Override
|
||||
@DataClass.Generated.Member
|
||||
public String toString() {
|
||||
@@ -267,7 +299,8 @@ public final class AssociationRequest implements Parcelable {
|
||||
"singleDevice = " + mSingleDevice + ", " +
|
||||
"deviceFilters = " + mDeviceFilters + ", " +
|
||||
"deviceProfile = " + mDeviceProfile + ", " +
|
||||
"callingPackage = " + mCallingPackage +
|
||||
"callingPackage = " + mCallingPackage + ", " +
|
||||
"deviceProfilePrivilegesDescription = " + mDeviceProfilePrivilegesDescription +
|
||||
" }";
|
||||
}
|
||||
|
||||
@@ -287,7 +320,8 @@ public final class AssociationRequest implements Parcelable {
|
||||
&& mSingleDevice == that.mSingleDevice
|
||||
&& Objects.equals(mDeviceFilters, that.mDeviceFilters)
|
||||
&& Objects.equals(mDeviceProfile, that.mDeviceProfile)
|
||||
&& Objects.equals(mCallingPackage, that.mCallingPackage);
|
||||
&& Objects.equals(mCallingPackage, that.mCallingPackage)
|
||||
&& Objects.equals(mDeviceProfilePrivilegesDescription, that.mDeviceProfilePrivilegesDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -301,6 +335,7 @@ public final class AssociationRequest implements Parcelable {
|
||||
_hash = 31 * _hash + Objects.hashCode(mDeviceFilters);
|
||||
_hash = 31 * _hash + Objects.hashCode(mDeviceProfile);
|
||||
_hash = 31 * _hash + Objects.hashCode(mCallingPackage);
|
||||
_hash = 31 * _hash + Objects.hashCode(mDeviceProfilePrivilegesDescription);
|
||||
return _hash;
|
||||
}
|
||||
|
||||
@@ -314,10 +349,12 @@ public final class AssociationRequest implements Parcelable {
|
||||
if (mSingleDevice) flg |= 0x1;
|
||||
if (mDeviceProfile != null) flg |= 0x4;
|
||||
if (mCallingPackage != null) flg |= 0x8;
|
||||
if (mDeviceProfilePrivilegesDescription != null) flg |= 0x10;
|
||||
dest.writeByte(flg);
|
||||
dest.writeParcelableList(mDeviceFilters, flags);
|
||||
if (mDeviceProfile != null) dest.writeString(mDeviceProfile);
|
||||
if (mCallingPackage != null) dest.writeString(mCallingPackage);
|
||||
if (mDeviceProfilePrivilegesDescription != null) dest.writeString(mDeviceProfilePrivilegesDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -337,6 +374,7 @@ public final class AssociationRequest implements Parcelable {
|
||||
in.readParcelableList(deviceFilters, DeviceFilter.class.getClassLoader());
|
||||
String deviceProfile = (flg & 0x4) == 0 ? null : in.readString();
|
||||
String callingPackage = (flg & 0x8) == 0 ? null : in.readString();
|
||||
String deviceProfilePrivilegesDescription = (flg & 0x10) == 0 ? null : in.readString();
|
||||
|
||||
this.mSingleDevice = singleDevice;
|
||||
this.mDeviceFilters = deviceFilters;
|
||||
@@ -346,6 +384,7 @@ public final class AssociationRequest implements Parcelable {
|
||||
com.android.internal.util.AnnotationValidations.validate(
|
||||
DeviceProfile.class, null, mDeviceProfile);
|
||||
this.mCallingPackage = callingPackage;
|
||||
this.mDeviceProfilePrivilegesDescription = deviceProfilePrivilegesDescription;
|
||||
|
||||
onConstructed();
|
||||
}
|
||||
@@ -365,10 +404,10 @@ public final class AssociationRequest implements Parcelable {
|
||||
};
|
||||
|
||||
@DataClass.Generated(
|
||||
time = 1604534468409L,
|
||||
codegenVersion = "1.0.20",
|
||||
time = 1610132130920L,
|
||||
codegenVersion = "1.0.22",
|
||||
sourceFile = "frameworks/base/core/java/android/companion/AssociationRequest.java",
|
||||
inputSignatures = "public static final java.lang.String DEVICE_PROFILE_WATCH\nprivate boolean mSingleDevice\nprivate @com.android.internal.util.DataClass.PluralOf(\"deviceFilter\") @android.annotation.NonNull java.util.List<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String mDeviceProfile\nprivate @android.annotation.Nullable java.lang.String mCallingPackage\npublic void setCallingPackage(java.lang.String)\nprivate void onConstructed()\npublic @android.compat.annotation.UnsupportedAppUsage boolean isSingleDevice()\npublic @android.annotation.NonNull @android.compat.annotation.UnsupportedAppUsage java.util.List<android.companion.DeviceFilter<?>> getDeviceFilters()\nclass AssociationRequest extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mSingleDevice\nprivate @android.annotation.Nullable java.util.ArrayList<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate @android.annotation.Nullable java.lang.String mDeviceProfile\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setSingleDevice(boolean)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder addDeviceFilter(android.companion.DeviceFilter<?>)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDeviceProfile(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override android.companion.AssociationRequest build()\nclass Builder extends android.provider.OneTimeUseBuilder<android.companion.AssociationRequest> implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true, genHiddenGetters=true, genParcelable=true, genHiddenConstructor=true, genBuilder=false)")
|
||||
inputSignatures = "public static final java.lang.String DEVICE_PROFILE_WATCH\nprivate boolean mSingleDevice\nprivate @com.android.internal.util.DataClass.PluralOf(\"deviceFilter\") @android.annotation.NonNull java.util.List<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String mDeviceProfile\nprivate @android.annotation.Nullable java.lang.String mCallingPackage\nprivate @android.annotation.Nullable java.lang.String mDeviceProfilePrivilegesDescription\npublic void setCallingPackage(java.lang.String)\npublic void setDeviceProfilePrivilegesDescription(java.lang.String)\nprivate void onConstructed()\npublic @android.compat.annotation.UnsupportedAppUsage boolean isSingleDevice()\npublic @android.annotation.NonNull @android.compat.annotation.UnsupportedAppUsage java.util.List<android.companion.DeviceFilter<?>> getDeviceFilters()\nclass AssociationRequest extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mSingleDevice\nprivate @android.annotation.Nullable java.util.ArrayList<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate @android.annotation.Nullable java.lang.String mDeviceProfile\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setSingleDevice(boolean)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder addDeviceFilter(android.companion.DeviceFilter<?>)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDeviceProfile(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override android.companion.AssociationRequest build()\nclass Builder extends android.provider.OneTimeUseBuilder<android.companion.AssociationRequest> implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true, genHiddenGetters=true, genParcelable=true, genHiddenConstructor=true, genBuilder=false)")
|
||||
@Deprecated
|
||||
private void __metadata() {}
|
||||
|
||||
|
||||
@@ -662,6 +662,34 @@ public final class PermissionControllerManager {
|
||||
}, executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the description of the privileges associated with the given device profiles
|
||||
*
|
||||
* @param profileName Name of the device profile
|
||||
* @param executor Executor on which to invoke the callback
|
||||
* @param callback Callback to receive the result
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RequiresPermission(Manifest.permission.MANAGE_COMPANION_DEVICES)
|
||||
public void getPrivilegesDescriptionStringForProfile(
|
||||
@NonNull String profileName,
|
||||
@NonNull @CallbackExecutor Executor executor,
|
||||
@NonNull Consumer<String> callback) {
|
||||
mRemoteService.postAsync(service -> {
|
||||
AndroidFuture<String> future = new AndroidFuture<>();
|
||||
service.getPrivilegesDescriptionStringForProfile(profileName, future);
|
||||
return future;
|
||||
}).whenCompleteAsync((description, err) -> {
|
||||
if (err != null) {
|
||||
Log.e(TAG, "Error from getPrivilegesDescriptionStringForProfile", err);
|
||||
callback.accept(null);
|
||||
} else {
|
||||
callback.accept(description);
|
||||
}
|
||||
}, executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PermissionControllerManager#updateUserSensitiveForApp
|
||||
* @hide
|
||||
|
||||
@@ -540,12 +540,13 @@ public abstract class PermissionControllerService extends Service {
|
||||
public void getPrivilegesDescriptionStringForProfile(
|
||||
@NonNull String deviceProfileName,
|
||||
@NonNull AndroidFuture<String> callback) {
|
||||
checkStringNotEmpty(deviceProfileName);
|
||||
Objects.requireNonNull(callback);
|
||||
|
||||
enforceSomePermissionsGrantedToCaller(Manifest.permission.MANAGE_COMPANION_DEVICES);
|
||||
|
||||
try {
|
||||
checkStringNotEmpty(deviceProfileName);
|
||||
Objects.requireNonNull(callback);
|
||||
|
||||
enforceSomePermissionsGrantedToCaller(
|
||||
Manifest.permission.MANAGE_COMPANION_DEVICES);
|
||||
|
||||
callback.complete(PermissionControllerService
|
||||
.this
|
||||
.getPrivilegesDescriptionStringForProfile(deviceProfileName));
|
||||
|
||||
@@ -25,11 +25,14 @@
|
||||
<!-- The generic placeholder for a device type when nothing specific is known about it [CHAR LIMIT=30] -->
|
||||
<string name="profile_name_generic">device</string>
|
||||
|
||||
<!-- The name of the "watch" device type [CHAR LIMIT=30] -->
|
||||
<string name="profile_name_watch">watch</string>
|
||||
|
||||
<!-- Title of the device association confirmation dialog. -->
|
||||
<string name="confirmation_title">Set <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to manage your <xliff:g id="profile_name" example="watch">%2$s</xliff:g> - <strong><xliff:g id="device_name" example="ASUS ZenWatch 2">%3$s</xliff:g></strong></string>
|
||||
|
||||
<!-- Text of the device profile permissions explanation in the association dialog. -->
|
||||
<string name="profile_summary"><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g> is needed to manage your <xliff:g id="profile_name" example="watch">%2$s</xliff:g>. <xliff:g id="app_name2" example="Android Wear">%3$s</xliff:g> will get access to <xliff:g id="permissions" example="Notifications, Calendar and Phone">%4$s</xliff:g> while the <xliff:g id="profile_name2" example="watch">%5$s</xliff:g> is connected.</string>
|
||||
<string name="profile_summary"><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g> is needed to manage your <xliff:g id="profile_name" example="watch">%2$s</xliff:g>. <xliff:g id="privileges_discplaimer" example="Android Wear will get access to your Notifications, Calendar and Contacts.">%3$s</xliff:g></string>
|
||||
|
||||
<!-- Positive button for the device-app association consent dialog [CHAR LIMIT=30] -->
|
||||
<string name="consent_yes">Yes</string>
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
package com.android.companiondevicemanager;
|
||||
|
||||
import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress;
|
||||
import static android.text.TextUtils.emptyIfNull;
|
||||
import static android.text.TextUtils.withoutPrefix;
|
||||
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.app.Activity;
|
||||
import android.companion.AssociationRequest;
|
||||
import android.companion.CompanionDeviceManager;
|
||||
@@ -65,10 +67,7 @@ public class DeviceChooserActivity extends Activity {
|
||||
getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
|
||||
|
||||
String deviceProfile = getRequest().getDeviceProfile();
|
||||
String profileName = deviceProfile == null
|
||||
? getString(R.string.profile_name_generic)
|
||||
//TODO introduce PermissionController APIs to resolve UI values
|
||||
: withoutPrefix("android.app.role.COMPANION_DEVICE_", deviceProfile).toLowerCase();
|
||||
String profileName = getDeviceProfileName(deviceProfile);
|
||||
|
||||
if (getRequest().isSingleDevice()) {
|
||||
setContentView(R.layout.device_confirmation);
|
||||
@@ -112,15 +111,14 @@ public class DeviceChooserActivity extends Activity {
|
||||
TextView profileSummary = findViewById(R.id.profile_summary);
|
||||
|
||||
if (deviceProfile != null) {
|
||||
//TODO introduce PermissionController APIs to resolve UI values
|
||||
String privileges = "Notifications, Phone, Contacts and Calendar";
|
||||
String privacyDisclaimer = emptyIfNull(getRequest()
|
||||
.getDeviceProfilePrivilegesDescription())
|
||||
.replace("APP_NAME", getCallingAppName());
|
||||
profileSummary.setVisibility(View.VISIBLE);
|
||||
profileSummary.setText(getString(R.string.profile_summary,
|
||||
getCallingAppName(),
|
||||
profileName,
|
||||
getCallingAppName(),
|
||||
privileges,
|
||||
profileName));
|
||||
privacyDisclaimer));
|
||||
} else {
|
||||
profileSummary.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -135,6 +133,24 @@ public class DeviceChooserActivity extends Activity {
|
||||
return getService().mRequest;
|
||||
}
|
||||
|
||||
private String getDeviceProfileName(@Nullable String deviceProfile) {
|
||||
if (deviceProfile == null) {
|
||||
return getString(R.string.profile_name_generic);
|
||||
}
|
||||
switch (deviceProfile) {
|
||||
case AssociationRequest.DEVICE_PROFILE_WATCH: {
|
||||
return getString(R.string.profile_name_watch);
|
||||
}
|
||||
default: {
|
||||
Log.wtf(LOG_TAG,
|
||||
"No localized profile name found for device profile: " + deviceProfile);
|
||||
return withoutPrefix("android.app.role.COMPANION_DEVICE_", deviceProfile)
|
||||
.toLowerCase()
|
||||
.replace('_', ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void cancel() {
|
||||
getService().onCancel();
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
@@ -27,6 +27,7 @@ import static com.android.internal.util.Preconditions.checkState;
|
||||
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
|
||||
import static com.android.internal.util.function.pooled.PooledLambda.obtainRunnable;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||
|
||||
import android.annotation.CheckResult;
|
||||
@@ -72,6 +73,7 @@ import android.os.ShellCallback;
|
||||
import android.os.ShellCommand;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.permission.PermissionControllerManager;
|
||||
import android.provider.Settings;
|
||||
import android.provider.SettingsStringUtil.ComponentNameSet;
|
||||
import android.text.BidiFormatter;
|
||||
@@ -159,6 +161,7 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
|
||||
private AssociationRequest mRequest;
|
||||
private String mCallingPackage;
|
||||
private AndroidFuture<Association> mOngoingDeviceDiscovery;
|
||||
private PermissionControllerManager mPermissionControllerManager;
|
||||
|
||||
private BluetoothDeviceConnectedListener mBluetoothDeviceConnectedListener =
|
||||
new BluetoothDeviceConnectedListener();
|
||||
@@ -184,6 +187,8 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
|
||||
mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
|
||||
mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
|
||||
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
|
||||
mPermissionControllerManager = requireNonNull(
|
||||
context.getSystemService(PermissionControllerManager.class));
|
||||
|
||||
Intent serviceIntent = new Intent().setComponent(SERVICE_TO_BIND_TO);
|
||||
mServiceConnectors = new PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>>() {
|
||||
@@ -344,13 +349,25 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
|
||||
request.setCallingPackage(callingPackage);
|
||||
callback.asBinder().linkToDeath(CompanionDeviceManagerService.this /* recipient */, 0);
|
||||
|
||||
final long callingIdentity = Binder.clearCallingIdentity();
|
||||
try {
|
||||
mOngoingDeviceDiscovery = mServiceConnectors.forUser(userId).postAsync(service -> {
|
||||
AndroidFuture<String> fetchProfileDescription =
|
||||
request.getDeviceProfile() == null
|
||||
? AndroidFuture.completedFuture(null)
|
||||
: getDeviceProfilePermissionDescription(
|
||||
request.getDeviceProfile());
|
||||
|
||||
mOngoingDeviceDiscovery = fetchProfileDescription.thenComposeAsync(description -> {
|
||||
request.setDeviceProfilePrivilegesDescription(description);
|
||||
|
||||
return mServiceConnectors.forUser(userId).postAsync(service -> {
|
||||
AndroidFuture<Association> future = new AndroidFuture<>();
|
||||
service.startDiscovery(request, callingPackage, callback, future);
|
||||
return future;
|
||||
}).cancelTimeout().whenComplete(uncheckExceptions((association, err) -> {
|
||||
}).cancelTimeout();
|
||||
|
||||
}, FgThread.getExecutor()).whenComplete(uncheckExceptions((association, err) -> {
|
||||
|
||||
final long callingIdentity = Binder.clearCallingIdentity();
|
||||
try {
|
||||
if (err == null) {
|
||||
addAssociation(association);
|
||||
} else {
|
||||
@@ -358,10 +375,10 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
|
||||
callback.onFailure("No devices found: " + err.getMessage());
|
||||
}
|
||||
cleanup();
|
||||
}));
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(callingIdentity);
|
||||
}
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(callingIdentity);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -904,6 +921,19 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
|
||||
mCurrentlyConnectedDevices.remove(address);
|
||||
}
|
||||
|
||||
private AndroidFuture<String> getDeviceProfilePermissionDescription(String deviceProfile) {
|
||||
AndroidFuture<String> result = new AndroidFuture<>();
|
||||
mPermissionControllerManager.getPrivilegesDescriptionStringForProfile(
|
||||
deviceProfile, FgThread.getExecutor(), desc -> {
|
||||
try {
|
||||
result.complete(requireNonNull(desc));
|
||||
} catch (Exception e) {
|
||||
result.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private class ShellCmd extends ShellCommand {
|
||||
public static final String USAGE = "help\n"
|
||||
+ "list USER_ID\n"
|
||||
|
||||
Reference in New Issue
Block a user