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