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