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