/* * 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.TranslationManager.STATUS_SYNC_CALL_FAIL; import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_SUCCESS; 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.content.pm.ParceledListSlice; import android.os.BaseBundle; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Handler; import android.os.IBinder; import android.os.ICancellationSignal; import android.os.Looper; import android.os.RemoteException; import android.os.ResultReceiver; import android.util.Log; import android.view.translation.ITranslationDirectManager; import android.view.translation.ITranslationServiceCallback; import android.view.translation.TranslationCapability; import android.view.translation.TranslationContext; import android.view.translation.TranslationManager; import android.view.translation.TranslationRequest; import android.view.translation.TranslationResponse; import android.view.translation.TranslationSpec; import android.view.translation.Translator; import com.android.internal.os.IResultReceiver; import java.util.Arrays; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; /** * 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. * *

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

This meta-data should reference an XML resource containing a * <{@link * android.R.styleable#TranslationService translation-service}> tag. * *

Here's an example of how to use it on {@code AndroidManifest.xml}: *

 <translation-service
     *     android:settingsActivity="foo.bar.SettingsActivity"
     *     . . .
     * />
*/ public static final String SERVICE_META_DATA = "android.translation_service"; private Handler mHandler; private ITranslationServiceCallback mCallback; /** * Binder to receive calls from system server. */ private final ITranslationService mInterface = new ITranslationService.Stub() { @Override public void onConnected(IBinder callback) { mHandler.sendMessage(obtainMessage(TranslationService::handleOnConnected, TranslationService.this, callback)); } @Override public void onDisconnected() { mHandler.sendMessage(obtainMessage(TranslationService::onDisconnected, TranslationService.this)); } @Override public void onCreateTranslationSession(TranslationContext translationContext, int sessionId, IResultReceiver receiver) throws RemoteException { mHandler.sendMessage(obtainMessage(TranslationService::handleOnCreateTranslationSession, TranslationService.this, translationContext, sessionId, receiver)); } @Override public void onTranslationCapabilitiesRequest(@TranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull ResultReceiver resultReceiver) throws RemoteException { mHandler.sendMessage( obtainMessage(TranslationService::handleOnTranslationCapabilitiesRequest, TranslationService.this, sourceFormat, targetFormat, resultReceiver)); } }; /** * Interface definition for a callback to be invoked when the translation is compleled. * @removed use a {@link Consumer} instead. */ @Deprecated public interface OnTranslationResultCallback { /** * Notifies the Android System that a translation request * {@link TranslationService#onTranslationRequest(TranslationRequest, int, * CancellationSignal, OnTranslationResultCallback)} was successfully fulfilled by the * service. * *

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); /** * @removed use {@link #onTranslationSuccess} with an error response instead. */ @Deprecated void onError(); } /** * Binder that receives calls from the app. */ private final ITranslationDirectManager mClientInterface = new ITranslationDirectManager.Stub() { @Override public void onTranslationRequest(TranslationRequest request, int sessionId, ICancellationSignal transport, ITranslationCallback callback) throws RemoteException { final Consumer consumer = new OnTranslationResultCallbackWrapper(callback); mHandler.sendMessage(obtainMessage(TranslationService::onTranslationRequest, TranslationService.this, request, sessionId, CancellationSignal.fromTransport(transport), consumer)); } @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. * *

You should generally do initialization here rather than in {@link #onCreate}. */ public void onConnected() { } /** * Called when the Android system disconnects from the service. * *

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() { } /** * Called to notify the service that a session was created * (see {@link android.view.translation.Translator}). * *

The service must call {@code callback.accept()} to acknowledge whether the session is * supported and created successfully. If the translation context is not supported, the service * should call back with {@code false}.

* * @param translationContext the {@link TranslationContext} of the session being created. * @param sessionId the id of the session. * @param callback {@link Consumer} to notify whether the session was successfully created. */ // 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 TranslationContext translationContext, int sessionId, @NonNull Consumer callback); /** * @removed use {@link #onCreateTranslationSession(TranslationContext, int, Consumer)} * instead. */ @Deprecated public void onCreateTranslationSession(@NonNull TranslationContext translationContext, int sessionId) { // no-op } /** * Called when a translation session is finished. * *

The translation session is finished when the client calls {@link Translator#destroy()} on * the corresponding translator. * * @param sessionId id of the session that finished. */ public abstract void onFinishTranslationSession(int sessionId); /** * @removed use * {@link #onTranslationRequest(TranslationRequest, int, CancellationSignal, Consumer)} instead. */ @Deprecated public void onTranslationRequest(@NonNull TranslationRequest request, int sessionId, @Nullable CancellationSignal cancellationSignal, @NonNull OnTranslationResultCallback callback) { // no-op } /** * Called to the service with a {@link TranslationRequest} to be translated. * *

The service must call {@code callback.accept()} with the {@link TranslationResponse}. If * {@link TranslationRequest#FLAG_PARTIAL_RESPONSES} was set, the service may call * {@code callback.accept()} multiple times with partial responses.

* * @param request The translation request containing the data to be translated. * @param sessionId id of the session that sent the translation request. * @param cancellationSignal A {@link CancellationSignal} that notifies when a client has * cancelled the operation in progress. * @param callback {@link Consumer} to pass back the translation response. */ public abstract void onTranslationRequest(@NonNull TranslationRequest request, int sessionId, @Nullable CancellationSignal cancellationSignal, @NonNull Consumer callback); /** * Called to request a set of {@link TranslationCapability}s that are supported by the service. * *

The set of translation capabilities are limited to those supporting the source and target * {@link TranslationSpec.DataFormat}. e.g. Calling this with * {@link TranslationSpec#DATA_FORMAT_TEXT} as source and target returns only capabilities that * translates text to text.

* *

Must call {@code callback.accept} to pass back the set of translation capabilities.

* * @param sourceFormat data format restriction of the translation source spec. * @param targetFormat data format restriction of the translation target spec. * @param callback {@link Consumer} to pass back the set of translation capabilities. */ public abstract void onTranslationCapabilitiesRequest( @TranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull Consumer> callback); /** * Called by the service to notify an update in existing {@link TranslationCapability}s. * * @param capability the updated {@link TranslationCapability} with its new states and flags. */ public final void updateTranslationCapability(@NonNull TranslationCapability capability) { Objects.requireNonNull(capability, "translation capability should not be null"); final ITranslationServiceCallback callback = mCallback; if (callback == null) { Log.w(TAG, "updateTranslationCapability(): no server callback"); return; } try { callback.updateTranslationCapability(capability); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } private void handleOnConnected(@NonNull IBinder callback) { mCallback = ITranslationServiceCallback.Stub.asInterface(callback); onConnected(); } // TODO(b/176464808): Need to handle client dying case private void handleOnCreateTranslationSession(@NonNull TranslationContext translationContext, int sessionId, IResultReceiver resultReceiver) { onCreateTranslationSession(translationContext, sessionId, new Consumer() { @Override public void accept(Boolean created) { try { if (!created) { Log.w(TAG, "handleOnCreateTranslationSession(): context=" + translationContext + " not supported by service."); resultReceiver.send(STATUS_SYNC_CALL_FAIL, null); return; } final Bundle extras = new Bundle(); extras.putBinder(EXTRA_SERVICE_BINDER, mClientInterface.asBinder()); extras.putInt(EXTRA_SESSION_ID, sessionId); resultReceiver.send(STATUS_SYNC_CALL_SUCCESS, extras); } catch (RemoteException e) { Log.w(TAG, "RemoteException sending client interface: " + e); } } }); } private void handleOnTranslationCapabilitiesRequest( @TranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull ResultReceiver resultReceiver) { onTranslationCapabilitiesRequest(sourceFormat, targetFormat, new Consumer>() { @Override public void accept(Set values) { if (!isValidCapabilities(sourceFormat, targetFormat, values)) { throw new IllegalStateException("Invalid capabilities and " + "format compatibility"); } final Bundle bundle = new Bundle(); final ParceledListSlice listSlice = new ParceledListSlice<>(Arrays.asList( values.toArray(new TranslationCapability[0]))); bundle.putParcelable(TranslationManager.EXTRA_CAPABILITIES, listSlice); resultReceiver.send(STATUS_SYNC_CALL_SUCCESS, bundle); } }); } /** * Helper method to validate capabilities and format compatibility. */ private boolean isValidCapabilities(@TranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, Set capabilities) { if (sourceFormat != TranslationSpec.DATA_FORMAT_TEXT && targetFormat != TranslationSpec.DATA_FORMAT_TEXT) { return true; } for (TranslationCapability capability : capabilities) { if (capability.getState() == TranslationCapability.STATE_REMOVED_AND_AVAILABLE) { return false; } } return true; } }