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