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