| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.view.translation; |
| |
| import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_FAIL; |
| import static android.view.translation.TranslationManager.SYNC_CALLS_TIMEOUT_MS; |
| import static android.view.translation.UiTranslationController.DEBUG; |
| |
| import android.annotation.CallbackExecutor; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.SuppressLint; |
| import android.content.Context; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.CancellationSignal; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.ICancellationSignal; |
| import android.os.RemoteException; |
| import android.service.translation.ITranslationCallback; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.os.IResultReceiver; |
| |
| import java.io.PrintWriter; |
| import java.util.Objects; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.Consumer; |
| |
| /** |
| * The {@link Translator} for translation, defined by a {@link TranslationContext}. |
| */ |
| @SuppressLint("NotCloseable") |
| public class Translator { |
| |
| private static final String TAG = "Translator"; |
| |
| // TODO: make this configurable and cross the Translation component |
| private static boolean sDEBUG = false; |
| |
| private final Object mLock = new Object(); |
| |
| private int mId; |
| |
| @NonNull |
| private final Context mContext; |
| |
| @NonNull |
| private final TranslationContext mTranslationContext; |
| |
| @NonNull |
| private final TranslationManager mManager; |
| |
| @NonNull |
| private final Handler mHandler; |
| |
| /** |
| * Interface to the system_server binder object. |
| */ |
| private ITranslationManager mSystemServerBinder; |
| |
| /** |
| * Direct interface to the TranslationService binder object. |
| */ |
| @Nullable |
| private ITranslationDirectManager mDirectServiceBinder; |
| |
| @NonNull |
| private final ServiceBinderReceiver mServiceBinderReceiver; |
| |
| @GuardedBy("mLock") |
| private boolean mDestroyed; |
| |
| /** |
| * Name of the {@link IResultReceiver} extra used to pass the binder interface to Translator. |
| * @hide |
| */ |
| public static final String EXTRA_SERVICE_BINDER = "binder"; |
| /** |
| * Name of the extra used to pass the session id to Translator. |
| * @hide |
| */ |
| public static final String EXTRA_SESSION_ID = "sessionId"; |
| |
| static class ServiceBinderReceiver extends IResultReceiver.Stub { |
| // TODO: refactor how translator is instantiated after removing deprecated createTranslator. |
| private final Translator mTranslator; |
| private final CountDownLatch mLatch = new CountDownLatch(1); |
| private int mSessionId; |
| |
| private Consumer<Translator> mCallback; |
| |
| ServiceBinderReceiver(Translator translator, Consumer<Translator> callback) { |
| mTranslator = translator; |
| mCallback = callback; |
| } |
| |
| ServiceBinderReceiver(Translator translator) { |
| mTranslator = translator; |
| } |
| |
| int getSessionStateResult() throws TimeoutException { |
| try { |
| if (!mLatch.await(SYNC_CALLS_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { |
| throw new TimeoutException( |
| "Session not created in " + SYNC_CALLS_TIMEOUT_MS + "ms"); |
| } |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| throw new TimeoutException("Session not created because interrupted"); |
| } |
| return mSessionId; |
| } |
| |
| @Override |
| public void send(int resultCode, Bundle resultData) { |
| if (resultCode == STATUS_SYNC_CALL_FAIL) { |
| mLatch.countDown(); |
| if (mCallback != null) { |
| mCallback.accept(null); |
| } |
| return; |
| } |
| final IBinder binder; |
| if (resultData != null) { |
| mSessionId = resultData.getInt(EXTRA_SESSION_ID); |
| binder = resultData.getBinder(EXTRA_SERVICE_BINDER); |
| if (binder == null) { |
| Log.wtf(TAG, "No " + EXTRA_SERVICE_BINDER + " extra result"); |
| return; |
| } |
| } else { |
| binder = null; |
| } |
| mTranslator.setServiceBinder(binder); |
| mLatch.countDown(); |
| if (mCallback != null) { |
| mCallback.accept(mTranslator); |
| } |
| } |
| |
| // TODO(b/176464808): maybe make SyncResultReceiver.TimeoutException constructor public |
| // and use it. |
| static final class TimeoutException extends Exception { |
| private TimeoutException(String msg) { |
| super(msg); |
| } |
| } |
| } |
| |
| /** |
| * Create the Translator. |
| * |
| * @hide |
| */ |
| public Translator(@NonNull Context context, |
| @NonNull TranslationContext translationContext, int sessionId, |
| @NonNull TranslationManager translationManager, @NonNull Handler handler, |
| @Nullable ITranslationManager systemServerBinder, |
| @NonNull Consumer<Translator> callback) { |
| mContext = context; |
| mTranslationContext = translationContext; |
| mId = sessionId; |
| mManager = translationManager; |
| mHandler = handler; |
| mSystemServerBinder = systemServerBinder; |
| mServiceBinderReceiver = new ServiceBinderReceiver(this, callback); |
| |
| try { |
| mSystemServerBinder.onSessionCreated(mTranslationContext, mId, |
| mServiceBinderReceiver, mContext.getUserId()); |
| } catch (RemoteException e) { |
| Log.w(TAG, "RemoteException calling startSession(): " + e); |
| } |
| } |
| |
| /** |
| * Create the Translator. |
| * |
| * @hide |
| */ |
| public Translator(@NonNull Context context, |
| @NonNull TranslationContext translationContext, int sessionId, |
| @NonNull TranslationManager translationManager, @NonNull Handler handler, |
| @Nullable ITranslationManager systemServerBinder) { |
| mContext = context; |
| mTranslationContext = translationContext; |
| mId = sessionId; |
| mManager = translationManager; |
| mHandler = handler; |
| mSystemServerBinder = systemServerBinder; |
| mServiceBinderReceiver = new ServiceBinderReceiver(this); |
| } |
| |
| /** |
| * Starts this Translator session. |
| */ |
| void start() { |
| try { |
| mSystemServerBinder.onSessionCreated(mTranslationContext, mId, |
| mServiceBinderReceiver, mContext.getUserId()); |
| } catch (RemoteException e) { |
| Log.w(TAG, "RemoteException calling startSession(): " + e); |
| } |
| } |
| |
| /** |
| * Wait this Translator session created. |
| * |
| * @return {@code true} if the session is created successfully. |
| */ |
| boolean isSessionCreated() throws ServiceBinderReceiver.TimeoutException { |
| int receivedId = mServiceBinderReceiver.getSessionStateResult(); |
| return receivedId > 0; |
| } |
| |
| private int getNextRequestId() { |
| // Get from manager to keep the request id unique to different Translators |
| return mManager.getAvailableRequestId().getAndIncrement(); |
| } |
| |
| private void setServiceBinder(@Nullable IBinder binder) { |
| synchronized (mLock) { |
| if (mDirectServiceBinder != null) { |
| return; |
| } |
| if (binder != null) { |
| mDirectServiceBinder = ITranslationDirectManager.Stub.asInterface(binder); |
| } |
| } |
| } |
| |
| /** @hide */ |
| public TranslationContext getTranslationContext() { |
| return mTranslationContext; |
| } |
| |
| /** @hide */ |
| public int getTranslatorId() { |
| return mId; |
| } |
| |
| /** @hide */ |
| public void dump(@NonNull String prefix, @NonNull PrintWriter pw) { |
| pw.print(prefix); pw.print("translationContext: "); pw.println(mTranslationContext); |
| } |
| |
| /** |
| * Requests a translation for the provided {@link TranslationRequest} using the Translator's |
| * source spec and destination spec. |
| * |
| * @param request {@link TranslationRequest} request to be translate. |
| * |
| * @throws IllegalStateException if this Translator session was destroyed when called. |
| * |
| * @removed use {@link #translate(TranslationRequest, CancellationSignal, |
| * Executor, Consumer)} instead. |
| */ |
| @Deprecated |
| @Nullable |
| public void translate(@NonNull TranslationRequest request, |
| @NonNull @CallbackExecutor Executor executor, |
| @NonNull Consumer<TranslationResponse> callback) { |
| Objects.requireNonNull(request, "Translation request cannot be null"); |
| Objects.requireNonNull(executor, "Executor cannot be null"); |
| Objects.requireNonNull(callback, "Callback cannot be null"); |
| |
| if (isDestroyed()) { |
| // TODO(b/176464808): Disallow multiple Translator now, it will throw |
| // IllegalStateException. Need to discuss if we can allow multiple Translators. |
| throw new IllegalStateException( |
| "This translator has been destroyed"); |
| } |
| |
| final ITranslationCallback responseCallback = |
| new TranslationResponseCallbackImpl(callback, executor); |
| try { |
| mDirectServiceBinder.onTranslationRequest(request, mId, |
| CancellationSignal.createTransport(), responseCallback); |
| } catch (RemoteException e) { |
| Log.w(TAG, "RemoteException calling requestTranslate(): " + e); |
| } |
| } |
| |
| /** |
| * Requests a translation for the provided {@link TranslationRequest} using the Translator's |
| * source spec and destination spec. |
| * |
| * @param request {@link TranslationRequest} request to be translate. |
| * @param cancellationSignal signal to cancel the operation in progress. |
| * @param executor Executor to run callback operations |
| * @param callback {@link Consumer} to receive the translation response. Multiple responses may |
| * be received if {@link TranslationRequest#FLAG_PARTIAL_RESPONSES} is set. |
| * |
| * @throws IllegalStateException if this Translator session was destroyed when called. |
| */ |
| @Nullable |
| public void translate(@NonNull TranslationRequest request, |
| @Nullable CancellationSignal cancellationSignal, |
| @NonNull @CallbackExecutor Executor executor, |
| @NonNull Consumer<TranslationResponse> callback) { |
| Objects.requireNonNull(request, "Translation request cannot be null"); |
| Objects.requireNonNull(executor, "Executor cannot be null"); |
| Objects.requireNonNull(callback, "Callback cannot be null"); |
| |
| if (isDestroyed()) { |
| // TODO(b/176464808): Disallow multiple Translator now, it will throw |
| // IllegalStateException. Need to discuss if we can allow multiple Translators. |
| throw new IllegalStateException( |
| "This translator has been destroyed"); |
| } |
| |
| ICancellationSignal transport = null; |
| if (cancellationSignal != null) { |
| transport = CancellationSignal.createTransport(); |
| cancellationSignal.setRemote(transport); |
| } |
| final ITranslationCallback responseCallback = |
| new TranslationResponseCallbackImpl(callback, executor); |
| |
| try { |
| mDirectServiceBinder.onTranslationRequest(request, mId, transport, |
| responseCallback); |
| } catch (RemoteException e) { |
| Log.w(TAG, "RemoteException calling requestTranslate(): " + e); |
| } |
| } |
| |
| /** |
| * Destroy this Translator. |
| */ |
| public void destroy() { |
| synchronized (mLock) { |
| if (mDestroyed) { |
| return; |
| } |
| mDestroyed = true; |
| try { |
| mDirectServiceBinder.onFinishTranslationSession(mId); |
| } catch (RemoteException e) { |
| Log.w(TAG, "RemoteException calling onSessionFinished"); |
| } |
| mDirectServiceBinder = null; |
| mManager.removeTranslator(mId); |
| } |
| } |
| |
| /** |
| * Returns whether or not this Translator has been destroyed. |
| * |
| * @see #destroy() |
| */ |
| public boolean isDestroyed() { |
| synchronized (mLock) { |
| return mDestroyed; |
| } |
| } |
| |
| // TODO: add methods for UI-toolkit case. |
| /** @hide */ |
| public void requestUiTranslate(@NonNull TranslationRequest request, |
| @NonNull Executor executor, |
| @NonNull Consumer<TranslationResponse> callback) { |
| if (mDirectServiceBinder == null) { |
| Log.wtf(TAG, "Translator created without proper initialization."); |
| return; |
| } |
| final ITranslationCallback translationCallback = |
| new TranslationResponseCallbackImpl(callback, executor); |
| try { |
| mDirectServiceBinder.onTranslationRequest(request, mId, |
| CancellationSignal.createTransport(), translationCallback); |
| } catch (RemoteException e) { |
| Log.w(TAG, "RemoteException calling flushRequest"); |
| } |
| } |
| |
| private static class TranslationResponseCallbackImpl extends ITranslationCallback.Stub { |
| |
| private final Consumer<TranslationResponse> mCallback; |
| private final Executor mExecutor; |
| |
| TranslationResponseCallbackImpl(Consumer<TranslationResponse> callback, Executor executor) { |
| mCallback = callback; |
| mExecutor = executor; |
| } |
| |
| @Override |
| public void onTranslationResponse(TranslationResponse response) throws RemoteException { |
| if (DEBUG) { |
| Log.i(TAG, "onTranslationResponse called."); |
| } |
| final Runnable runnable = |
| () -> mCallback.accept(response); |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| mExecutor.execute(runnable); |
| } finally { |
| restoreCallingIdentity(token); |
| } |
| } |
| } |
| } |