diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 992d054737b9a..3c668b2aba5a6 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -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. diff --git a/core/java/android/view/translation/TranslationSpec.java b/core/java/android/view/translation/TranslationSpec.java index ab1bc477e0fd7..16418a78f2c1a 100644 --- a/core/java/android/view/translation/TranslationSpec.java +++ b/core/java/android/view/translation/TranslationSpec.java @@ -28,7 +28,7 @@ import com.android.internal.util.DataClass; *

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.)

*/ -@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() {} diff --git a/core/java/android/view/translation/Translator.java b/core/java/android/view/translation/Translator.java index 22c3e57ecc957..163f832882c5f 100644 --- a/core/java/android/view/translation/Translator.java +++ b/core/java/android/view/translation/Translator.java @@ -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. diff --git a/core/java/android/view/translation/UiTranslationController.java b/core/java/android/view/translation/UiTranslationController.java index b49d3c004f448..802f01fab445b 100644 --- a/core/java/android/view/translation/UiTranslationController.java +++ b/core/java/android/view/translation/UiTranslationController.java @@ -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. diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 8fd5d804adc05..1871e085feb4a 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -480,6 +480,8 @@ applications that come with the platform + + diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index a15ceb6d8811e..50c7e080209b5 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -163,6 +163,7 @@ + diff --git a/services/translation/java/com/android/server/translation/TranslationManagerService.java b/services/translation/java/com/android/server/translation/TranslationManagerService.java index 6aadd23d211fe..b6244b8fb93b0 100644 --- a/services/translation/java/com/android/server/translation/TranslationManagerService.java +++ b/services/translation/java/com/android/server/translation/TranslationManagerService.java @@ -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 diff --git a/services/translation/java/com/android/server/translation/TranslationManagerServiceShellCommand.java b/services/translation/java/com/android/server/translation/TranslationManagerServiceShellCommand.java new file mode 100644 index 0000000000000..ba1b3908a91e5 --- /dev/null +++ b/services/translation/java/com/android/server/translation/TranslationManagerServiceShellCommand.java @@ -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(""); + } + } +}