1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.service.translation;
18 
19 import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_FAIL;
20 import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_SUCCESS;
21 import static android.view.translation.Translator.EXTRA_SERVICE_BINDER;
22 import static android.view.translation.Translator.EXTRA_SESSION_ID;
23 
24 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
25 
26 import android.annotation.CallSuper;
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.annotation.SystemApi;
30 import android.app.Service;
31 import android.content.Intent;
32 import android.os.BaseBundle;
33 import android.os.Bundle;
34 import android.os.CancellationSignal;
35 import android.os.Handler;
36 import android.os.IBinder;
37 import android.os.ICancellationSignal;
38 import android.os.Looper;
39 import android.os.RemoteException;
40 import android.os.ResultReceiver;
41 import android.util.ArraySet;
42 import android.util.Log;
43 import android.view.translation.ITranslationDirectManager;
44 import android.view.translation.ITranslationServiceCallback;
45 import android.view.translation.TranslationCapability;
46 import android.view.translation.TranslationContext;
47 import android.view.translation.TranslationManager;
48 import android.view.translation.TranslationRequest;
49 import android.view.translation.TranslationResponse;
50 import android.view.translation.TranslationSpec;
51 import android.view.translation.Translator;
52 
53 import com.android.internal.os.IResultReceiver;
54 
55 import java.util.Objects;
56 import java.util.Set;
57 import java.util.function.Consumer;
58 
59 /**
60  * Service for translating text.
61  * @hide
62  */
63 @SystemApi
64 public abstract class TranslationService extends Service {
65     private static final String TAG = "TranslationService";
66 
67     /**
68      * The {@link Intent} that must be declared as handled by the service.
69      *
70      * <p>To be supported, the service must also require the
71      * {@link android.Manifest.permission#BIND_TRANSLATION_SERVICE} permission so
72      * that other applications can not abuse it.
73      */
74     public static final String SERVICE_INTERFACE =
75             "android.service.translation.TranslationService";
76 
77     /**
78      * Name under which a TranslationService component publishes information about itself.
79      *
80      * <p>This meta-data should reference an XML resource containing a
81      * <code>&lt;{@link
82      * android.R.styleable#TranslationService translation-service}&gt;</code> tag.
83      *
84      * <p>Here's an example of how to use it on {@code AndroidManifest.xml}:
85      * <pre> &lt;translation-service
86      *     android:settingsActivity="foo.bar.SettingsActivity"
87      *     . . .
88      * /&gt;</pre>
89      */
90     public static final String SERVICE_META_DATA = "android.translation_service";
91 
92     private Handler mHandler;
93     private ITranslationServiceCallback mCallback;
94 
95 
96     /**
97      * Binder to receive calls from system server.
98      */
99     private final ITranslationService mInterface = new ITranslationService.Stub() {
100         @Override
101         public void onConnected(IBinder callback) {
102             mHandler.sendMessage(obtainMessage(TranslationService::handleOnConnected,
103                     TranslationService.this, callback));
104         }
105 
106         @Override
107         public void onDisconnected() {
108             mHandler.sendMessage(obtainMessage(TranslationService::onDisconnected,
109                     TranslationService.this));
110         }
111 
112         @Override
113         public void onCreateTranslationSession(TranslationContext translationContext,
114                 int sessionId, IResultReceiver receiver) throws RemoteException {
115             mHandler.sendMessage(obtainMessage(TranslationService::handleOnCreateTranslationSession,
116                     TranslationService.this, translationContext, sessionId, receiver));
117         }
118 
119         @Override
120         public void onTranslationCapabilitiesRequest(@TranslationSpec.DataFormat int sourceFormat,
121                 @TranslationSpec.DataFormat int targetFormat,
122                 @NonNull ResultReceiver resultReceiver) throws RemoteException {
123             mHandler.sendMessage(
124                     obtainMessage(TranslationService::handleOnTranslationCapabilitiesRequest,
125                             TranslationService.this, sourceFormat, targetFormat,
126                             resultReceiver));
127         }
128     };
129 
130     /**
131      * Interface definition for a callback to be invoked when the translation is compleled.
132      * @removed use a {@link Consumer} instead.
133      */
134     @Deprecated
135     public interface OnTranslationResultCallback {
136         /**
137          * Notifies the Android System that a translation request
138          * {@link TranslationService#onTranslationRequest(TranslationRequest, int,
139          * CancellationSignal, OnTranslationResultCallback)} was successfully fulfilled by the
140          * service.
141          *
142          * <p>This method should always be called, even if the service cannot fulfill the request
143          * (in which case it should be called with a TranslationResponse with
144          * {@link android.view.translation.TranslationResponse#TRANSLATION_STATUS_UNKNOWN_ERROR},
145          * or {@link android.view.translation.TranslationResponse
146          * #TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE}).
147          *
148          * @param response translation response for the provided request infos.
149          *
150          * @throws IllegalStateException if this method was already called.
151          */
onTranslationSuccess(@onNull TranslationResponse response)152         void onTranslationSuccess(@NonNull TranslationResponse response);
153 
154         /**
155          * @removed use {@link #onTranslationSuccess} with an error response instead.
156          */
157         @Deprecated
onError()158         void onError();
159     }
160 
161     /**
162      * Binder that receives calls from the app.
163      */
164     private final ITranslationDirectManager mClientInterface =
165             new ITranslationDirectManager.Stub() {
166                 @Override
167                 public void onTranslationRequest(TranslationRequest request, int sessionId,
168                         ICancellationSignal transport, ITranslationCallback callback)
169                         throws RemoteException {
170                     final Consumer<TranslationResponse> consumer =
171                             new OnTranslationResultCallbackWrapper(callback);
172                     mHandler.sendMessage(obtainMessage(TranslationService::onTranslationRequest,
173                             TranslationService.this, request, sessionId,
174                             CancellationSignal.fromTransport(transport),
175                             consumer));
176                 }
177 
178                 @Override
179                 public void onFinishTranslationSession(int sessionId) throws RemoteException {
180                     mHandler.sendMessage(obtainMessage(
181                             TranslationService::onFinishTranslationSession,
182                             TranslationService.this, sessionId));
183                 }
184             };
185 
186     @CallSuper
187     @Override
onCreate()188     public void onCreate() {
189         super.onCreate();
190         mHandler = new Handler(Looper.getMainLooper(), null, true);
191         BaseBundle.setShouldDefuse(true);
192     }
193 
194     @Override
195     @Nullable
onBind(@onNull Intent intent)196     public final IBinder onBind(@NonNull Intent intent) {
197         if (SERVICE_INTERFACE.equals(intent.getAction())) {
198             return mInterface.asBinder();
199         }
200         Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
201         return null;
202     }
203 
204     /**
205      * Called when the Android system connects to service.
206      *
207      * <p>You should generally do initialization here rather than in {@link #onCreate}.
208      */
onConnected()209     public void onConnected() {
210     }
211 
212     /**
213      * Called when the Android system disconnects from the service.
214      *
215      * <p> At this point this service may no longer be an active {@link TranslationService}.
216      * It should not make calls on {@link TranslationManager} that requires the caller to be
217      * the current service.
218      */
onDisconnected()219     public void onDisconnected() {
220     }
221 
222     /**
223      * Called to notify the service that a session was created
224      * (see {@link android.view.translation.Translator}).
225      *
226      * <p>The service must call {@code callback.accept()} to acknowledge whether the session is
227      * supported and created successfully. If the translation context is not supported, the service
228      * should call back with {@code false}.</p>
229      *
230      * @param translationContext the {@link TranslationContext} of the session being created.
231      * @param sessionId the id of the session.
232      * @param callback {@link Consumer} to notify whether the session was successfully created.
233      */
234     // TODO(b/176464808): the session id won't be unique cross client/server process. Need to find
235     // solution to make it's safe.
onCreateTranslationSession(@onNull TranslationContext translationContext, int sessionId, @NonNull Consumer<Boolean> callback)236     public abstract void onCreateTranslationSession(@NonNull TranslationContext translationContext,
237             int sessionId, @NonNull Consumer<Boolean> callback);
238 
239     /**
240      * @removed use {@link #onCreateTranslationSession(TranslationContext, int, Consumer)}
241      * instead.
242      */
243     @Deprecated
onCreateTranslationSession(@onNull TranslationContext translationContext, int sessionId)244     public void onCreateTranslationSession(@NonNull TranslationContext translationContext,
245             int sessionId) {
246         // no-op
247     }
248 
249     /**
250      * Called when a translation session is finished.
251      *
252      * <p>The translation session is finished when the client calls {@link Translator#destroy()} on
253      * the corresponding translator.
254      *
255      * @param sessionId id of the session that finished.
256      */
onFinishTranslationSession(int sessionId)257     public abstract void onFinishTranslationSession(int sessionId);
258 
259     /**
260      * @removed use
261      * {@link #onTranslationRequest(TranslationRequest, int, CancellationSignal, Consumer)} instead.
262      */
263     @Deprecated
onTranslationRequest(@onNull TranslationRequest request, int sessionId, @Nullable CancellationSignal cancellationSignal, @NonNull OnTranslationResultCallback callback)264     public void onTranslationRequest(@NonNull TranslationRequest request, int sessionId,
265             @Nullable CancellationSignal cancellationSignal,
266             @NonNull OnTranslationResultCallback callback) {
267         // no-op
268     }
269 
270     /**
271      * Called to the service with a {@link TranslationRequest} to be translated.
272      *
273      * <p>The service must call {@code callback.accept()} with the {@link TranslationResponse}. If
274      * {@link TranslationRequest#FLAG_PARTIAL_RESPONSES} was set, the service may call
275      * {@code callback.accept()} multiple times with partial responses.</p>
276      *
277      * @param request The translation request containing the data to be translated.
278      * @param sessionId id of the session that sent the translation request.
279      * @param cancellationSignal A {@link CancellationSignal} that notifies when a client has
280      *                           cancelled the operation in progress.
281      * @param callback {@link Consumer} to pass back the translation response.
282      */
onTranslationRequest(@onNull TranslationRequest request, int sessionId, @Nullable CancellationSignal cancellationSignal, @NonNull Consumer<TranslationResponse> callback)283     public abstract void onTranslationRequest(@NonNull TranslationRequest request, int sessionId,
284             @Nullable CancellationSignal cancellationSignal,
285             @NonNull Consumer<TranslationResponse> callback);
286 
287     /**
288      * Called to request a set of {@link TranslationCapability}s that are supported by the service.
289      *
290      * <p>The set of translation capabilities are limited to those supporting the source and target
291      * {@link TranslationSpec.DataFormat}. e.g. Calling this with
292      * {@link TranslationSpec#DATA_FORMAT_TEXT} as source and target returns only capabilities that
293      * translates text to text.</p>
294      *
295      * <p>Must call {@code callback.accept} to pass back the set of translation capabilities.</p>
296      *
297      * @param sourceFormat data format restriction of the translation source spec.
298      * @param targetFormat data format restriction of the translation target spec.
299      * @param callback {@link Consumer} to pass back the set of translation capabilities.
300      */
onTranslationCapabilitiesRequest( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull Consumer<Set<TranslationCapability>> callback)301     public abstract void onTranslationCapabilitiesRequest(
302             @TranslationSpec.DataFormat int sourceFormat,
303             @TranslationSpec.DataFormat int targetFormat,
304             @NonNull Consumer<Set<TranslationCapability>> callback);
305 
306     /**
307      * Called by the service to notify an update in existing {@link TranslationCapability}s.
308      *
309      * @param capability the updated {@link TranslationCapability} with its new states and flags.
310      */
updateTranslationCapability(@onNull TranslationCapability capability)311     public final void updateTranslationCapability(@NonNull TranslationCapability capability) {
312         Objects.requireNonNull(capability, "translation capability should not be null");
313 
314         final ITranslationServiceCallback callback = mCallback;
315         if (callback == null) {
316             Log.w(TAG, "updateTranslationCapability(): no server callback");
317             return;
318         }
319 
320         try {
321             callback.updateTranslationCapability(capability);
322         } catch (RemoteException e) {
323             e.rethrowFromSystemServer();
324         }
325     }
326 
handleOnConnected(@onNull IBinder callback)327     private void handleOnConnected(@NonNull IBinder callback) {
328         mCallback = ITranslationServiceCallback.Stub.asInterface(callback);
329         onConnected();
330     }
331 
332     // TODO(b/176464808): Need to handle client dying case
333 
handleOnCreateTranslationSession(@onNull TranslationContext translationContext, int sessionId, IResultReceiver resultReceiver)334     private void handleOnCreateTranslationSession(@NonNull TranslationContext translationContext,
335             int sessionId, IResultReceiver resultReceiver) {
336         onCreateTranslationSession(translationContext, sessionId,
337                 new Consumer<Boolean>() {
338                     @Override
339                     public void accept(Boolean created) {
340                         try {
341                             if (!created) {
342                                 Log.w(TAG, "handleOnCreateTranslationSession(): context="
343                                         + translationContext + " not supported by service.");
344                                 resultReceiver.send(STATUS_SYNC_CALL_FAIL, null);
345                                 return;
346                             }
347 
348                             final Bundle extras = new Bundle();
349                             extras.putBinder(EXTRA_SERVICE_BINDER, mClientInterface.asBinder());
350                             extras.putInt(EXTRA_SESSION_ID, sessionId);
351                             resultReceiver.send(STATUS_SYNC_CALL_SUCCESS, extras);
352                         } catch (RemoteException e) {
353                             Log.w(TAG, "RemoteException sending client interface: " + e);
354                         }
355                     }
356                 });
357 
358     }
359 
handleOnTranslationCapabilitiesRequest( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull ResultReceiver resultReceiver)360     private void handleOnTranslationCapabilitiesRequest(
361             @TranslationSpec.DataFormat int sourceFormat,
362             @TranslationSpec.DataFormat int targetFormat,
363             @NonNull ResultReceiver resultReceiver) {
364         onTranslationCapabilitiesRequest(sourceFormat, targetFormat,
365                 new Consumer<Set<TranslationCapability>>() {
366                     @Override
367                     public void accept(Set<TranslationCapability> values) {
368                         if (!isValidCapabilities(sourceFormat, targetFormat, values)) {
369                             throw new IllegalStateException("Invalid capabilities and "
370                                     + "format compatibility");
371                         }
372 
373                         final ArraySet<TranslationCapability> capabilities = new ArraySet<>(values);
374                         final Bundle bundle = new Bundle();
375                         bundle.putParcelableArray(TranslationManager.EXTRA_CAPABILITIES,
376                                 capabilities.toArray(new TranslationCapability[0]));
377                         resultReceiver.send(STATUS_SYNC_CALL_SUCCESS, bundle);
378                     }
379                 });
380     }
381 
382     /**
383      * Helper method to validate capabilities and format compatibility.
384      */
isValidCapabilities(@ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, Set<TranslationCapability> capabilities)385     private boolean isValidCapabilities(@TranslationSpec.DataFormat int sourceFormat,
386             @TranslationSpec.DataFormat int targetFormat, Set<TranslationCapability> capabilities) {
387         if (sourceFormat != TranslationSpec.DATA_FORMAT_TEXT
388                 && targetFormat != TranslationSpec.DATA_FORMAT_TEXT) {
389             return true;
390         }
391 
392         for (TranslationCapability capability : capabilities) {
393             if (capability.getState() == TranslationCapability.STATE_REMOVED_AND_AVAILABLE) {
394                 return false;
395             }
396         }
397 
398         return true;
399     }
400 }
401