• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.view.translation;
18 
19 import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_FAIL;
20 import static android.view.translation.TranslationManager.SYNC_CALLS_TIMEOUT_MS;
21 import static android.view.translation.UiTranslationController.DEBUG;
22 
23 import android.annotation.CallbackExecutor;
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.annotation.SuppressLint;
27 import android.content.Context;
28 import android.os.Binder;
29 import android.os.Bundle;
30 import android.os.CancellationSignal;
31 import android.os.Handler;
32 import android.os.IBinder;
33 import android.os.ICancellationSignal;
34 import android.os.RemoteException;
35 import android.service.translation.ITranslationCallback;
36 import android.util.Log;
37 
38 import com.android.internal.annotations.GuardedBy;
39 import com.android.internal.os.IResultReceiver;
40 
41 import java.io.PrintWriter;
42 import java.util.Objects;
43 import java.util.concurrent.CountDownLatch;
44 import java.util.concurrent.Executor;
45 import java.util.concurrent.TimeUnit;
46 import java.util.function.Consumer;
47 
48 /**
49  * The {@link Translator} for translation, defined by a {@link TranslationContext}.
50  */
51 @SuppressLint("NotCloseable")
52 public class Translator {
53 
54     private static final String TAG = "Translator";
55 
56     private final Object mLock = new Object();
57 
58     private int mId;
59 
60     @NonNull
61     private final Context mContext;
62 
63     @NonNull
64     private final TranslationContext mTranslationContext;
65 
66     @NonNull
67     private final TranslationManager mManager;
68 
69     @NonNull
70     private final Handler mHandler;
71 
72     /**
73      * Interface to the system_server binder object.
74      */
75     private ITranslationManager mSystemServerBinder;
76 
77     /**
78      * Direct interface to the TranslationService binder object.
79      */
80     @Nullable
81     private ITranslationDirectManager mDirectServiceBinder;
82 
83     @NonNull
84     private final ServiceBinderReceiver mServiceBinderReceiver;
85 
86     @GuardedBy("mLock")
87     private boolean mDestroyed;
88 
89     /**
90      * Name of the {@link IResultReceiver} extra used to pass the binder interface to Translator.
91      * @hide
92      */
93     public static final String EXTRA_SERVICE_BINDER = "binder";
94     /**
95      * Name of the extra used to pass the session id to Translator.
96      * @hide
97      */
98     public static final String EXTRA_SESSION_ID = "sessionId";
99 
100     static class ServiceBinderReceiver extends IResultReceiver.Stub {
101         // TODO: refactor how translator is instantiated after removing deprecated createTranslator.
102         private final Translator mTranslator;
103         private final CountDownLatch mLatch = new CountDownLatch(1);
104         private int mSessionId;
105 
106         private Consumer<Translator> mCallback;
107 
ServiceBinderReceiver(Translator translator, Consumer<Translator> callback)108         ServiceBinderReceiver(Translator translator, Consumer<Translator> callback) {
109             mTranslator = translator;
110             mCallback = callback;
111         }
112 
ServiceBinderReceiver(Translator translator)113         ServiceBinderReceiver(Translator translator) {
114             mTranslator = translator;
115         }
116 
getSessionStateResult()117         int getSessionStateResult() throws TimeoutException {
118             try {
119                 if (!mLatch.await(SYNC_CALLS_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
120                     throw new TimeoutException(
121                             "Session not created in " + SYNC_CALLS_TIMEOUT_MS + "ms");
122                 }
123             } catch (InterruptedException e) {
124                 Thread.currentThread().interrupt();
125                 throw new TimeoutException("Session not created because interrupted");
126             }
127             return mSessionId;
128         }
129 
130         @Override
send(int resultCode, Bundle resultData)131         public void send(int resultCode, Bundle resultData) {
132             if (resultCode == STATUS_SYNC_CALL_FAIL) {
133                 mLatch.countDown();
134                 if (mCallback != null) {
135                     mCallback.accept(null);
136                 }
137                 return;
138             }
139             final IBinder binder;
140             if (resultData != null) {
141                 mSessionId = resultData.getInt(EXTRA_SESSION_ID);
142                 binder = resultData.getBinder(EXTRA_SERVICE_BINDER);
143                 if (binder == null) {
144                     Log.wtf(TAG, "No " + EXTRA_SERVICE_BINDER + " extra result");
145                     return;
146                 }
147             } else {
148                 binder = null;
149             }
150             mTranslator.setServiceBinder(binder);
151             mLatch.countDown();
152             if (mCallback != null) {
153                 mCallback.accept(mTranslator);
154             }
155         }
156 
157         // TODO(b/176464808): maybe make SyncResultReceiver.TimeoutException constructor public
158         //  and use it.
159         static final class TimeoutException extends Exception {
TimeoutException(String msg)160             private TimeoutException(String msg) {
161                 super(msg);
162             }
163         }
164     }
165 
166     /**
167      * Create the Translator.
168      *
169      * @hide
170      */
Translator(@onNull Context context, @NonNull TranslationContext translationContext, int sessionId, @NonNull TranslationManager translationManager, @NonNull Handler handler, @Nullable ITranslationManager systemServerBinder, @NonNull Consumer<Translator> callback)171     public Translator(@NonNull Context context,
172             @NonNull TranslationContext translationContext, int sessionId,
173             @NonNull TranslationManager translationManager, @NonNull Handler handler,
174             @Nullable ITranslationManager systemServerBinder,
175             @NonNull Consumer<Translator> callback) {
176         mContext = context;
177         mTranslationContext = translationContext;
178         mId = sessionId;
179         mManager = translationManager;
180         mHandler = handler;
181         mSystemServerBinder = systemServerBinder;
182         mServiceBinderReceiver = new ServiceBinderReceiver(this, callback);
183 
184         try {
185             mSystemServerBinder.onSessionCreated(mTranslationContext, mId,
186                     mServiceBinderReceiver, mContext.getUserId());
187         } catch (RemoteException e) {
188             Log.w(TAG, "RemoteException calling startSession(): " + e);
189         }
190     }
191 
192     /**
193      * Create the Translator.
194      *
195      * @hide
196      */
Translator(@onNull Context context, @NonNull TranslationContext translationContext, int sessionId, @NonNull TranslationManager translationManager, @NonNull Handler handler, @Nullable ITranslationManager systemServerBinder)197     public Translator(@NonNull Context context,
198             @NonNull TranslationContext translationContext, int sessionId,
199             @NonNull TranslationManager translationManager, @NonNull Handler handler,
200             @Nullable ITranslationManager systemServerBinder) {
201         mContext = context;
202         mTranslationContext = translationContext;
203         mId = sessionId;
204         mManager = translationManager;
205         mHandler = handler;
206         mSystemServerBinder = systemServerBinder;
207         mServiceBinderReceiver = new ServiceBinderReceiver(this);
208     }
209 
210     /**
211      * Starts this Translator session.
212      */
start()213     void start() {
214         try {
215             mSystemServerBinder.onSessionCreated(mTranslationContext, mId,
216                     mServiceBinderReceiver, mContext.getUserId());
217         } catch (RemoteException e) {
218             Log.w(TAG, "RemoteException calling startSession(): " + e);
219         }
220     }
221 
222     /**
223      * Wait this Translator session created.
224      *
225      * @return {@code true} if the session is created successfully.
226      */
isSessionCreated()227     boolean isSessionCreated() throws ServiceBinderReceiver.TimeoutException {
228         int receivedId = mServiceBinderReceiver.getSessionStateResult();
229         return receivedId > 0;
230     }
231 
getNextRequestId()232     private int getNextRequestId() {
233         // Get from manager to keep the request id unique to different Translators
234         return mManager.getAvailableRequestId().getAndIncrement();
235     }
236 
setServiceBinder(@ullable IBinder binder)237     private void setServiceBinder(@Nullable IBinder binder) {
238         synchronized (mLock) {
239             if (mDirectServiceBinder != null) {
240                 return;
241             }
242             if (binder != null) {
243                 mDirectServiceBinder = ITranslationDirectManager.Stub.asInterface(binder);
244             }
245         }
246     }
247 
248     /** @hide */
getTranslationContext()249     public TranslationContext getTranslationContext() {
250         return mTranslationContext;
251     }
252 
253     /** @hide */
getTranslatorId()254     public int getTranslatorId() {
255         return mId;
256     }
257 
258     /** @hide */
dump(@onNull String prefix, @NonNull PrintWriter pw)259     public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
260         pw.print(prefix); pw.print("translationContext: "); pw.println(mTranslationContext);
261     }
262 
263     /**
264      * Requests a translation for the provided {@link TranslationRequest} using the Translator's
265      * source spec and destination spec.
266      *
267      * @param request {@link TranslationRequest} request to be translate.
268      *
269      * @throws IllegalStateException if this Translator session was destroyed when called.
270      *
271      * @removed use {@link #translate(TranslationRequest, CancellationSignal,
272      *             Executor, Consumer)} instead.
273      */
274     @Deprecated
275     @Nullable
translate(@onNull TranslationRequest request, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<TranslationResponse> callback)276     public void translate(@NonNull TranslationRequest request,
277             @NonNull @CallbackExecutor Executor executor,
278             @NonNull Consumer<TranslationResponse> callback) {
279         Objects.requireNonNull(request, "Translation request cannot be null");
280         Objects.requireNonNull(executor, "Executor cannot be null");
281         Objects.requireNonNull(callback, "Callback cannot be null");
282 
283         if (isDestroyed()) {
284             // TODO(b/176464808): Disallow multiple Translator now, it will throw
285             //  IllegalStateException. Need to discuss if we can allow multiple Translators.
286             throw new IllegalStateException(
287                     "This translator has been destroyed");
288         }
289 
290         final ITranslationCallback responseCallback =
291                 new TranslationResponseCallbackImpl(callback, executor);
292         try {
293             mDirectServiceBinder.onTranslationRequest(request, mId,
294                     CancellationSignal.createTransport(), responseCallback);
295         } catch (RemoteException e) {
296             Log.w(TAG, "RemoteException calling requestTranslate(): " + e);
297         }
298     }
299 
300     /**
301      * Requests a translation for the provided {@link TranslationRequest} using the Translator's
302      * source spec and destination spec.
303      *
304      * @param request {@link TranslationRequest} request to be translate.
305      * @param cancellationSignal signal to cancel the operation in progress.
306      * @param executor Executor to run callback operations
307      * @param callback {@link Consumer} to receive the translation response. Multiple responses may
308      *                 be received if {@link TranslationRequest#FLAG_PARTIAL_RESPONSES} is set.
309      *
310      * @throws IllegalStateException if this Translator session was destroyed when called.
311      */
312     @Nullable
translate(@onNull TranslationRequest request, @Nullable CancellationSignal cancellationSignal, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<TranslationResponse> callback)313     public void translate(@NonNull TranslationRequest request,
314             @Nullable CancellationSignal cancellationSignal,
315             @NonNull @CallbackExecutor Executor executor,
316             @NonNull Consumer<TranslationResponse> callback) {
317         Objects.requireNonNull(request, "Translation request cannot be null");
318         Objects.requireNonNull(executor, "Executor cannot be null");
319         Objects.requireNonNull(callback, "Callback cannot be null");
320 
321         if (isDestroyed()) {
322             // TODO(b/176464808): Disallow multiple Translator now, it will throw
323             //  IllegalStateException. Need to discuss if we can allow multiple Translators.
324             throw new IllegalStateException(
325                     "This translator has been destroyed");
326         }
327 
328         ICancellationSignal transport = null;
329         if (cancellationSignal != null) {
330             transport = CancellationSignal.createTransport();
331             cancellationSignal.setRemote(transport);
332         }
333         final ITranslationCallback responseCallback =
334                 new TranslationResponseCallbackImpl(callback, executor);
335 
336         try {
337             mDirectServiceBinder.onTranslationRequest(request, mId, transport,
338                     responseCallback);
339         } catch (RemoteException e) {
340             Log.w(TAG, "RemoteException calling requestTranslate(): " + e);
341         }
342     }
343 
344     /**
345      * Destroy this Translator.
346      */
destroy()347     public void destroy() {
348         synchronized (mLock) {
349             if (mDestroyed) {
350                 return;
351             }
352             mDestroyed = true;
353             try {
354                 mDirectServiceBinder.onFinishTranslationSession(mId);
355             } catch (RemoteException e) {
356                 Log.w(TAG, "RemoteException calling onSessionFinished");
357             }
358             mDirectServiceBinder = null;
359             mManager.removeTranslator(mId);
360         }
361     }
362 
363     /**
364      * Returns whether or not this Translator has been destroyed.
365      *
366      * @see #destroy()
367      */
isDestroyed()368     public boolean isDestroyed() {
369         synchronized (mLock) {
370             return mDestroyed;
371         }
372     }
373 
374     // TODO: add methods for UI-toolkit case.
375     /** @hide */
requestUiTranslate(@onNull TranslationRequest request, @NonNull Executor executor, @NonNull Consumer<TranslationResponse> callback)376     public void requestUiTranslate(@NonNull TranslationRequest request,
377             @NonNull Executor executor,
378             @NonNull Consumer<TranslationResponse> callback) {
379         if (mDirectServiceBinder == null) {
380             Log.wtf(TAG, "Translator created without proper initialization.");
381             return;
382         }
383         final ITranslationCallback translationCallback =
384                 new TranslationResponseCallbackImpl(callback, executor);
385         try {
386             mDirectServiceBinder.onTranslationRequest(request, mId,
387                     CancellationSignal.createTransport(), translationCallback);
388         } catch (RemoteException e) {
389             Log.w(TAG, "RemoteException calling flushRequest");
390         }
391     }
392 
393     private static class TranslationResponseCallbackImpl extends ITranslationCallback.Stub {
394 
395         private final Consumer<TranslationResponse> mCallback;
396         private final Executor mExecutor;
397 
TranslationResponseCallbackImpl(Consumer<TranslationResponse> callback, Executor executor)398         TranslationResponseCallbackImpl(Consumer<TranslationResponse> callback, Executor executor) {
399             mCallback = callback;
400             mExecutor = executor;
401         }
402 
403         @Override
onTranslationResponse(TranslationResponse response)404         public void onTranslationResponse(TranslationResponse response) throws RemoteException {
405             if (DEBUG) {
406                 Log.i(TAG, "onTranslationResponse called.");
407             }
408             final Runnable runnable =
409                     () -> mCallback.accept(response);
410             final long token = Binder.clearCallingIdentity();
411             try {
412                 mExecutor.execute(runnable);
413             } finally {
414                 restoreCallingIdentity(token);
415             }
416         }
417     }
418 }
419