blob: de981e82437579aa99d2979cbcc5f495ed5bbb34 [file] [log] [blame]
Aurimas Liutikas88c7ff12023-08-10 12:42:26 -07001/*
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
17package android.service.translation;
18
19import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_FAIL;
20import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_SUCCESS;
21import static android.view.translation.Translator.EXTRA_SERVICE_BINDER;
22import static android.view.translation.Translator.EXTRA_SESSION_ID;
23
24import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
25
26import android.annotation.CallSuper;
27import android.annotation.NonNull;
28import android.annotation.Nullable;
29import android.annotation.SystemApi;
30import android.app.Service;
31import android.content.Intent;
32import android.content.pm.ParceledListSlice;
33import android.os.BaseBundle;
34import android.os.Bundle;
35import android.os.CancellationSignal;
36import android.os.Handler;
37import android.os.IBinder;
38import android.os.ICancellationSignal;
39import android.os.Looper;
40import android.os.RemoteException;
41import android.os.ResultReceiver;
42import android.util.Log;
43import android.view.translation.ITranslationDirectManager;
44import android.view.translation.ITranslationServiceCallback;
45import android.view.translation.TranslationCapability;
46import android.view.translation.TranslationContext;
47import android.view.translation.TranslationManager;
48import android.view.translation.TranslationRequest;
49import android.view.translation.TranslationResponse;
50import android.view.translation.TranslationSpec;
51import android.view.translation.Translator;
52
53import com.android.internal.os.IResultReceiver;
54
55import java.util.Arrays;
56import java.util.Objects;
57import java.util.Set;
58import java.util.function.Consumer;
59
60/**
61 * Service for translating text.
62 * @hide
63 */
64@SystemApi
65public 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 */
153 void onTranslationSuccess(@NonNull TranslationResponse response);
154
155 /**
156 * @removed use {@link #onTranslationSuccess} with an error response instead.
157 */
158 @Deprecated
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
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
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 */
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 */
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.
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
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 */
258 public abstract void onFinishTranslationSession(int sessionId);
259
260 /**
261 * @removed use
262 * {@link #onTranslationRequest(TranslationRequest, int, CancellationSignal, Consumer)} instead.
263 */
264 @Deprecated
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 */
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 */
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 */
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
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
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
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 */
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}