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