diff options
34 files changed, 2749 insertions, 0 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 2464ddad8607..419c18a0730a 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -12325,6 +12325,7 @@ package android.content.pm { field public static final String FEATURE_TOUCHSCREEN_MULTITOUCH = "android.hardware.touchscreen.multitouch"; field public static final String FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT = "android.hardware.touchscreen.multitouch.distinct"; field public static final String FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND = "android.hardware.touchscreen.multitouch.jazzhand"; + field public static final String FEATURE_TRANSLATION = "android.software.translation"; field public static final String FEATURE_USB_ACCESSORY = "android.hardware.usb.accessory"; field public static final String FEATURE_USB_HOST = "android.hardware.usb.host"; field public static final String FEATURE_VERIFIED_BOOT = "android.software.verified_boot"; @@ -51524,6 +51525,66 @@ package android.view.textservice { } +package android.view.translation { + + public final class TranslationManager { + method @Nullable @WorkerThread public android.view.translation.Translator createTranslator(@NonNull android.view.translation.TranslationSpec, @NonNull android.view.translation.TranslationSpec); + method @NonNull @WorkerThread public java.util.List<java.lang.String> getSupportedLocales(); + } + + public final class TranslationRequest implements android.os.Parcelable { + ctor public TranslationRequest(@Nullable CharSequence); + method public int describeContents(); + method @Nullable public android.view.autofill.AutofillId getAutofillId(); + method @Nullable public CharSequence getTranslationText(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.view.translation.TranslationRequest> CREATOR; + } + + public static final class TranslationRequest.Builder { + ctor public TranslationRequest.Builder(); + method @NonNull public android.view.translation.TranslationRequest build(); + method @NonNull public android.view.translation.TranslationRequest.Builder setAutofillId(@NonNull android.view.autofill.AutofillId); + method @NonNull public android.view.translation.TranslationRequest.Builder setTranslationText(@NonNull CharSequence); + } + + public final class TranslationResponse implements android.os.Parcelable { + method public int describeContents(); + method public int getTranslationStatus(); + method @NonNull public java.util.List<android.view.translation.TranslationRequest> getTranslations(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.view.translation.TranslationResponse> CREATOR; + field public static final int TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE = 2; // 0x2 + field public static final int TRANSLATION_STATUS_SUCCESS = 0; // 0x0 + field public static final int TRANSLATION_STATUS_UNKNOWN_ERROR = 1; // 0x1 + } + + public static final class TranslationResponse.Builder { + ctor public TranslationResponse.Builder(int); + method @NonNull public android.view.translation.TranslationResponse.Builder addTranslations(@NonNull android.view.translation.TranslationRequest); + method @NonNull public android.view.translation.TranslationResponse build(); + method @NonNull public android.view.translation.TranslationResponse.Builder setTranslationStatus(int); + method @NonNull public android.view.translation.TranslationResponse.Builder setTranslations(@NonNull java.util.List<android.view.translation.TranslationRequest>); + } + + public final class TranslationSpec implements android.os.Parcelable { + ctor public TranslationSpec(@NonNull String, int); + method public int describeContents(); + method public int getDataFormat(); + method @NonNull public String getLanguage(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.view.translation.TranslationSpec> CREATOR; + field public static final int DATA_FORMAT_TEXT = 1; // 0x1 + } + + public class Translator { + method public void destroy(); + method public boolean isDestroyed(); + method @Nullable @WorkerThread public android.view.translation.TranslationResponse translate(@NonNull android.view.translation.TranslationRequest); + } + +} + package android.webkit { public abstract class ClientCertRequest { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 8d748e6370d2..7bbfe9b5ab80 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -52,6 +52,7 @@ package android { field public static final String BIND_TELEPHONY_NETWORK_SERVICE = "android.permission.BIND_TELEPHONY_NETWORK_SERVICE"; field public static final String BIND_TEXTCLASSIFIER_SERVICE = "android.permission.BIND_TEXTCLASSIFIER_SERVICE"; field public static final String BIND_TIME_ZONE_PROVIDER_SERVICE = "android.permission.BIND_TIME_ZONE_PROVIDER_SERVICE"; + field public static final String BIND_TRANSLATION_SERVICE = "android.permission.BIND_TRANSLATION_SERVICE"; field public static final String BIND_TRUST_AGENT = "android.permission.BIND_TRUST_AGENT"; field public static final String BIND_TV_REMOTE_SERVICE = "android.permission.BIND_TV_REMOTE_SERVICE"; field public static final String BRICK = "android.permission.BRICK"; @@ -1935,6 +1936,7 @@ package android.content { field public static final String SYSTEM_CONFIG_SERVICE = "system_config"; field public static final String SYSTEM_UPDATE_SERVICE = "system_update"; field public static final String TETHERING_SERVICE = "tethering"; + field public static final String TRANSLATION_MANAGER_SERVICE = "transformer"; field public static final String VR_SERVICE = "vrmanager"; field public static final String WIFI_NL80211_SERVICE = "wifinl80211"; field @Deprecated public static final String WIFI_RTT_SERVICE = "rttmanager"; @@ -9795,6 +9797,48 @@ package android.service.timezone { } +package android.service.translation { + + public final class TranslationRequest implements android.os.Parcelable { + ctor public TranslationRequest(int, @NonNull android.view.translation.TranslationSpec, @NonNull android.view.translation.TranslationSpec, @NonNull java.util.List<android.view.translation.TranslationRequest>); + method public int describeContents(); + method @NonNull public android.view.translation.TranslationSpec getDestSpec(); + method public int getRequestId(); + method @NonNull public android.view.translation.TranslationSpec getSourceSpec(); + method @NonNull public java.util.List<android.view.translation.TranslationRequest> getTranslationRequests(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.service.translation.TranslationRequest> CREATOR; + } + + public static final class TranslationRequest.Builder { + ctor public TranslationRequest.Builder(int, @NonNull android.view.translation.TranslationSpec, @NonNull android.view.translation.TranslationSpec, @NonNull java.util.List<android.view.translation.TranslationRequest>); + method @NonNull public android.service.translation.TranslationRequest.Builder addTranslationRequests(@NonNull android.view.translation.TranslationRequest); + method @NonNull public android.service.translation.TranslationRequest build(); + method @NonNull public android.service.translation.TranslationRequest.Builder setDestSpec(@NonNull android.view.translation.TranslationSpec); + method @NonNull public android.service.translation.TranslationRequest.Builder setRequestId(int); + method @NonNull public android.service.translation.TranslationRequest.Builder setSourceSpec(@NonNull android.view.translation.TranslationSpec); + method @NonNull public android.service.translation.TranslationRequest.Builder setTranslationRequests(@NonNull java.util.List<android.view.translation.TranslationRequest>); + } + + public abstract class TranslationService extends android.app.Service { + ctor public TranslationService(); + method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent); + method public void onConnected(); + method public abstract void onCreateTranslationSession(@NonNull android.view.translation.TranslationSpec, @NonNull android.view.translation.TranslationSpec, int); + method public void onDisconnected(); + method public abstract void onFinishTranslationSession(int); + method public abstract void onTranslationRequest(@NonNull android.service.translation.TranslationRequest, int, @NonNull android.os.CancellationSignal, @NonNull android.service.translation.TranslationService.OnTranslationResultCallback); + field public static final String SERVICE_INTERFACE = "android.service.translation.TranslationService"; + field public static final String SERVICE_META_DATA = "android.translation_service"; + } + + public static interface TranslationService.OnTranslationResultCallback { + method public void onError(); + method public void onTranslationSuccess(@NonNull android.view.translation.TranslationResponse); + } + +} + package android.service.trust { public class TrustAgentService extends android.app.Service { diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index abe7fdae0bf7..d9f4e41a4aca 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4508,6 +4508,17 @@ public abstract class Context { public static final String CONTENT_CAPTURE_MANAGER_SERVICE = "content_capture"; /** + * Official published name of the translation service. + * + * @hide + * @see #getSystemService(String) + */ + // TODO(b/176208267): change it back to translation before S release. + @SystemApi + @SuppressLint("ServiceName") + public static final String TRANSLATION_MANAGER_SERVICE = "transformer"; + + /** * Used for getting content selections and classifications for task snapshots. * * @hide diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index e074eab99a7c..17c4d25d82d7 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -3406,6 +3406,14 @@ public abstract class PackageManager { /** * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: + * The device supports translation of text-to-text in multiple languages via integration with + * the system {@link android.service.translation.TranslationService translation provider}. + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_TRANSLATION = "android.software.translation"; + + /** + * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: * The device implements headtracking suitable for a VR device. */ @SdkConstant(SdkConstantType.FEATURE) diff --git a/core/java/android/service/translation/ITranslationCallback.aidl b/core/java/android/service/translation/ITranslationCallback.aidl new file mode 100644 index 000000000000..333cb577f790 --- /dev/null +++ b/core/java/android/service/translation/ITranslationCallback.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2020 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.service.translation; + +import android.view.translation.TranslationResponse; + +/** + * Interface to receive the result of a {@code TranslationRequest}. + * + * @hide + */ +oneway interface ITranslationCallback { + void onTranslationComplete(in TranslationResponse translationResponse); + void onError(); +} diff --git a/core/java/android/service/translation/ITranslationService.aidl b/core/java/android/service/translation/ITranslationService.aidl new file mode 100644 index 000000000000..6d6f2782ef4b --- /dev/null +++ b/core/java/android/service/translation/ITranslationService.aidl @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2020 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.service.translation; + +import android.service.translation.TranslationRequest; +import android.service.translation.ITranslationCallback; +import android.view.translation.TranslationSpec; +import com.android.internal.os.IResultReceiver; + +/** + * System-wide on-device translation service. + * + * <p>Services requests to translate text between different languages. The primary use case for this + * service is automatic translation of text and web views, when the auto Translate feature is + * enabled. + * + * @hide + */ +oneway interface ITranslationService { + void onConnected(); + void onDisconnected(); + void onCreateTranslationSession(in TranslationSpec sourceSpec, in TranslationSpec destSpec, + int sessionId, in IResultReceiver receiver); +} diff --git a/core/java/android/service/translation/OWNERS b/core/java/android/service/translation/OWNERS new file mode 100644 index 000000000000..a1e663aa8ff7 --- /dev/null +++ b/core/java/android/service/translation/OWNERS @@ -0,0 +1,8 @@ +# Bug component: 994311 + +adamhe@google.com +augale@google.com +joannechung@google.com +lpeter@google.com +svetoslavganov@google.com +tymtsai@google.com diff --git a/core/java/android/service/translation/OnTranslationResultCallbackWrapper.java b/core/java/android/service/translation/OnTranslationResultCallbackWrapper.java new file mode 100644 index 000000000000..345c69c0935d --- /dev/null +++ b/core/java/android/service/translation/OnTranslationResultCallbackWrapper.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2020 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.service.translation; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.DeadObjectException; +import android.os.RemoteException; +import android.util.Log; +import android.view.translation.TranslationResponse; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Callback to receive the {@link TranslationResponse} on successful translation. + * + * @hide + */ +final class OnTranslationResultCallbackWrapper implements + TranslationService.OnTranslationResultCallback { + + private static final String TAG = "OnTranslationResultCallback"; + + private final @NonNull ITranslationCallback mCallback; + + private AtomicBoolean mCalled; + + /** + * @hide + */ + public OnTranslationResultCallbackWrapper(@NonNull ITranslationCallback callback) { + mCallback = Objects.requireNonNull(callback); + mCalled = new AtomicBoolean(); + } + + @Override + public void onTranslationSuccess(@Nullable TranslationResponse response) { + assertNotCalled(); + if (mCalled.getAndSet(true)) { + throw new IllegalStateException("Already called"); + } + + try { + mCallback.onTranslationComplete(response); + } catch (RemoteException e) { + if (e instanceof DeadObjectException) { + Log.w(TAG, "Process is dead, ignore."); + return; + } + throw e.rethrowAsRuntimeException(); + } + } + + @Override + public void onError() { + assertNotCalled(); + if (mCalled.getAndSet(true)) { + throw new IllegalStateException("Already called"); + } + + try { + mCallback.onError(); + } catch (RemoteException e) { + if (e instanceof DeadObjectException) { + Log.w(TAG, "Process is dead, ignore."); + return; + } + throw e.rethrowAsRuntimeException(); + } + } + + private void assertNotCalled() { + if (mCalled.get()) { + throw new IllegalStateException("Already called"); + } + } +} diff --git a/core/java/android/service/translation/TranslationRequest.aidl b/core/java/android/service/translation/TranslationRequest.aidl new file mode 100644 index 000000000000..9a2d4157696e --- /dev/null +++ b/core/java/android/service/translation/TranslationRequest.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 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.service.translation; + +parcelable TranslationRequest; diff --git a/core/java/android/service/translation/TranslationRequest.java b/core/java/android/service/translation/TranslationRequest.java new file mode 100644 index 000000000000..b8afd7049a82 --- /dev/null +++ b/core/java/android/service/translation/TranslationRequest.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2020 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.service.translation; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.translation.TranslationSpec; + +import com.android.internal.util.DataClass; + +import java.util.ArrayList; +import java.util.List; + +/** + * Internal translation request sent to the {@link android.service.translation.TranslationService} + * which contains the text to be translated. + * + * @hide + */ +@SystemApi +@DataClass(genConstructor = true, genBuilder = true, genToString = true) +public final class TranslationRequest implements Parcelable { + + private final int mRequestId; + @NonNull + private final TranslationSpec mSourceSpec; + @NonNull + private final TranslationSpec mDestSpec; + @NonNull + private final List<android.view.translation.TranslationRequest> mTranslationRequests; + + + + // Code below generated by codegen v1.0.22. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/translation/TranslationRequest.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated.Member + public TranslationRequest( + int requestId, + @NonNull TranslationSpec sourceSpec, + @NonNull TranslationSpec destSpec, + @NonNull List<android.view.translation.TranslationRequest> translationRequests) { + this.mRequestId = requestId; + this.mSourceSpec = sourceSpec; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mSourceSpec); + this.mDestSpec = destSpec; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mDestSpec); + this.mTranslationRequests = translationRequests; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mTranslationRequests); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public int getRequestId() { + return mRequestId; + } + + @DataClass.Generated.Member + public @NonNull TranslationSpec getSourceSpec() { + return mSourceSpec; + } + + @DataClass.Generated.Member + public @NonNull TranslationSpec getDestSpec() { + return mDestSpec; + } + + @DataClass.Generated.Member + public @NonNull List<android.view.translation.TranslationRequest> getTranslationRequests() { + return mTranslationRequests; + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "TranslationRequest { " + + "requestId = " + mRequestId + ", " + + "sourceSpec = " + mSourceSpec + ", " + + "destSpec = " + mDestSpec + ", " + + "translationRequests = " + mTranslationRequests + + " }"; + } + + @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) { ... } + + dest.writeInt(mRequestId); + dest.writeTypedObject(mSourceSpec, flags); + dest.writeTypedObject(mDestSpec, flags); + dest.writeParcelableList(mTranslationRequests, flags); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ TranslationRequest(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + int requestId = in.readInt(); + TranslationSpec sourceSpec = (TranslationSpec) in.readTypedObject(TranslationSpec.CREATOR); + TranslationSpec destSpec = (TranslationSpec) in.readTypedObject(TranslationSpec.CREATOR); + List<android.view.translation.TranslationRequest> translationRequests = new ArrayList<>(); + in.readParcelableList(translationRequests, android.view.translation.TranslationRequest.class.getClassLoader()); + + this.mRequestId = requestId; + this.mSourceSpec = sourceSpec; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mSourceSpec); + this.mDestSpec = destSpec; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mDestSpec); + this.mTranslationRequests = translationRequests; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mTranslationRequests); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<TranslationRequest> CREATOR + = new Parcelable.Creator<TranslationRequest>() { + @Override + public TranslationRequest[] newArray(int size) { + return new TranslationRequest[size]; + } + + @Override + public TranslationRequest createFromParcel(@NonNull Parcel in) { + return new TranslationRequest(in); + } + }; + + /** + * A builder for {@link TranslationRequest} + */ + @SuppressWarnings("WeakerAccess") + @DataClass.Generated.Member + public static final class Builder { + + private int mRequestId; + private @NonNull TranslationSpec mSourceSpec; + private @NonNull TranslationSpec mDestSpec; + private @NonNull List<android.view.translation.TranslationRequest> mTranslationRequests; + + private long mBuilderFieldsSet = 0L; + + public Builder( + int requestId, + @NonNull TranslationSpec sourceSpec, + @NonNull TranslationSpec destSpec, + @NonNull List<android.view.translation.TranslationRequest> translationRequests) { + mRequestId = requestId; + mSourceSpec = sourceSpec; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mSourceSpec); + mDestSpec = destSpec; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mDestSpec); + mTranslationRequests = translationRequests; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mTranslationRequests); + } + + @DataClass.Generated.Member + public @NonNull Builder setRequestId(int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x1; + mRequestId = value; + return this; + } + + @DataClass.Generated.Member + public @NonNull Builder setSourceSpec(@NonNull TranslationSpec value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x2; + mSourceSpec = value; + return this; + } + + @DataClass.Generated.Member + public @NonNull Builder setDestSpec(@NonNull TranslationSpec value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x4; + mDestSpec = value; + return this; + } + + @DataClass.Generated.Member + public @NonNull Builder setTranslationRequests(@NonNull List<android.view.translation.TranslationRequest> value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x8; + mTranslationRequests = value; + return this; + } + + /** @see #setTranslationRequests */ + @DataClass.Generated.Member + public @NonNull Builder addTranslationRequests(@NonNull android.view.translation.TranslationRequest value) { + // You can refine this method's name by providing item's singular name, e.g.: + // @DataClass.PluralOf("item")) mItems = ... + + if (mTranslationRequests == null) setTranslationRequests(new ArrayList<>()); + mTranslationRequests.add(value); + return this; + } + + /** Builds the instance. This builder should not be touched after calling this! */ + public @NonNull TranslationRequest build() { + checkNotUsed(); + mBuilderFieldsSet |= 0x10; // Mark builder used + + TranslationRequest o = new TranslationRequest( + mRequestId, + mSourceSpec, + mDestSpec, + mTranslationRequests); + return o; + } + + private void checkNotUsed() { + if ((mBuilderFieldsSet & 0x10) != 0) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + } + } + + @DataClass.Generated( + time = 1609966181888L, + codegenVersion = "1.0.22", + sourceFile = "frameworks/base/core/java/android/service/translation/TranslationRequest.java", + inputSignatures = "private final int mRequestId\nprivate final @android.annotation.NonNull android.view.translation.TranslationSpec mSourceSpec\nprivate final @android.annotation.NonNull android.view.translation.TranslationSpec mDestSpec\nprivate final @android.annotation.NonNull java.util.List<android.view.translation.TranslationRequest> mTranslationRequests\nclass TranslationRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=true, genBuilder=true, genToString=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/service/translation/TranslationService.java b/core/java/android/service/translation/TranslationService.java new file mode 100644 index 000000000000..b0288076376c --- /dev/null +++ b/core/java/android/service/translation/TranslationService.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2020 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.service.translation; + +import static android.view.translation.Translator.EXTRA_SERVICE_BINDER; +import static android.view.translation.Translator.EXTRA_SESSION_ID; + +import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; + +import android.annotation.CallSuper; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.Service; +import android.content.Intent; +import android.os.BaseBundle; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; +import android.view.translation.ITranslationDirectManager; +import android.view.translation.TranslationManager; +import android.view.translation.TranslationResponse; +import android.view.translation.TranslationSpec; + +import com.android.internal.os.IResultReceiver; +import com.android.internal.util.SyncResultReceiver; + +/** + * Service for translating text. + * @hide + */ +@SystemApi +public abstract class TranslationService extends Service { + private static final String TAG = "TranslationService"; + + /** + * The {@link Intent} that must be declared as handled by the service. + * + * <p>To be supported, the service must also require the + * {@link android.Manifest.permission#BIND_TRANSLATION_SERVICE} permission so + * that other applications can not abuse it. + */ + public static final String SERVICE_INTERFACE = + "android.service.translation.TranslationService"; + + /** + * Name under which a TranslationService component publishes information about itself. + * + * <p>This meta-data should reference an XML resource containing a + * <code><{@link + * android.R.styleable#TranslationService translation-service}></code> tag. + * + * <p>Here's an example of how to use it on {@code AndroidManifest.xml}: + * TODO: fill in doc example (check CCService/AFService). + */ + public static final String SERVICE_META_DATA = "android.translation_service"; + + private Handler mHandler; + + /** + * Binder to receive calls from system server. + */ + private final ITranslationService mInterface = new ITranslationService.Stub() { + @Override + public void onConnected() { + mHandler.sendMessage(obtainMessage(TranslationService::onConnected, + TranslationService.this)); + } + + @Override + public void onDisconnected() { + mHandler.sendMessage(obtainMessage(TranslationService::onDisconnected, + TranslationService.this)); + } + + @Override + public void onCreateTranslationSession(TranslationSpec sourceSpec, TranslationSpec destSpec, + int sessionId, IResultReceiver receiver) throws RemoteException { + mHandler.sendMessage(obtainMessage(TranslationService::handleOnCreateTranslationSession, + TranslationService.this, sourceSpec, destSpec, sessionId, receiver)); + } + }; + + /** + * Interface definition for a callback to be invoked when the translation is compleled. + */ + public interface OnTranslationResultCallback { + /** + * Notifies the Android System that a translation request + * {@link TranslationService#onTranslationRequest(TranslationRequest, int, + * CancellationSignal, OnTranslationResultCallback)} was successfully fulfilled by the + * service. + * + * <p>This method should always be called, even if the service cannot fulfill the request + * (in which case it should be called with a TranslationResponse with + * {@link android.view.translation.TranslationResponse#TRANSLATION_STATUS_UNKNOWN_ERROR}, + * or {@link android.view.translation.TranslationResponse + * #TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE}). + * + * @param response translation response for the provided request infos. + * + * @throws IllegalStateException if this method was already called. + */ + void onTranslationSuccess(@NonNull TranslationResponse response); + + /** + * TODO: implement javadoc + */ + void onError(); + } + + /** + * Binder that receives calls from the app. + */ + private final ITranslationDirectManager mClientInterface = + new ITranslationDirectManager.Stub() { + // TODO: Implement cancellation signal + @NonNull + private final CancellationSignal mCancellationSignal = new CancellationSignal(); + + @Override + public void onTranslationRequest(TranslationRequest request, int sessionId, + ITranslationCallback callback, IResultReceiver receiver) + throws RemoteException { + // TODO(b/176464808): Currently, the API is used for both sync and async case. + // It may work now, but maybe two methods is more cleaner. To think how to + // define the APIs for these two cases. + final ITranslationCallback cb = callback != null + ? callback + : new ITranslationCallback.Stub() { + @Override + public void onTranslationComplete( + TranslationResponse translationResponse) + throws RemoteException { + receiver.send(0, + SyncResultReceiver.bundleFor(translationResponse)); + } + + @Override + public void onError() throws RemoteException { + //TODO: implement default error callback + } + }; + // TODO(b/176464808): make it a private member of client + final OnTranslationResultCallback translationResultCallback = + new OnTranslationResultCallbackWrapper(cb); + mHandler.sendMessage(obtainMessage(TranslationService::onTranslationRequest, + TranslationService.this, request, sessionId, mCancellationSignal, + translationResultCallback)); + } + + @Override + public void onFinishTranslationSession(int sessionId) throws RemoteException { + mHandler.sendMessage(obtainMessage( + TranslationService::onFinishTranslationSession, + TranslationService.this, sessionId)); + } + }; + + @CallSuper + @Override + public void onCreate() { + super.onCreate(); + mHandler = new Handler(Looper.getMainLooper(), null, true); + BaseBundle.setShouldDefuse(true); + } + + @Override + @Nullable + public final IBinder onBind(@NonNull Intent intent) { + if (SERVICE_INTERFACE.equals(intent.getAction())) { + return mInterface.asBinder(); + } + Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent); + return null; + } + + /** + * Called when the Android system connects to service. + * + * <p>You should generally do initialization here rather than in {@link #onCreate}. + */ + public void onConnected() { + } + + /** + * Called when the Android system disconnects from the service. + * + * <p> At this point this service may no longer be an active {@link TranslationService}. + * It should not make calls on {@link TranslationManager} that requires the caller to be + * the current service. + */ + public void onDisconnected() { + } + + /** + * TODO: fill in javadoc. + * + * @param sourceSpec + * @param destSpec + * @param sessionId + */ + // TODO(b/176464808): the session id won't be unique cross client/server process. Need to find + // solution to make it's safe. + public abstract void onCreateTranslationSession(@NonNull TranslationSpec sourceSpec, + @NonNull TranslationSpec destSpec, int sessionId); + + /** + * TODO: fill in javadoc. + * + * @param sessionId + */ + public abstract void onFinishTranslationSession(int sessionId); + + /** + * TODO: fill in javadoc. + * + * @param request + * @param sessionId + * @param callback + * @param cancellationSignal + */ + public abstract void onTranslationRequest(@NonNull TranslationRequest request, int sessionId, + @NonNull CancellationSignal cancellationSignal, + @NonNull OnTranslationResultCallback callback); + + // TODO(b/176464808): Need to handle client dying case + + // TODO(b/176464808): Need to handle the failure case. e.g. if the specs does not support + + private void handleOnCreateTranslationSession(@NonNull TranslationSpec sourceSpec, + @NonNull TranslationSpec destSpec, int sessionId, IResultReceiver resultReceiver) { + try { + final Bundle extras = new Bundle(); + extras.putBinder(EXTRA_SERVICE_BINDER, mClientInterface.asBinder()); + extras.putInt(EXTRA_SESSION_ID, sessionId); + resultReceiver.send(0, extras); + } catch (RemoteException e) { + Log.w(TAG, "RemoteException sending client interface: " + e); + } + onCreateTranslationSession(sourceSpec, destSpec, sessionId); + } +} diff --git a/core/java/android/service/translation/TranslationServiceInfo.java b/core/java/android/service/translation/TranslationServiceInfo.java new file mode 100644 index 000000000000..18cc29d12b5f --- /dev/null +++ b/core/java/android/service/translation/TranslationServiceInfo.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2020 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.service.translation; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.AppGlobals; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.os.RemoteException; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Slog; +import android.util.Xml; + +import com.android.internal.R; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.PrintWriter; + +/** + * {@link ServiceInfo} and meta-data about an {@link TranslationService}. + * + * @hide + */ +public final class TranslationServiceInfo { + + private static final String TAG = "TranslationServiceInfo"; + private static final String XML_TAG_SERVICE = "translation-service"; + + @NonNull + private final ServiceInfo mServiceInfo; + + @Nullable + private final String mSettingsActivity; + + private static ServiceInfo getServiceInfoOrThrow(ComponentName comp, boolean isTemp, + @UserIdInt int userId) throws PackageManager.NameNotFoundException { + int flags = PackageManager.GET_META_DATA; + if (!isTemp) { + flags |= PackageManager.MATCH_SYSTEM_ONLY; + } + + ServiceInfo si = null; + try { + si = AppGlobals.getPackageManager().getServiceInfo(comp, flags, userId); + } catch (RemoteException e) { + } + if (si == null) { + throw new NameNotFoundException("Could not get serviceInfo for " + + (isTemp ? " (temp)" : "(default system)") + + " " + comp.flattenToShortString()); + } + return si; + } + + @NonNull + public ServiceInfo getServiceInfo() { + return mServiceInfo; + } + + @Nullable + public String getSettingsActivity() { + return mSettingsActivity; + } + + public TranslationServiceInfo(@NonNull Context context, @NonNull ComponentName comp, + boolean isTemporaryService, @UserIdInt int userId) + throws PackageManager.NameNotFoundException { + this(context, getServiceInfoOrThrow(comp, isTemporaryService, userId)); + } + + private TranslationServiceInfo(@NonNull Context context, @NonNull ServiceInfo si) { + // Check for permission. + if (!Manifest.permission.BIND_TRANSLATION_SERVICE.equals(si.permission)) { + Slog.w(TAG, "TranslationServiceInfo from '" + si.packageName + + "' does not require permission " + + Manifest.permission.BIND_CONTENT_CAPTURE_SERVICE); + throw new SecurityException("Service does not require permission " + + Manifest.permission.BIND_CONTENT_CAPTURE_SERVICE); + } + + mServiceInfo = si; + + // Get the metadata, if declared. + // TODO: Try to find more easier way to do this. + final XmlResourceParser parser = si.loadXmlMetaData(context.getPackageManager(), + TranslationService.SERVICE_META_DATA); + if (parser == null) { + mSettingsActivity = null; + return; + } + + String settingsActivity = null; + + try { + final Resources resources = context.getPackageManager().getResourcesForApplication( + si.applicationInfo); + + int type = 0; + while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { + type = parser.next(); + } + + if (XML_TAG_SERVICE.equals(parser.getName())) { + final AttributeSet allAttributes = Xml.asAttributeSet(parser); + TypedArray afsAttributes = null; + try { + afsAttributes = resources.obtainAttributes(allAttributes, + com.android.internal.R.styleable.TranslationService); + settingsActivity = afsAttributes.getString( + R.styleable.ContentCaptureService_settingsActivity); + } finally { + if (afsAttributes != null) { + afsAttributes.recycle(); + } + } + } else { + Log.e(TAG, "Meta-data does not start with translation-service tag"); + } + } catch (PackageManager.NameNotFoundException | IOException | XmlPullParserException e) { + Log.e(TAG, "Error parsing auto fill service meta-data", e); + } + + mSettingsActivity = settingsActivity; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getClass().getSimpleName()); + builder.append("[").append(mServiceInfo); + builder.append(", settings:").append(mSettingsActivity); + return builder.toString(); + } + + /** + * Dumps the service information. + */ + public void dump(@NonNull String prefix, @NonNull PrintWriter pw) { + pw.print(prefix); + pw.print("Component: "); + pw.println(getServiceInfo().getComponentName()); + pw.print(prefix); + pw.print("Settings: "); + pw.println(mSettingsActivity); + } +} diff --git a/core/java/android/view/translation/ITranslationDirectManager.aidl b/core/java/android/view/translation/ITranslationDirectManager.aidl new file mode 100644 index 000000000000..358f99a5104b --- /dev/null +++ b/core/java/android/view/translation/ITranslationDirectManager.aidl @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2020 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.service.translation.TranslationRequest; +import android.service.translation.ITranslationCallback; +import com.android.internal.os.IResultReceiver; + +/** + * Interface between an app (TranslationManager / Translator) and the remote TranslationService + * providing the TranslationService implementation. + * + * @hide + */ +oneway interface ITranslationDirectManager { + void onTranslationRequest(in TranslationRequest request, int sessionId, + in ITranslationCallback callback, in IResultReceiver receiver); + void onFinishTranslationSession(int sessionId); +} diff --git a/core/java/android/view/translation/ITranslationManager.aidl b/core/java/android/view/translation/ITranslationManager.aidl new file mode 100644 index 000000000000..73addf4d1894 --- /dev/null +++ b/core/java/android/view/translation/ITranslationManager.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2020 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.service.translation.TranslationRequest; +import android.view.translation.TranslationSpec; +import com.android.internal.os.IResultReceiver; + +/** + * Mediator between apps being translated and translation service implementation. + * + * {@hide} + */ +oneway interface ITranslationManager { + void getSupportedLocales(in IResultReceiver receiver, int userId); + void onSessionCreated(in TranslationSpec sourceSpec, in TranslationSpec destSpec, + int sessionId, in IResultReceiver receiver, int userId); +} diff --git a/core/java/android/view/translation/OWNERS b/core/java/android/view/translation/OWNERS new file mode 100644 index 000000000000..a1e663aa8ff7 --- /dev/null +++ b/core/java/android/view/translation/OWNERS @@ -0,0 +1,8 @@ +# Bug component: 994311 + +adamhe@google.com +augale@google.com +joannechung@google.com +lpeter@google.com +svetoslavganov@google.com +tymtsai@google.com diff --git a/core/java/android/view/translation/TranslationData.aidl b/core/java/android/view/translation/TranslationData.aidl new file mode 100644 index 000000000000..40f21a6b3d4e --- /dev/null +++ b/core/java/android/view/translation/TranslationData.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 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 TranslationData; diff --git a/core/java/android/view/translation/TranslationManager.java b/core/java/android/view/translation/TranslationManager.java new file mode 100644 index 000000000000..6554e1a1db54 --- /dev/null +++ b/core/java/android/view/translation/TranslationManager.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2020 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.Nullable; +import android.annotation.RequiresFeature; +import android.annotation.SystemService; +import android.annotation.WorkerThread; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.service.translation.TranslationService; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.SyncResultReceiver; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * The {@link TranslationManager} class provides ways for apps to integrate and use the + * translation framework. + * + * <p>The TranslationManager manages {@link Translator}s and help bridge client calls to + * the server {@link android.service.translation.TranslationService} </p> + */ +@SystemService(Context.TRANSLATION_MANAGER_SERVICE) +@RequiresFeature(PackageManager.FEATURE_TRANSLATION) +public final class TranslationManager { + + private static final String TAG = "TranslationManager"; + + /** + * Timeout for calls to system_server. + */ + static final int SYNC_CALLS_TIMEOUT_MS = 5000; + /** + * The result code from result receiver success. + * @hide + */ + public static final int STATUS_SYNC_CALL_SUCCESS = 1; + /** + * The result code from result receiver fail. + * @hide + */ + public static final int STATUS_SYNC_CALL_FAIL = 2; + + private static final Random ID_GENERATOR = new Random(); + private final Object mLock = new Object(); + + @NonNull + private final Context mContext; + + private final ITranslationManager mService; + + @Nullable + @GuardedBy("mLock") + private ITranslationDirectManager mDirectServiceBinder; + + @NonNull + @GuardedBy("mLock") + private final SparseArray<Translator> mTranslators = new SparseArray<>(); + + @NonNull + @GuardedBy("mLock") + private final ArrayMap<Pair<TranslationSpec, TranslationSpec>, Integer> mTranslatorIds = + new ArrayMap<>(); + + @NonNull + private final Handler mHandler; + + private static final AtomicInteger sAvailableRequestId = new AtomicInteger(1); + + /** + * @hide + */ + public TranslationManager(@NonNull Context context, ITranslationManager service) { + mContext = Objects.requireNonNull(context, "context cannot be null"); + mService = service; + + mHandler = Handler.createAsync(Looper.getMainLooper()); + } + + /** + * Create a Translator for translation. + * + * <p><strong>NOTE: </strong>Call on a worker thread. + * + * @param sourceSpec {@link TranslationSpec} for the data to be translated. + * @param destSpec {@link TranslationSpec} for the translated data. + * @return a {@link Translator} to be used for calling translation APIs. + */ + @Nullable + @WorkerThread + public Translator createTranslator(@NonNull TranslationSpec sourceSpec, + @NonNull TranslationSpec destSpec) { + Objects.requireNonNull(sourceSpec, "sourceSpec cannot be null"); + Objects.requireNonNull(sourceSpec, "destSpec cannot be null"); + + synchronized (mLock) { + // TODO(b/176464808): Disallow multiple Translator now, it will throw + // IllegalStateException. Need to discuss if we can allow multiple Translators. + final Pair<TranslationSpec, TranslationSpec> specs = new Pair<>(sourceSpec, destSpec); + if (mTranslatorIds.containsKey(specs)) { + return mTranslators.get(mTranslatorIds.get(specs)); + } + + int translatorId; + do { + translatorId = Math.abs(ID_GENERATOR.nextInt()); + } while (translatorId == 0 || mTranslators.indexOfKey(translatorId) >= 0); + + final Translator newTranslator = new Translator(mContext, sourceSpec, destSpec, + translatorId, this, mHandler, mService); + // Start the Translator session and wait for the result + newTranslator.start(); + try { + if (!newTranslator.isSessionCreated()) { + return null; + } + mTranslators.put(translatorId, newTranslator); + mTranslatorIds.put(specs, translatorId); + return newTranslator; + } catch (Translator.ServiceBinderReceiver.TimeoutException e) { + // TODO(b/176464808): maybe make SyncResultReceiver.TimeoutException constructor + // public and use it. + Log.e(TAG, "Timed out getting create session: " + e); + return null; + } + } + } + + /** + * Returns a list of locales supported by the {@link TranslationService}. + * + * <p><strong>NOTE: </strong>Call on a worker thread. + * + * TODO: Change to correct language/locale format + */ + @NonNull + @WorkerThread + public List<String> getSupportedLocales() { + try { + // TODO: implement it + final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); + mService.getSupportedLocales(receiver, mContext.getUserId()); + int resutCode = receiver.getIntResult(); + if (resutCode != STATUS_SYNC_CALL_SUCCESS) { + return Collections.emptyList(); + } + return receiver.getParcelableResult(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (SyncResultReceiver.TimeoutException e) { + Log.e(TAG, "Timed out getting supported locales: " + e); + return Collections.emptyList(); + } + } + + void removeTranslator(int id) { + synchronized (mLock) { + mTranslators.remove(id); + for (int i = 0; i < mTranslatorIds.size(); i++) { + if (mTranslatorIds.valueAt(i) == id) { + mTranslatorIds.removeAt(i); + break; + } + } + } + } + + AtomicInteger getAvailableRequestId() { + synchronized (mLock) { + return sAvailableRequestId; + } + } +} diff --git a/core/java/android/view/translation/TranslationRequest.aidl b/core/java/android/view/translation/TranslationRequest.aidl new file mode 100644 index 000000000000..c34bf3011462 --- /dev/null +++ b/core/java/android/view/translation/TranslationRequest.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 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 TranslationRequest; diff --git a/core/java/android/view/translation/TranslationRequest.java b/core/java/android/view/translation/TranslationRequest.java new file mode 100644 index 000000000000..a5e3f758ba9f --- /dev/null +++ b/core/java/android/view/translation/TranslationRequest.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2020 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.Nullable; +import android.os.Parcelable; +import android.view.autofill.AutofillId; + +import com.android.internal.util.DataClass; + +/** + * Wrapper class for data to be translated by {@link android.service.translation.TranslationService} + */ +@DataClass(genToString = true, genBuilder = true) +public final class TranslationRequest implements Parcelable { + + @Nullable + private final AutofillId mAutofillId; + + @Nullable + private final CharSequence mTranslationText; + + public TranslationRequest(@Nullable CharSequence text) { + mAutofillId = null; + mTranslationText = text; + } + + private static CharSequence defaultTranslationText() { + return null; + } + + private static AutofillId defaultAutofillId() { + return null; + } + + + + // Code below generated by codegen v1.0.22. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/TranslationRequest.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 */ TranslationRequest( + @Nullable AutofillId autofillId, + @Nullable CharSequence translationText) { + this.mAutofillId = autofillId; + this.mTranslationText = translationText; + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public @Nullable AutofillId getAutofillId() { + return mAutofillId; + } + + @DataClass.Generated.Member + public @Nullable CharSequence getTranslationText() { + return mTranslationText; + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "TranslationRequest { " + + "autofillId = " + mAutofillId + ", " + + "translationText = " + mTranslationText + + " }"; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + byte flg = 0; + if (mAutofillId != null) flg |= 0x1; + if (mTranslationText != null) flg |= 0x2; + dest.writeByte(flg); + if (mAutofillId != null) dest.writeTypedObject(mAutofillId, flags); + if (mTranslationText != null) dest.writeCharSequence(mTranslationText); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ TranslationRequest(@NonNull android.os.Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + byte flg = in.readByte(); + AutofillId autofillId = (flg & 0x1) == 0 ? null : (AutofillId) in.readTypedObject(AutofillId.CREATOR); + CharSequence translationText = (flg & 0x2) == 0 ? null : (CharSequence) in.readCharSequence(); + + this.mAutofillId = autofillId; + this.mTranslationText = translationText; + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<TranslationRequest> CREATOR + = new Parcelable.Creator<TranslationRequest>() { + @Override + public TranslationRequest[] newArray(int size) { + return new TranslationRequest[size]; + } + + @Override + public TranslationRequest createFromParcel(@NonNull android.os.Parcel in) { + return new TranslationRequest(in); + } + }; + + /** + * A builder for {@link TranslationRequest} + */ + @SuppressWarnings("WeakerAccess") + @DataClass.Generated.Member + public static final class Builder { + + private @Nullable AutofillId mAutofillId; + private @Nullable CharSequence mTranslationText; + + private long mBuilderFieldsSet = 0L; + + public Builder() { + } + + @DataClass.Generated.Member + public @NonNull Builder setAutofillId(@NonNull AutofillId value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x1; + mAutofillId = value; + return this; + } + + @DataClass.Generated.Member + public @NonNull Builder setTranslationText(@NonNull CharSequence value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x2; + mTranslationText = value; + return this; + } + + /** Builds the instance. This builder should not be touched after calling this! */ + public @NonNull TranslationRequest build() { + checkNotUsed(); + mBuilderFieldsSet |= 0x4; // Mark builder used + + if ((mBuilderFieldsSet & 0x1) == 0) { + mAutofillId = defaultAutofillId(); + } + if ((mBuilderFieldsSet & 0x2) == 0) { + mTranslationText = defaultTranslationText(); + } + TranslationRequest o = new TranslationRequest( + mAutofillId, + mTranslationText); + return o; + } + + private void checkNotUsed() { + if ((mBuilderFieldsSet & 0x4) != 0) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + } + } + + @DataClass.Generated( + time = 1610060189421L, + codegenVersion = "1.0.22", + sourceFile = "frameworks/base/core/java/android/view/translation/TranslationRequest.java", + inputSignatures = "private final @android.annotation.Nullable android.view.autofill.AutofillId mAutofillId\nprivate final @android.annotation.Nullable java.lang.CharSequence mTranslationText\nprivate static java.lang.CharSequence defaultTranslationText()\nprivate static android.view.autofill.AutofillId defaultAutofillId()\nclass TranslationRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genBuilder=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/view/translation/TranslationResponse.aidl b/core/java/android/view/translation/TranslationResponse.aidl new file mode 100644 index 000000000000..e5350bb54dc2 --- /dev/null +++ b/core/java/android/view/translation/TranslationResponse.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 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 TranslationResponse; diff --git a/core/java/android/view/translation/TranslationResponse.java b/core/java/android/view/translation/TranslationResponse.java new file mode 100644 index 000000000000..d29063fbd914 --- /dev/null +++ b/core/java/android/view/translation/TranslationResponse.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2020 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.IntDef; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.service.translation.TranslationService; + +import com.android.internal.util.DataClass; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; + +/** + * Response from the {@link TranslationService}, which contains the translated result. + */ +@DataClass(genBuilder = true, genToString = true, genHiddenConstDefs = true) +public final class TranslationResponse implements Parcelable { + + /** + * The {@link TranslationService} was successful in translating. + */ + public static final int TRANSLATION_STATUS_SUCCESS = 0; + /** + * The {@link TranslationService} returned unknown translation result. + */ + public static final int TRANSLATION_STATUS_UNKNOWN_ERROR = 1; + /** + * The language of the request is not available to be translated. + */ + public static final int TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE = 2; + + /** + * The translation result status code. + */ + private final @TranslationStatus int mTranslationStatus; + /** + * The translation results. If there is no translation result, set it with an empty list. + */ + @NonNull + private List<TranslationRequest> mTranslations = new ArrayList(); + + + + + // Code below generated by codegen v1.0.22. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/TranslationResponse.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + /** @hide */ + @IntDef(prefix = "TRANSLATION_STATUS_", value = { + TRANSLATION_STATUS_SUCCESS, + TRANSLATION_STATUS_UNKNOWN_ERROR, + TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE + }) + @Retention(RetentionPolicy.SOURCE) + @DataClass.Generated.Member + public @interface TranslationStatus {} + + /** @hide */ + @DataClass.Generated.Member + public static String translationStatusToString(@TranslationStatus int value) { + switch (value) { + case TRANSLATION_STATUS_SUCCESS: + return "TRANSLATION_STATUS_SUCCESS"; + case TRANSLATION_STATUS_UNKNOWN_ERROR: + return "TRANSLATION_STATUS_UNKNOWN_ERROR"; + case TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE: + return "TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE"; + default: return Integer.toHexString(value); + } + } + + @DataClass.Generated.Member + /* package-private */ TranslationResponse( + @TranslationStatus int translationStatus, + @NonNull List<TranslationRequest> translations) { + this.mTranslationStatus = translationStatus; + + if (!(mTranslationStatus == TRANSLATION_STATUS_SUCCESS) + && !(mTranslationStatus == TRANSLATION_STATUS_UNKNOWN_ERROR) + && !(mTranslationStatus == TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE)) { + throw new java.lang.IllegalArgumentException( + "translationStatus was " + mTranslationStatus + " but must be one of: " + + "TRANSLATION_STATUS_SUCCESS(" + TRANSLATION_STATUS_SUCCESS + "), " + + "TRANSLATION_STATUS_UNKNOWN_ERROR(" + TRANSLATION_STATUS_UNKNOWN_ERROR + "), " + + "TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE(" + TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE + ")"); + } + + this.mTranslations = translations; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mTranslations); + + // onConstructed(); // You can define this method to get a callback + } + + /** + * The translation result status code. + */ + @DataClass.Generated.Member + public @TranslationStatus int getTranslationStatus() { + return mTranslationStatus; + } + + /** + * The translation results. If there is no translation result, set it with an empty list. + */ + @DataClass.Generated.Member + public @NonNull List<TranslationRequest> getTranslations() { + return mTranslations; + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "TranslationResponse { " + + "translationStatus = " + translationStatusToString(mTranslationStatus) + ", " + + "translations = " + mTranslations + + " }"; + } + + @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) { ... } + + dest.writeInt(mTranslationStatus); + dest.writeParcelableList(mTranslations, flags); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ TranslationResponse(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + int translationStatus = in.readInt(); + List<TranslationRequest> translations = new ArrayList<>(); + in.readParcelableList(translations, TranslationRequest.class.getClassLoader()); + + this.mTranslationStatus = translationStatus; + + if (!(mTranslationStatus == TRANSLATION_STATUS_SUCCESS) + && !(mTranslationStatus == TRANSLATION_STATUS_UNKNOWN_ERROR) + && !(mTranslationStatus == TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE)) { + throw new java.lang.IllegalArgumentException( + "translationStatus was " + mTranslationStatus + " but must be one of: " + + "TRANSLATION_STATUS_SUCCESS(" + TRANSLATION_STATUS_SUCCESS + "), " + + "TRANSLATION_STATUS_UNKNOWN_ERROR(" + TRANSLATION_STATUS_UNKNOWN_ERROR + "), " + + "TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE(" + TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE + ")"); + } + + this.mTranslations = translations; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mTranslations); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<TranslationResponse> CREATOR + = new Parcelable.Creator<TranslationResponse>() { + @Override + public TranslationResponse[] newArray(int size) { + return new TranslationResponse[size]; + } + + @Override + public TranslationResponse createFromParcel(@NonNull Parcel in) { + return new TranslationResponse(in); + } + }; + + /** + * A builder for {@link TranslationResponse} + */ + @SuppressWarnings("WeakerAccess") + @DataClass.Generated.Member + public static final class Builder { + + private @TranslationStatus int mTranslationStatus; + private @NonNull List<TranslationRequest> mTranslations; + + private long mBuilderFieldsSet = 0L; + + /** + * Creates a new Builder. + * + * @param translationStatus + * The translation result status code. + */ + public Builder( + @TranslationStatus int translationStatus) { + mTranslationStatus = translationStatus; + + if (!(mTranslationStatus == TRANSLATION_STATUS_SUCCESS) + && !(mTranslationStatus == TRANSLATION_STATUS_UNKNOWN_ERROR) + && !(mTranslationStatus == TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE)) { + throw new java.lang.IllegalArgumentException( + "translationStatus was " + mTranslationStatus + " but must be one of: " + + "TRANSLATION_STATUS_SUCCESS(" + TRANSLATION_STATUS_SUCCESS + "), " + + "TRANSLATION_STATUS_UNKNOWN_ERROR(" + TRANSLATION_STATUS_UNKNOWN_ERROR + "), " + + "TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE(" + TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE + ")"); + } + + } + + /** + * The translation result status code. + */ + @DataClass.Generated.Member + public @NonNull Builder setTranslationStatus(@TranslationStatus int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x1; + mTranslationStatus = value; + return this; + } + + /** + * The translation results. If there is no translation result, set it with an empty list. + */ + @DataClass.Generated.Member + public @NonNull Builder setTranslations(@NonNull List<TranslationRequest> value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x2; + mTranslations = value; + return this; + } + + /** @see #setTranslations */ + @DataClass.Generated.Member + public @NonNull Builder addTranslations(@NonNull TranslationRequest value) { + // You can refine this method's name by providing item's singular name, e.g.: + // @DataClass.PluralOf("item")) mItems = ... + + if (mTranslations == null) setTranslations(new ArrayList<>()); + mTranslations.add(value); + return this; + } + + /** Builds the instance. This builder should not be touched after calling this! */ + public @NonNull TranslationResponse build() { + checkNotUsed(); + mBuilderFieldsSet |= 0x4; // Mark builder used + + if ((mBuilderFieldsSet & 0x2) == 0) { + mTranslations = new ArrayList(); + } + TranslationResponse o = new TranslationResponse( + mTranslationStatus, + mTranslations); + return o; + } + + private void checkNotUsed() { + if ((mBuilderFieldsSet & 0x4) != 0) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + } + } + + @DataClass.Generated( + time = 1609973911361L, + codegenVersion = "1.0.22", + sourceFile = "frameworks/base/core/java/android/view/translation/TranslationResponse.java", + inputSignatures = "public static final int TRANSLATION_STATUS_SUCCESS\npublic static final int TRANSLATION_STATUS_UNKNOWN_ERROR\npublic static final int TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE\nprivate final @android.view.translation.TranslationResponse.TranslationStatus int mTranslationStatus\nprivate @android.annotation.NonNull java.util.List<android.view.translation.TranslationRequest> mTranslations\nclass TranslationResponse extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genToString=true, genHiddenConstDefs=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/view/translation/TranslationSpec.aidl b/core/java/android/view/translation/TranslationSpec.aidl new file mode 100644 index 000000000000..875d798370d4 --- /dev/null +++ b/core/java/android/view/translation/TranslationSpec.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 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 TranslationSpec; diff --git a/core/java/android/view/translation/TranslationSpec.java b/core/java/android/view/translation/TranslationSpec.java new file mode 100644 index 000000000000..ab1bc477e0fd --- /dev/null +++ b/core/java/android/view/translation/TranslationSpec.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2020 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.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.DataClass; + +/** + * Specs and additional info for the translation data. + * + * <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) +public final class TranslationSpec implements Parcelable { + + /** Data format for translation is text. */ + public static final int DATA_FORMAT_TEXT = 1; + + /** @hide */ + @android.annotation.IntDef(prefix = "DATA_FORMAT_", value = { + DATA_FORMAT_TEXT + }) + @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) + @DataClass.Generated.Member + public @interface DataFormat {} + + /** + * String representation of language codes e.g. "en", "es", etc. + */ + private final @NonNull String mLanguage; + + private final @DataFormat int mDataFormat; + + + + // Code below generated by codegen v1.0.22. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/TranslationSpec.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + /** + * Creates a new TranslationSpec. + * + * @param language + * String representation of language codes e.g. "en", "es", etc. + */ + @DataClass.Generated.Member + public TranslationSpec( + @NonNull String language, + @DataFormat int dataFormat) { + this.mLanguage = language; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mLanguage); + this.mDataFormat = dataFormat; + com.android.internal.util.AnnotationValidations.validate( + DataFormat.class, null, mDataFormat); + + // onConstructed(); // You can define this method to get a callback + } + + /** + * String representation of language codes e.g. "en", "es", etc. + */ + @DataClass.Generated.Member + public @NonNull String getLanguage() { + return mLanguage; + } + + @DataClass.Generated.Member + public @DataFormat int getDataFormat() { + return mDataFormat; + } + + @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(TranslationSpec other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + TranslationSpec that = (TranslationSpec) o; + //noinspection PointlessBooleanExpression + return true + && java.util.Objects.equals(mLanguage, that.mLanguage) + && mDataFormat == that.mDataFormat; + } + + @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 + java.util.Objects.hashCode(mLanguage); + _hash = 31 * _hash + mDataFormat; + 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) { ... } + + dest.writeString(mLanguage); + dest.writeInt(mDataFormat); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ TranslationSpec(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + String language = in.readString(); + int dataFormat = in.readInt(); + + this.mLanguage = language; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mLanguage); + this.mDataFormat = dataFormat; + com.android.internal.util.AnnotationValidations.validate( + DataFormat.class, null, mDataFormat); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<TranslationSpec> CREATOR + = new Parcelable.Creator<TranslationSpec>() { + @Override + public TranslationSpec[] newArray(int size) { + return new TranslationSpec[size]; + } + + @Override + public TranslationSpec createFromParcel(@NonNull Parcel in) { + return new TranslationSpec(in); + } + }; + + @DataClass.Generated( + time = 1609964630624L, + 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)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/view/translation/Translator.java b/core/java/android/view/translation/Translator.java new file mode 100644 index 000000000000..675f32b19d17 --- /dev/null +++ b/core/java/android/view/translation/Translator.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2020 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 static android.view.translation.TranslationManager.STATUS_SYNC_CALL_FAIL; +import static android.view.translation.TranslationManager.SYNC_CALLS_TIMEOUT_MS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.WorkerThread; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.os.IResultReceiver; +import com.android.internal.util.SyncResultReceiver; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * The {@link Translator} for translation, defined by a source and a dest {@link TranslationSpec}. + */ +@SuppressLint("NotCloseable") +public class Translator { + + private static final String TAG = "Translator"; + + // TODO: make this configurable and cross the Translation component + private static boolean sDEBUG = false; + + private final Object mLock = new Object(); + + private int mId; + + @NonNull + private final Context mContext; + + @NonNull + private final TranslationSpec mSourceSpec; + + @NonNull + private final TranslationSpec mDestSpec; + + @NonNull + private final TranslationManager mManager; + + @NonNull + private final Handler mHandler; + + /** + * Interface to the system_server binder object. + */ + private ITranslationManager mSystemServerBinder; + + /** + * Direct interface to the TranslationService binder object. + */ + @Nullable + private ITranslationDirectManager mDirectServiceBinder; + + @NonNull + private final ServiceBinderReceiver mServiceBinderReceiver; + + @GuardedBy("mLock") + private boolean mDestroyed; + + /** + * Name of the {@link IResultReceiver} extra used to pass the binder interface to Translator. + * @hide + */ + public static final String EXTRA_SERVICE_BINDER = "binder"; + /** + * Name of the extra used to pass the session id to Translator. + * @hide + */ + public static final String EXTRA_SESSION_ID = "sessionId"; + + static class ServiceBinderReceiver extends IResultReceiver.Stub { + private final WeakReference<Translator> mTranslator; + private final CountDownLatch mLatch = new CountDownLatch(1); + private int mSessionId; + + ServiceBinderReceiver(Translator translator) { + mTranslator = new WeakReference<>(translator); + } + + int getSessionStateResult() throws TimeoutException { + try { + if (!mLatch.await(SYNC_CALLS_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + throw new TimeoutException( + "Session not created in " + SYNC_CALLS_TIMEOUT_MS + "ms"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new TimeoutException("Session not created because interrupted"); + } + return mSessionId; + } + + @Override + public void send(int resultCode, Bundle resultData) { + if (resultCode == STATUS_SYNC_CALL_FAIL) { + mLatch.countDown(); + return; + } + mSessionId = resultData.getInt(EXTRA_SESSION_ID); + final Translator translator = mTranslator.get(); + if (translator == null) { + Log.w(TAG, "received result after session is finished"); + return; + } + final IBinder binder; + if (resultData != null) { + binder = resultData.getBinder(EXTRA_SERVICE_BINDER); + if (binder == null) { + Log.wtf(TAG, "No " + EXTRA_SERVICE_BINDER + " extra result"); + return; + } + } else { + binder = null; + } + translator.setServiceBinder(binder); + mLatch.countDown(); + } + + // TODO(b/176464808): maybe make SyncResultReceiver.TimeoutException constructor public + // and use it. + static final class TimeoutException extends Exception { + private TimeoutException(String msg) { + super(msg); + } + } + } + + /** + * Create the Translator. + * + * @hide + */ + public Translator(@NonNull Context context, + @NonNull TranslationSpec sourceSpec, + @NonNull TranslationSpec destSpec, int sessionId, + @NonNull TranslationManager translationManager, @NonNull Handler handler, + @Nullable ITranslationManager systemServerBinder) { + mContext = context; + mSourceSpec = sourceSpec; + mDestSpec = destSpec; + mId = sessionId; + mManager = translationManager; + mHandler = handler; + mSystemServerBinder = systemServerBinder; + mServiceBinderReceiver = new ServiceBinderReceiver(this); + } + + /** + * Starts this Translator session. + */ + void start() { + try { + mSystemServerBinder.onSessionCreated(mSourceSpec, mDestSpec, mId, + mServiceBinderReceiver, mContext.getUserId()); + } catch (RemoteException e) { + Log.w(TAG, "RemoteException calling startSession(): " + e); + } + } + + /** + * Wait this Translator session created. + * + * @return {@code true} if the session is created successfully. + */ + boolean isSessionCreated() throws ServiceBinderReceiver.TimeoutException { + int receivedId = mServiceBinderReceiver.getSessionStateResult(); + return receivedId > 0; + } + + private int getNextRequestId() { + // Get from manager to keep the request id unique to different Translators + return mManager.getAvailableRequestId().getAndIncrement(); + } + + private void setServiceBinder(@Nullable IBinder binder) { + synchronized (mLock) { + if (mDirectServiceBinder != null) { + return; + } + if (binder != null) { + mDirectServiceBinder = ITranslationDirectManager.Stub.asInterface(binder); + } + } + } + + /** @hide */ + public int getTranslatorId() { + return mId; + } + + /** + * Requests a translation for the provided {@link TranslationRequest} using the Translator's + * source spec and destination spec. + * + * <p><strong>NOTE: </strong>Call on a worker thread. + * + * @param request {@link TranslationRequest} request to be translated. + * + * @return {@link TranslationRequest} containing translated request, + * or null if translation could not be done. + * @throws IllegalStateException if this TextClassification session was destroyed when calls + */ + @Nullable + @WorkerThread + public TranslationResponse translate(@NonNull TranslationRequest request) { + Objects.requireNonNull(request, "Translation request cannot be null"); + if (isDestroyed()) { + // TODO(b/176464808): Disallow multiple Translator now, it will throw + // IllegalStateException. Need to discuss if we can allow multiple Translators. + throw new IllegalStateException( + "This translator has been destroyed"); + } + final ArrayList<TranslationRequest> requests = new ArrayList<>(); + requests.add(request); + final android.service.translation.TranslationRequest internalRequest = + new android.service.translation.TranslationRequest + .Builder(getNextRequestId(), mSourceSpec, mDestSpec, requests) + .build(); + + TranslationResponse response = null; + try { + final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); + mDirectServiceBinder.onTranslationRequest(internalRequest, mId, null, receiver); + + response = receiver.getParcelableResult(); + } catch (RemoteException e) { + Log.w(TAG, "RemoteException calling requestTranslate(): " + e); + } catch (SyncResultReceiver.TimeoutException e) { + Log.e(TAG, "Timed out calling requestTranslate: " + e); + } + if (sDEBUG) { + Log.v(TAG, "Receive translation response: " + response); + } + return response; + } + + /** + * Destroy this Translator. + */ + public void destroy() { + synchronized (mLock) { + if (mDestroyed) { + return; + } + mDestroyed = true; + try { + mDirectServiceBinder.onFinishTranslationSession(mId); + } catch (RemoteException e) { + Log.w(TAG, "RemoteException calling onSessionFinished"); + } + mDirectServiceBinder = null; + mManager.removeTranslator(mId); + } + } + + /** + * Returns whether or not this Translator has been destroyed. + * + * @see #destroy() + */ + public boolean isDestroyed() { + synchronized (mLock) { + return mDestroyed; + } + } + + // TODO: add methods for UI-toolkit case. +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 8682fea1f8dc..b5c458634454 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3591,6 +3591,14 @@ <permission android:name="android.permission.BIND_CONTENT_CAPTURE_SERVICE" android:protectionLevel="signature" /> + <!-- Must be required by a android.service.translation.TranslationService, + to ensure that only the system can bind to it. + @SystemApi @hide This is not a third-party API (intended for OEMs and system apps). + <p>Protection level: signature + --> + <permission android:name="android.permission.BIND_TRANSLATION_SERVICE" + android:protectionLevel="signature" /> + <!-- Must be required by a android.service.contentsuggestions.ContentSuggestionsService, to ensure that only the system can bind to it. @SystemApi @hide This is not a third-party API (intended for OEMs and system apps). diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 7ca3fafdca47..ef54db1a422c 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -8292,6 +8292,23 @@ </declare-styleable> <!-- =============================== --> + <!-- Translation attributes --> + <!-- =============================== --> + <eat-comment /> + + <!-- Use <code>translation-service</code> as the root tag of the XML resource that describes + a {@link android.service.translation.TranslationService}, which is referenced from + its {@link android.service.translation.TranslationService#SERVICE_META_DATA} meta-data + entry. + @hide @SystemApi + --> + <declare-styleable name="TranslationService"> + <!-- Fully qualified class name of an activity that allows the user to modify + the settings for this service. --> + <attr name="settingsActivity" /> + </declare-styleable> + + <!-- =============================== --> <!-- Contacts meta-data attributes --> <!-- =============================== --> <eat-comment /> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index da658cc3d525..cff1bdaa4477 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3724,6 +3724,14 @@ --> <string name="config_defaultAugmentedAutofillService" translatable="false"></string> + <!-- The package name for the system's translation service. + This service must be trusted, as it can be activated without explicit consent of the user. + If no service with the specified name exists on the device, translation wil be + disabled. + Example: "com.android.translation/.TranslationService" +--> + <string name="config_defaultTranslationService" translatable="false"></string> + <!-- The package name for the system's app prediction service. This service must be trusted, as it can be activated without explicit consent of the user. Example: "com.android.intelligence/.AppPredictionService" diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index fa664519152b..9dadd5fab9ae 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3479,6 +3479,7 @@ <java-symbol type="string" name="config_defaultWellbeingPackage" /> <java-symbol type="string" name="config_defaultContentCaptureService" /> <java-symbol type="string" name="config_defaultAugmentedAutofillService" /> + <java-symbol type="string" name="config_defaultTranslationService" /> <java-symbol type="string" name="config_defaultAppPredictionService" /> <java-symbol type="string" name="config_defaultContentSuggestionsService" /> <java-symbol type="string" name="config_defaultSearchUiService" /> diff --git a/services/Android.bp b/services/Android.bp index 3447b5cbea52..947bd00ecd6f 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -30,6 +30,7 @@ filegroup { ":services.searchui-sources", ":services.startop.iorap-sources", ":services.systemcaptions-sources", + ":services.translation-sources", ":services.usage-sources", ":services.usb-sources", ":services.voiceinteraction-sources", @@ -76,6 +77,7 @@ java_library { "services.searchui", "services.startop", "services.systemcaptions", + "services.translation", "services.usage", "services.usb", "services.voiceinteraction", diff --git a/services/translation/Android.bp b/services/translation/Android.bp new file mode 100644 index 000000000000..804a6177e94e --- /dev/null +++ b/services/translation/Android.bp @@ -0,0 +1,13 @@ +filegroup { + name: "services.translation-sources", + srcs: ["java/**/*.java"], + path: "java", + visibility: ["//frameworks/base/services"], +} + +java_library_static { + name: "services.translation", + defaults: ["platform_service_defaults"], + srcs: [":services.translation-sources"], + libs: ["services.core"], +}
\ No newline at end of file diff --git a/services/translation/OWNERS b/services/translation/OWNERS new file mode 100644 index 000000000000..a1e663aa8ff7 --- /dev/null +++ b/services/translation/OWNERS @@ -0,0 +1,8 @@ +# Bug component: 994311 + +adamhe@google.com +augale@google.com +joannechung@google.com +lpeter@google.com +svetoslavganov@google.com +tymtsai@google.com diff --git a/services/translation/java/com/android/server/translation/RemoteTranslationService.java b/services/translation/java/com/android/server/translation/RemoteTranslationService.java new file mode 100644 index 000000000000..0c7e61765501 --- /dev/null +++ b/services/translation/java/com/android/server/translation/RemoteTranslationService.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020 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.annotation.NonNull; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.service.translation.ITranslationService; +import android.service.translation.TranslationService; +import android.util.Slog; +import android.view.translation.TranslationSpec; + +import com.android.internal.infra.AbstractRemoteService; +import com.android.internal.infra.ServiceConnector; +import com.android.internal.os.IResultReceiver; + +final class RemoteTranslationService extends ServiceConnector.Impl<ITranslationService> { + + private static final String TAG = RemoteTranslationService.class.getSimpleName(); + + // TODO(b/176590870): Make PERMANENT now. + private static final long TIMEOUT_IDLE_UNBIND_MS = + AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS; + private static final int TIMEOUT_REQUEST_MS = 5_000; + + private final long mIdleUnbindTimeoutMs; + private final int mRequestTimeoutMs; + private final ComponentName mComponentName; + + RemoteTranslationService(Context context, ComponentName serviceName, + int userId, boolean bindInstantServiceAllowed) { + super(context, + new Intent(TranslationService.SERVICE_INTERFACE).setComponent(serviceName), + bindInstantServiceAllowed ? Context.BIND_ALLOW_INSTANT : 0, + userId, ITranslationService.Stub::asInterface); + mIdleUnbindTimeoutMs = TIMEOUT_IDLE_UNBIND_MS; + mRequestTimeoutMs = TIMEOUT_REQUEST_MS; + mComponentName = serviceName; + + // Bind right away. + connect(); + } + + public ComponentName getComponentName() { + return mComponentName; + } + + @Override // from ServiceConnector.Impl + protected void onServiceConnectionStatusChanged(ITranslationService service, + boolean connected) { + try { + if (connected) { + service.onConnected(); + } else { + service.onDisconnected(); + } + } catch (Exception e) { + Slog.w(TAG, + "Exception calling onServiceConnectionStatusChanged(" + connected + "): ", e); + } + } + + @Override // from AbstractRemoteService + protected long getAutoDisconnectTimeoutMs() { + return mIdleUnbindTimeoutMs; + } + + public void onSessionCreated(@NonNull TranslationSpec sourceSpec, + @NonNull TranslationSpec destSpec, int sessionId, IResultReceiver resultReceiver) { + run((s) -> s.onCreateTranslationSession(sourceSpec, destSpec, sessionId, resultReceiver)); + } +} diff --git a/services/translation/java/com/android/server/translation/TranslationManagerService.java b/services/translation/java/com/android/server/translation/TranslationManagerService.java new file mode 100644 index 000000000000..e2aabe6a89ea --- /dev/null +++ b/services/translation/java/com/android/server/translation/TranslationManagerService.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2020 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 static android.content.Context.TRANSLATION_MANAGER_SERVICE; +import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_FAIL; + +import android.content.Context; +import android.os.RemoteException; +import android.util.Slog; +import android.view.translation.ITranslationManager; +import android.view.translation.TranslationSpec; + +import com.android.internal.os.IResultReceiver; +import com.android.server.infra.AbstractMasterSystemService; +import com.android.server.infra.FrameworkResourcesServiceNameResolver; + +/** + * Entry point service for translation management. + * + * <p>This service provides the {@link ITranslationManager} implementation and keeps a list of + * {@link TranslationManagerServiceImpl} per user; the real work is done by + * {@link TranslationManagerServiceImpl} itself. + */ +public final class TranslationManagerService + extends AbstractMasterSystemService<TranslationManagerService, + TranslationManagerServiceImpl> { + + private static final String TAG = "TranslationManagerService"; + + public TranslationManagerService(Context context) { + // TODO: Discuss the disallow policy + super(context, new FrameworkResourcesServiceNameResolver(context, + com.android.internal.R.string.config_defaultTranslationService), + /* disallowProperty */ null, PACKAGE_UPDATE_POLICY_REFRESH_EAGER); + } + + @Override + protected TranslationManagerServiceImpl newServiceLocked(int resolvedUserId, boolean disabled) { + return new TranslationManagerServiceImpl(this, mLock, resolvedUserId, disabled); + } + + 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) { + service.getSupportedLocalesLocked(receiver); + } else { + Slog.v(TAG, "getSupportedLocales(): no service for " + userId); + receiver.send(STATUS_SYNC_CALL_FAIL, null); + } + } + } + + @Override + public void onSessionCreated(TranslationSpec sourceSpec, TranslationSpec destSpec, + int sessionId, IResultReceiver receiver, int userId) throws RemoteException { + synchronized (mLock) { + final TranslationManagerServiceImpl service = getServiceForUserLocked(userId); + if (service != null) { + service.onSessionCreatedLocked(sourceSpec, destSpec, sessionId, receiver); + } else { + Slog.v(TAG, "onSessionCreated(): no service for " + userId); + receiver.send(STATUS_SYNC_CALL_FAIL, null); + } + } + } + } + + @Override // from SystemService + public void onStart() { + publishBinderService(TRANSLATION_MANAGER_SERVICE, + new TranslationManagerService.TranslationManagerServiceStub()); + } +} diff --git a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java new file mode 100644 index 000000000000..b1f6f80d4158 --- /dev/null +++ b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2020 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 static android.view.translation.TranslationManager.STATUS_SYNC_CALL_SUCCESS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.os.RemoteException; +import android.service.translation.TranslationServiceInfo; +import android.util.Slog; +import android.view.translation.TranslationSpec; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.os.IResultReceiver; +import com.android.internal.util.SyncResultReceiver; +import com.android.server.infra.AbstractPerUserSystemService; + +import java.util.ArrayList; + +final class TranslationManagerServiceImpl extends + AbstractPerUserSystemService<TranslationManagerServiceImpl, TranslationManagerService> { + + private static final String TAG = "TranslationManagerServiceImpl"; + + @GuardedBy("mLock") + @Nullable + private RemoteTranslationService mRemoteTranslationService; + + @GuardedBy("mLock") + @Nullable + private ServiceInfo mRemoteTranslationServiceInfo; + + protected TranslationManagerServiceImpl( + @NonNull TranslationManagerService master, + @NonNull Object lock, int userId, boolean disabled) { + super(master, lock, userId); + updateRemoteServiceLocked(); + } + + @GuardedBy("mLock") + @Override // from PerUserSystemService + protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) + throws PackageManager.NameNotFoundException { + final TranslationServiceInfo info = new TranslationServiceInfo(getContext(), + serviceComponent, isTemporaryServiceSetLocked(), mUserId); + mRemoteTranslationServiceInfo = info.getServiceInfo(); + return info.getServiceInfo(); + } + + @GuardedBy("mLock") + @Override // from PerUserSystemService + protected boolean updateLocked(boolean disabled) { + final boolean enabledChanged = super.updateLocked(disabled); + updateRemoteServiceLocked(); + return enabledChanged; + } + + /** + * Updates the reference to the remote service. + */ + @GuardedBy("mLock") + private void updateRemoteServiceLocked() { + if (mRemoteTranslationService != null) { + if (mMaster.debug) Slog.d(TAG, "updateRemoteService(): destroying old remote service"); + mRemoteTranslationService.unbind(); + mRemoteTranslationService = null; + } + } + + @GuardedBy("mLock") + @Nullable + private RemoteTranslationService ensureRemoteServiceLocked() { + if (mRemoteTranslationService == null) { + final String serviceName = getComponentNameLocked(); + if (serviceName == null) { + if (mMaster.verbose) { + Slog.v(TAG, "ensureRemoteServiceLocked(): no service component name."); + } + return null; + } + final ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName); + mRemoteTranslationService = new RemoteTranslationService(getContext(), + serviceComponent, mUserId, /* isInstantAllowed= */ false); + } + return mRemoteTranslationService; + } + + @GuardedBy("mLock") + void getSupportedLocalesLocked(@NonNull IResultReceiver resultReceiver) { + // TODO: implement this + try { + resultReceiver.send(STATUS_SYNC_CALL_SUCCESS, + SyncResultReceiver.bundleFor(new ArrayList<>())); + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException returning supported locales: " + e); + } + } + + @GuardedBy("mLock") + void onSessionCreatedLocked(@NonNull TranslationSpec sourceSpec, + @NonNull TranslationSpec destSpec, int sessionId, IResultReceiver resultReceiver) { + final RemoteTranslationService remoteService = ensureRemoteServiceLocked(); + if (remoteService != null) { + remoteService.onSessionCreated(sourceSpec, destSpec, sessionId, resultReceiver); + } + } +} |