diff --git a/core/api/system-current.txt b/core/api/system-current.txt index e74efd1d1482d..51165b5f1feaf 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -14519,7 +14519,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 c2be9ffca2d63..5fbf228ec72fc 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; @@ -41,7 +42,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 31581df4e95e8..628f8cdc8f0c2 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; @@ -209,14 +210,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 0a23c827e6890..4198d3b86500d 100644 --- a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java +++ b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java @@ -41,6 +41,7 @@ import android.view.translation.TranslationCapability; 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; @@ -163,7 +164,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); @@ -174,6 +175,7 @@ final class TranslationManagerServiceImpl extends return; } try { + // TODO: Pipe uiTranslationSpec through to the UiTranslationController. taskTopActivityTokens.getApplicationThread().updateUiTranslationState( taskTopActivityTokens.getActivityToken(), state, sourceSpec, targetSpec, viewIds);