Add shell commands and dump information for Translation

1. Add shell command to allow a temporary override of the
TranslationService that helps e2e CTS tests.
2. Add permission to allow the shell call the UiTranslationManager
APIs for the tests.
3. Dump translation systemservice and activity translation related
information to help debugging

We don't want to leak translation feature now, so we submit the
change about Shell in internal branch.

Bug: 179047265
Test: manual.
1. Switch to temp TranslationService by "adb shell cmd transformer set
<myTranslationService> 12000" and make sure only the same app can
access the APIs when switching to the temp service.
2. adb shell dumpsys transformer
3. adb shell dumpsys activity <myactivity> --translation
(We set "transformer" now to prevent feature leak, the final value is
translation)

Change-Id: I2573b7ea226bd805a951f8346d5370d9c9776364
This commit is contained in:
Joanne Chung
2021-02-26 18:53:36 +08:00
parent b8737dac2c
commit 3a907ab1eb
8 changed files with 235 additions and 6 deletions

View File

@@ -7100,6 +7100,9 @@ public class Activity extends ContextThemeWrapper
case "--contentcapture":
dumpContentCaptureManager(prefix, writer);
return;
case "--translation":
dumpUiTranslation(prefix, writer);
return;
}
}
writer.print(prefix); writer.print("Local Activity ");
@@ -7140,6 +7143,7 @@ public class Activity extends ContextThemeWrapper
dumpAutofillManager(prefix, writer);
dumpContentCaptureManager(prefix, writer);
dumpUiTranslation(prefix, writer);
ResourcesManager.getInstance().dump(prefix, writer);
}
@@ -7164,6 +7168,14 @@ public class Activity extends ContextThemeWrapper
}
}
void dumpUiTranslation(String prefix, PrintWriter writer) {
if (mUiTranslationController != null) {
mUiTranslationController.dump(prefix, writer);
} else {
writer.print(prefix); writer.println("No UiTranslationController");
}
}
/**
* Bit indicating that this activity is "immersive" and should not be
* interrupted by notifications if possible.

View File

@@ -28,7 +28,7 @@ import com.android.internal.util.DataClass;
* <p>This spec help specify information such as the language/locale for the translation, as well
* as the data format for the translation (text, audio, etc.)</p>
*/
@DataClass(genEqualsHashCode = true, genHiddenConstDefs = true)
@DataClass(genEqualsHashCode = true, genHiddenConstDefs = true, genToString = true)
public final class TranslationSpec implements Parcelable {
/** Data format for translation is text. */
@@ -97,6 +97,18 @@ public final class TranslationSpec implements Parcelable {
return mDataFormat;
}
@Override
@DataClass.Generated.Member
public String toString() {
// You can override field toString logic by defining methods like:
// String fieldNameToString() { ... }
return "TranslationSpec { " +
"language = " + mLanguage + ", " +
"dataFormat = " + mDataFormat +
" }";
}
@Override
@DataClass.Generated.Member
public boolean equals(@android.annotation.Nullable Object o) {
@@ -175,10 +187,10 @@ public final class TranslationSpec implements Parcelable {
};
@DataClass.Generated(
time = 1609964630624L,
time = 1614326090637L,
codegenVersion = "1.0.22",
sourceFile = "frameworks/base/core/java/android/view/translation/TranslationSpec.java",
inputSignatures = "public static final int DATA_FORMAT_TEXT\nprivate final @android.annotation.NonNull java.lang.String mLanguage\nprivate final @android.view.translation.TranslationSpec.DataFormat int mDataFormat\nclass TranslationSpec extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genHiddenConstDefs=true)")
inputSignatures = "public static final int DATA_FORMAT_TEXT\nprivate final @android.annotation.NonNull java.lang.String mLanguage\nprivate final @android.view.translation.TranslationSpec.DataFormat int mDataFormat\nclass TranslationSpec extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genHiddenConstDefs=true, genToString=true)")
@Deprecated
private void __metadata() {}

View File

@@ -35,6 +35,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.IResultReceiver;
import com.android.internal.util.SyncResultReceiver;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
@@ -221,6 +222,12 @@ public class Translator {
return mId;
}
/** @hide */
public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
pw.print(prefix); pw.print("sourceSpec: "); pw.println(mSourceSpec);
pw.print(prefix); pw.print("destSpec: "); pw.println(mDestSpec);
}
/**
* Requests a translation for the provided {@link TranslationRequest} using the Translator's
* source spec and destination spec.

View File

@@ -37,6 +37,7 @@ import android.view.translation.UiTranslationManager.UiTranslationState;
import com.android.internal.util.function.pooled.PooledLambda;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
@@ -129,6 +130,23 @@ public class UiTranslationController {
}
}
/**
* Called to dump the translation information for Activity.
*/
public void dump(String outerPrefix, PrintWriter pw) {
pw.print(outerPrefix); pw.println("UiTranslationController:");
final String pfx = outerPrefix + " ";
pw.print(pfx); pw.print("activity: "); pw.println(mActivity);
final int translatorSize = mTranslators.size();
pw.print(outerPrefix); pw.print("number translator: "); pw.println(translatorSize);
for (int i = 0; i < translatorSize; i++) {
pw.print(outerPrefix); pw.print("#"); pw.println(i);
final Translator translator = mTranslators.valueAt(i);
translator.dump(outerPrefix, pw);
pw.println();
}
}
/**
* The method is used by {@link Translator}, it will be called when the translation is done. The
* translation result can be get from here.

View File

@@ -480,6 +480,8 @@ applications that come with the platform
<permission name="android.permission.SIGNAL_REBOOT_READINESS" />
<!-- Permission required for CTS test - PeopleManagerTest -->
<permission name="android.permission.READ_PEOPLE_DATA" />
<!-- Permission required for CTS test - UiTranslationManagerTest -->
<permission name="android.permission.MANAGE_UI_TRANSLATION" />
</privapp-permissions>
<privapp-permissions package="com.android.statementservice">

View File

@@ -163,6 +163,7 @@
<uses-permission android:name="android.permission.MANAGE_APP_PREDICTIONS" />
<uses-permission android:name="android.permission.MANAGE_SEARCH_UI" />
<uses-permission android:name="android.permission.MANAGE_SMARTSPACE" />
<uses-permission android:name="android.permission.MANAGE_UI_TRANSLATION" />
<uses-permission android:name="android.permission.NETWORK_SETTINGS" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.SET_TIME" />

View File

@@ -20,19 +20,28 @@ import static android.Manifest.permission.MANAGE_UI_TRANSLATION;
import static android.content.Context.TRANSLATION_MANAGER_SERVICE;
import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_FAIL;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.util.Slog;
import android.view.autofill.AutofillId;
import android.view.translation.ITranslationManager;
import android.view.translation.TranslationSpec;
import android.view.translation.UiTranslationManager.UiTranslationState;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.IResultReceiver;
import com.android.server.infra.AbstractMasterSystemService;
import com.android.server.infra.FrameworkResourcesServiceNameResolver;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.List;
/**
@@ -48,6 +57,8 @@ public final class TranslationManagerService
private static final String TAG = "TranslationManagerService";
private static final int MAX_TEMP_SERVICE_SUBSTITUTION_DURATION_MS = 2 * 60_000; // 2 minutes
public TranslationManagerService(Context context) {
// TODO: Discuss the disallow policy
super(context, new FrameworkResourcesServiceNameResolver(context,
@@ -60,19 +71,82 @@ public final class TranslationManagerService
return new TranslationManagerServiceImpl(this, mLock, resolvedUserId, disabled);
}
@Override
protected void enforceCallingPermissionForManagement() {
getContext().enforceCallingPermission(MANAGE_UI_TRANSLATION, TAG);
}
@Override
protected int getMaximumTemporaryServiceDurationMs() {
return MAX_TEMP_SERVICE_SUBSTITUTION_DURATION_MS;
}
@Override
protected void dumpLocked(String prefix, PrintWriter pw) {
super.dumpLocked(prefix, pw);
}
private void enforceCallerHasPermission(String permission) {
final String msg = "Permission Denial from pid =" + Binder.getCallingPid() + ", uid="
+ Binder.getCallingUid() + " doesn't hold " + permission;
getContext().enforceCallingPermission(permission, msg);
}
/** True if the currently set handler service is not overridden by the shell. */
@GuardedBy("mLock")
private boolean isDefaultServiceLocked(int userId) {
final String defaultServiceName = mServiceNameResolver.getDefaultServiceName(userId);
if (defaultServiceName == null) {
return false;
}
final String currentServiceName = mServiceNameResolver.getServiceName(userId);
return defaultServiceName.equals(currentServiceName);
}
/** True if the caller of the api is the same app which hosts the TranslationService. */
@GuardedBy("mLock")
private boolean isCalledByServiceAppLocked(int userId, @NonNull String methodName) {
final int callingUid = Binder.getCallingUid();
final String serviceName = mServiceNameResolver.getServiceName(userId);
if (serviceName == null) {
Slog.e(TAG, methodName + ": called by UID " + callingUid
+ ", but there's no service set for user " + userId);
return false;
}
final ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName);
if (serviceComponent == null) {
Slog.w(TAG, methodName + ": invalid service name: " + serviceName);
return false;
}
final String servicePackageName = serviceComponent.getPackageName();
final PackageManager pm = getContext().getPackageManager();
final int serviceUid;
try {
serviceUid = pm.getPackageUidAsUser(servicePackageName, userId);
} catch (PackageManager.NameNotFoundException e) {
Slog.w(TAG, methodName + ": could not verify UID for " + serviceName);
return false;
}
if (callingUid != serviceUid) {
Slog.e(TAG, methodName + ": called by UID " + callingUid + ", but service UID is "
+ serviceUid);
return false;
}
return true;
}
final class TranslationManagerServiceStub extends ITranslationManager.Stub {
@Override
public void getSupportedLocales(IResultReceiver receiver, int userId)
throws RemoteException {
synchronized (mLock) {
final TranslationManagerServiceImpl service = getServiceForUserLocked(userId);
if (service != null) {
if (service != null && (isDefaultServiceLocked(userId)
|| isCalledByServiceAppLocked(userId, "getSupportedLocales"))) {
service.getSupportedLocalesLocked(receiver);
} else {
Slog.v(TAG, "getSupportedLocales(): no service for " + userId);
@@ -86,7 +160,8 @@ public final class TranslationManagerService
int sessionId, IResultReceiver receiver, int userId) throws RemoteException {
synchronized (mLock) {
final TranslationManagerServiceImpl service = getServiceForUserLocked(userId);
if (service != null) {
if (service != null && (isDefaultServiceLocked(userId)
|| isCalledByServiceAppLocked(userId, "onSessionCreated"))) {
service.onSessionCreatedLocked(sourceSpec, destSpec, sessionId, receiver);
} else {
Slog.v(TAG, "onSessionCreated(): no service for " + userId);
@@ -102,12 +177,35 @@ public final class TranslationManagerService
enforceCallerHasPermission(MANAGE_UI_TRANSLATION);
synchronized (mLock) {
final TranslationManagerServiceImpl service = getServiceForUserLocked(userId);
if (service != null) {
if (service != null && (isDefaultServiceLocked(userId)
|| isCalledByServiceAppLocked(userId, "updateUiTranslationState"))) {
service.updateUiTranslationState(state, sourceSpec, destSpec, viewIds,
taskId);
}
}
}
/**
* Dump the service state into the given stream. You run "adb shell dumpsys translation".
*/
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
synchronized (mLock) {
dumpLocked("", pw);
}
}
@Override
public void onShellCommand(@Nullable FileDescriptor in,
@Nullable FileDescriptor out,
@Nullable FileDescriptor err,
@NonNull String[] args,
@Nullable ShellCallback callback,
@NonNull ResultReceiver resultReceiver) throws RemoteException {
new TranslationManagerServiceShellCommand(
TranslationManagerService.this).exec(this, in, out, err, args, callback,
resultReceiver);
}
}
@Override // from SystemService

View File

@@ -0,0 +1,79 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.translation;
import android.os.ShellCommand;
import java.io.PrintWriter;
/** Handles adb shell commands send to TranslationManagerService. */
public class TranslationManagerServiceShellCommand extends ShellCommand {
private final TranslationManagerService mService;
TranslationManagerServiceShellCommand(TranslationManagerService service) {
mService = service;
}
@Override
public int onCommand(String cmd) {
if (cmd == null) {
return handleDefaultCommands(cmd);
}
final PrintWriter pw = getOutPrintWriter();
if ("set".equals(cmd)) {
return requestSet(pw);
}
return handleDefaultCommands(cmd);
}
private int requestSet(PrintWriter pw) {
final String what = getNextArgRequired();
if ("temporary-service".equals(what)) {
return setTemporaryService(pw);
}
pw.println("Invalid set: " + what);
return -1;
}
private int setTemporaryService(PrintWriter pw) {
final int userId = Integer.parseInt(getNextArgRequired());
final String serviceName = getNextArg();
if (serviceName == null) {
mService.resetTemporaryService(userId);
return 0;
}
final int duration = Integer.parseInt(getNextArgRequired());
mService.setTemporaryService(userId, serviceName, duration);
pw.println("TranslationService temporarily set to " + serviceName + " for "
+ duration + "ms");
return 0;
}
@Override
public void onHelp() {
try (PrintWriter pw = getOutPrintWriter();) {
pw.println("Translation Service (translation) commands:");
pw.println(" help");
pw.println(" Prints this help text.");
pw.println("");
pw.println(" set temporary-service USER_ID [COMPONENT_NAME DURATION]");
pw.println(" Temporarily (for DURATION ms) changes the service implementation.");
pw.println(" To reset, call with just the USER_ID argument.");
pw.println("");
}
}
}