From 8dc9a795e0ec8de7dbcada76dfdd95b2f1f79401 Mon Sep 17 00:00:00 2001 From: Ahaan Ugale Date: Wed, 21 Apr 2021 13:00:43 -0700 Subject: [PATCH] Translate: API for content padding. This allows controlling the content padding behavior needed with some apps for compatibility reasons. (see I9d7db5d2bc200e4042baf3267796f00cfc298f19) The flag is currently the only property in the new UiTranslationSpec value type, but more properties can be added in the future (such as animation specifications). Bug: 179693024 Test: manual CTS-Coverage-Bug: 182990474 Change-Id: I11052cddb59c928b22ad9ad16e3c9b092da4b907 --- core/api/system-current.txt | 16 +- .../view/translation/ITranslationManager.aidl | 3 +- .../translation/UiTranslationManager.java | 33 ++- .../view/translation/UiTranslationSpec.aidl | 19 ++ .../view/translation/UiTranslationSpec.java | 248 ++++++++++++++++++ .../TranslationManagerService.java | 5 +- .../TranslationManagerServiceImpl.java | 4 +- 7 files changed, 317 insertions(+), 11 deletions(-) create mode 100644 core/java/android/view/translation/UiTranslationSpec.aidl create mode 100644 core/java/android/view/translation/UiTranslationSpec.java diff --git a/core/api/system-current.txt b/core/api/system-current.txt index d0326bb72a238..2b356aacf8349 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -14454,7 +14454,21 @@ package android.view.translation { method @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION) public void finishTranslation(@NonNull android.app.assist.ActivityId); method @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION) public void pauseTranslation(@NonNull android.app.assist.ActivityId); method @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION) public void resumeTranslation(@NonNull android.app.assist.ActivityId); - method @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION) public void startTranslation(@NonNull android.view.translation.TranslationSpec, @NonNull android.view.translation.TranslationSpec, @NonNull java.util.List, @NonNull android.app.assist.ActivityId); + method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION) public void startTranslation(@NonNull android.view.translation.TranslationSpec, @NonNull android.view.translation.TranslationSpec, @NonNull java.util.List, @NonNull android.app.assist.ActivityId); + method @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION) public void startTranslation(@NonNull android.view.translation.TranslationSpec, @NonNull android.view.translation.TranslationSpec, @NonNull java.util.List, @NonNull android.app.assist.ActivityId, @NonNull android.view.translation.UiTranslationSpec); + } + + public final class UiTranslationSpec implements android.os.Parcelable { + method public int describeContents(); + method public boolean shouldPadContentForCompat(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class UiTranslationSpec.Builder { + ctor public UiTranslationSpec.Builder(); + method @NonNull public android.view.translation.UiTranslationSpec build(); + method @NonNull public android.view.translation.UiTranslationSpec.Builder setShouldPadContentForCompat(boolean); } } diff --git a/core/java/android/view/translation/ITranslationManager.aidl b/core/java/android/view/translation/ITranslationManager.aidl index 560edecc9da60..7b4fb50077713 100644 --- a/core/java/android/view/translation/ITranslationManager.aidl +++ b/core/java/android/view/translation/ITranslationManager.aidl @@ -22,6 +22,7 @@ import android.os.ResultReceiver; import android.view.autofill.AutofillId; import android.view.translation.TranslationContext; import android.view.translation.TranslationSpec; +import android.view.translation.UiTranslationSpec; import com.android.internal.os.IResultReceiver; import java.util.List; @@ -39,7 +40,7 @@ oneway interface ITranslationManager { void updateUiTranslationState(int state, in TranslationSpec sourceSpec, in TranslationSpec targetSpec, in List viewIds, IBinder token, int taskId, - int userId); + in UiTranslationSpec uiTranslationSpec, int userId); void registerUiTranslationStateCallback(in IRemoteCallback callback, int userId); void unregisterUiTranslationStateCallback(in IRemoteCallback callback, int userId); diff --git a/core/java/android/view/translation/UiTranslationManager.java b/core/java/android/view/translation/UiTranslationManager.java index eefc7fdc7d775..541b4941c62e0 100644 --- a/core/java/android/view/translation/UiTranslationManager.java +++ b/core/java/android/view/translation/UiTranslationManager.java @@ -118,6 +118,23 @@ public final class UiTranslationManager { mService = service; } + /** + * @deprecated Use {@link #startTranslation(TranslationSpec, TranslationSpec, List, ActivityId, + * UiTranslationSpec)} instead. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION) + @Deprecated + @SystemApi + public void startTranslation(@NonNull TranslationSpec sourceSpec, + @NonNull TranslationSpec targetSpec, @NonNull List viewIds, + @NonNull ActivityId activityId) { + startTranslation( + sourceSpec, targetSpec, viewIds, activityId, + new UiTranslationSpec.Builder().setShouldPadContentForCompat(true).build()); + } + /** * Request ui translation for a given Views. * @@ -125,9 +142,8 @@ public final class UiTranslationManager { * @param targetSpec {@link TranslationSpec} for the translated data. * @param viewIds A list of the {@link View}'s {@link AutofillId} which needs to be translated * @param activityId the identifier for the Activity which needs ui translation + * @param uiTranslationSpec configuration for translation of the specified views * @throws IllegalArgumentException if the no {@link View}'s {@link AutofillId} in the list - * @throws NullPointerException the sourceSpec, targetSpec, viewIds, activityId or - * {@link android.app.assist.ActivityId#getToken()} is {@code null} * * @hide */ @@ -135,19 +151,21 @@ public final class UiTranslationManager { @SystemApi public void startTranslation(@NonNull TranslationSpec sourceSpec, @NonNull TranslationSpec targetSpec, @NonNull List viewIds, - @NonNull ActivityId activityId) { + @NonNull ActivityId activityId, @NonNull UiTranslationSpec uiTranslationSpec) { // TODO(b/177789967): Return result code or find a way to notify the status. Objects.requireNonNull(sourceSpec); Objects.requireNonNull(targetSpec); Objects.requireNonNull(viewIds); Objects.requireNonNull(activityId); Objects.requireNonNull(activityId.getToken()); + Objects.requireNonNull(uiTranslationSpec); if (viewIds.size() == 0) { throw new IllegalArgumentException("Invalid empty views: " + viewIds); } try { mService.updateUiTranslationState(STATE_UI_TRANSLATION_STARTED, sourceSpec, targetSpec, viewIds, activityId.getToken(), activityId.getTaskId(), + uiTranslationSpec, mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -172,7 +190,8 @@ public final class UiTranslationManager { Objects.requireNonNull(activityId.getToken()); mService.updateUiTranslationState(STATE_UI_TRANSLATION_FINISHED, null /* sourceSpec */, null /* targetSpec */, null /* viewIds */, - activityId.getToken(), activityId.getTaskId(), mContext.getUserId()); + activityId.getToken(), activityId.getTaskId(), null /* uiTranslationSpec */, + mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -196,7 +215,8 @@ public final class UiTranslationManager { Objects.requireNonNull(activityId.getToken()); mService.updateUiTranslationState(STATE_UI_TRANSLATION_PAUSED, null /* sourceSpec */, null /* targetSpec */, null /* viewIds */, - activityId.getToken(), activityId.getTaskId(), mContext.getUserId()); + activityId.getToken(), activityId.getTaskId(), null /* uiTranslationSpec */, + mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -220,7 +240,8 @@ public final class UiTranslationManager { Objects.requireNonNull(activityId.getToken()); mService.updateUiTranslationState(STATE_UI_TRANSLATION_RESUMED, null /* sourceSpec */, null /* targetSpec */, null /* viewIds */, - activityId.getToken(), activityId.getTaskId(), mContext.getUserId()); + activityId.getToken(), activityId.getTaskId(), null /* uiTranslationSpec */, + mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/view/translation/UiTranslationSpec.aidl b/core/java/android/view/translation/UiTranslationSpec.aidl new file mode 100644 index 0000000000000..7fbeb66389a55 --- /dev/null +++ b/core/java/android/view/translation/UiTranslationSpec.aidl @@ -0,0 +1,19 @@ +/* + * 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 android.view.translation; + +parcelable UiTranslationSpec; diff --git a/core/java/android/view/translation/UiTranslationSpec.java b/core/java/android/view/translation/UiTranslationSpec.java new file mode 100644 index 0000000000000..b43dbce312c39 --- /dev/null +++ b/core/java/android/view/translation/UiTranslationSpec.java @@ -0,0 +1,248 @@ +/* + * 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 android.view.translation; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.DataClass; + +/** + * Specifications for configuring UI translation. + * + * @hide + */ +@DataClass( + genBuilder = true, genEqualsHashCode = true, genHiddenConstDefs = true, genToString = true) +@DataClass.Suppress("isShouldPadContentForCompat") +@SystemApi +public final class UiTranslationSpec implements Parcelable { + + /** + * Whether the original content of the view should be directly modified to include padding that + * makes it the same size as the translated content. Defaults to {@code false}. + *

+ * For {@link android.widget.TextView}, the system does not directly modify the original text, + * rather changes the displayed content using a + * {@link android.text.method.TransformationMethod}. + * This can cause issues in apps that do not account for TransformationMethods. For example, an + * app using DynamicLayout may use the calculated text offsets to operate on the original text, + * but this can be problematic when the layout was calculated on translated text with a + * different length. + *

+ * If this is {@code true}, for a TextView the default implementation will append spaces to the + * text to make the length the same as the translated text. + */ + private boolean mShouldPadContentForCompat = false; + + /** + * Whether the original content of the view should be directly modified to include padding that + * makes it the same size as the translated content. + *

+ * For {@link android.widget.TextView}, the system does not directly modify the original text, + * rather changes the displayed content using a + * {@link android.text.method.TransformationMethod}. + * This can cause issues in apps that do not account for TransformationMethods. For example, an + * app using DynamicLayout may use the calculated text offsets to operate on the original text, + * but this can be problematic when the layout was calculated on translated text with a + * different length. + *

+ * If this is {@code true}, for a TextView the default implementation will append spaces to the + * text to make the length the same as the translated text. + */ + public boolean shouldPadContentForCompat() { + return mShouldPadContentForCompat; + } + + + + // Code below generated by codegen v1.0.23. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/UiTranslationSpec.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated.Member + /* package-private */ UiTranslationSpec( + boolean shouldPadContentForCompat) { + this.mShouldPadContentForCompat = shouldPadContentForCompat; + + // onConstructed(); // You can define this method to get a callback + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "UiTranslationSpec { " + + "shouldPadContentForCompat = " + mShouldPadContentForCompat + + " }"; + } + + @Override + @DataClass.Generated.Member + public boolean equals(@android.annotation.Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(UiTranslationSpec other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + UiTranslationSpec that = (UiTranslationSpec) o; + //noinspection PointlessBooleanExpression + return true + && mShouldPadContentForCompat == that.mShouldPadContentForCompat; + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + Boolean.hashCode(mShouldPadContentForCompat); + return _hash; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + byte flg = 0; + if (mShouldPadContentForCompat) flg |= 0x1; + dest.writeByte(flg); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ UiTranslationSpec(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + byte flg = in.readByte(); + boolean shouldPadContentForCompat = (flg & 0x1) != 0; + + this.mShouldPadContentForCompat = shouldPadContentForCompat; + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public UiTranslationSpec[] newArray(int size) { + return new UiTranslationSpec[size]; + } + + @Override + public UiTranslationSpec createFromParcel(@NonNull Parcel in) { + return new UiTranslationSpec(in); + } + }; + + /** + * A builder for {@link UiTranslationSpec} + */ + @SuppressWarnings("WeakerAccess") + @DataClass.Generated.Member + public static final class Builder { + + private boolean mShouldPadContentForCompat; + + private long mBuilderFieldsSet = 0L; + + public Builder() { + } + + /** + * Whether the original content of the view should be directly modified to include padding that + * makes it the same size as the translated content. Defaults to {@code false}. + *

+ * For {@link android.widget.TextView}, the system does not directly modify the original text, + * rather changes the displayed content using a + * {@link android.text.method.TransformationMethod}. + * This can cause issues in apps that do not account for TransformationMethods. For example, an + * app using DynamicLayout may use the calculated text offsets to operate on the original text, + * but this can be problematic when the layout was calculated on translated text with a + * different length. + *

+ * If this is {@code true}, for a TextView the default implementation will append spaces to the + * text to make the length the same as the translated text. + */ + @DataClass.Generated.Member + public @NonNull Builder setShouldPadContentForCompat(boolean value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x1; + mShouldPadContentForCompat = value; + return this; + } + + /** Builds the instance. This builder should not be touched after calling this! */ + public @NonNull UiTranslationSpec build() { + checkNotUsed(); + mBuilderFieldsSet |= 0x2; // Mark builder used + + if ((mBuilderFieldsSet & 0x1) == 0) { + mShouldPadContentForCompat = false; + } + UiTranslationSpec o = new UiTranslationSpec( + mShouldPadContentForCompat); + return o; + } + + private void checkNotUsed() { + if ((mBuilderFieldsSet & 0x2) != 0) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + } + } + + @DataClass.Generated( + time = 1619034161701L, + codegenVersion = "1.0.23", + sourceFile = "frameworks/base/core/java/android/view/translation/UiTranslationSpec.java", + inputSignatures = "private boolean mShouldPadContentForCompat\npublic boolean shouldPadContentForCompat()\nclass UiTranslationSpec extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genToString=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/services/translation/java/com/android/server/translation/TranslationManagerService.java b/services/translation/java/com/android/server/translation/TranslationManagerService.java index 6bba65dc36ae6..1242b7e522157 100644 --- a/services/translation/java/com/android/server/translation/TranslationManagerService.java +++ b/services/translation/java/com/android/server/translation/TranslationManagerService.java @@ -44,6 +44,7 @@ import android.view.translation.ITranslationManager; import android.view.translation.TranslationContext; import android.view.translation.TranslationSpec; import android.view.translation.UiTranslationManager.UiTranslationState; +import android.view.translation.UiTranslationSpec; import com.android.internal.annotations.GuardedBy; import com.android.internal.os.IResultReceiver; @@ -187,14 +188,14 @@ public final class TranslationManagerService @Override public void updateUiTranslationState(@UiTranslationState int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, List viewIds, - IBinder token, int taskId, int userId) { + IBinder token, int taskId, UiTranslationSpec uiTranslationSpec, int userId) { enforceCallerHasPermission(MANAGE_UI_TRANSLATION); synchronized (mLock) { final TranslationManagerServiceImpl service = getServiceForUserLocked(userId); if (service != null && (isDefaultServiceLocked(userId) || isCalledByServiceAppLocked(userId, "updateUiTranslationState"))) { service.updateUiTranslationStateLocked(state, sourceSpec, targetSpec, viewIds, - token, taskId); + token, taskId, uiTranslationSpec); } } } diff --git a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java index d2a69511f6eab..85296c62ab9ba 100644 --- a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java +++ b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java @@ -38,6 +38,7 @@ import android.view.inputmethod.InputMethodInfo; import android.view.translation.TranslationContext; import android.view.translation.TranslationSpec; import android.view.translation.UiTranslationManager.UiTranslationState; +import android.view.translation.UiTranslationSpec; import com.android.internal.annotations.GuardedBy; import com.android.internal.os.IResultReceiver; @@ -146,7 +147,7 @@ final class TranslationManagerServiceImpl extends @GuardedBy("mLock") public void updateUiTranslationStateLocked(@UiTranslationState int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, List viewIds, - IBinder token, int taskId) { + IBinder token, int taskId, UiTranslationSpec uiTranslationSpec) { // Get top activity for a given task id final ActivityTokens taskTopActivityTokens = mActivityTaskManagerInternal.getTopActivityForTask(taskId); @@ -157,6 +158,7 @@ final class TranslationManagerServiceImpl extends return; } try { + // TODO: Pipe uiTranslationSpec through to the UiTranslationController. taskTopActivityTokens.getApplicationThread().updateUiTranslationState( taskTopActivityTokens.getActivityToken(), state, sourceSpec, targetSpec, viewIds);