/* * 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 android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemService; import android.annotation.WorkerThread; import android.app.PendingIntent; import android.content.Context; import android.content.pm.ParceledListSlice; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IRemoteCallback; import android.os.Looper; import android.os.RemoteException; import android.os.SynchronousResultReceiver; import android.util.ArrayMap; import android.util.ArraySet; import android.util.IntArray; import android.util.Log; import android.util.Pair; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.SyncResultReceiver; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; /** * The {@link TranslationManager} class provides ways for apps to integrate and use the * translation framework. * *

The TranslationManager manages {@link Translator}s and help bridge client calls to * the server translation service

*/ @SystemService(Context.TRANSLATION_MANAGER_SERVICE) public final class TranslationManager { private static final String TAG = "TranslationManager"; /** * Timeout for calls to system_server, default 1 minute. */ static final int SYNC_CALLS_TIMEOUT_MS = 60_000; /** * The result code from result receiver success. * @hide */ public static final int STATUS_SYNC_CALL_SUCCESS = 1; /** * The result code from result receiver fail. * @hide */ public static final int STATUS_SYNC_CALL_FAIL = 2; /** * Name of the extra used to pass the translation capabilities. * @hide */ public static final String EXTRA_CAPABILITIES = "translation_capabilities"; @GuardedBy("mLock") private final ArrayMap, ArrayList> mTranslationCapabilityUpdateListeners = new ArrayMap<>(); @GuardedBy("mLock") private final Map, IRemoteCallback> mCapabilityCallbacks = new ArrayMap<>(); // TODO(b/158778794): make the session ids truly globally unique across processes private static final SecureRandom ID_GENERATOR = new SecureRandom(); private final Object mLock = new Object(); @NonNull private final Context mContext; private final ITranslationManager mService; @NonNull @GuardedBy("mLock") private final IntArray mTranslatorIds = new IntArray(); @NonNull private final Handler mHandler; private static final AtomicInteger sAvailableRequestId = new AtomicInteger(1); /** * @hide */ public TranslationManager(@NonNull Context context, ITranslationManager service) { mContext = Objects.requireNonNull(context, "context cannot be null"); mService = service; mHandler = Handler.createAsync(Looper.getMainLooper()); } /** * Creates an on-device Translator for natural language translation. * *

In Android 12, this method provided the same cached Translator object when given the * same TranslationContext object. Do not use a Translator destroyed elsewhere as this will * cause an exception on Android 12. * *

In later versions, this method never returns a cached Translator. * * @param translationContext {@link TranslationContext} containing the specs for creating the * Translator. * @param executor Executor to run callback operations * @param callback {@link Consumer} to receive the translator. A {@code null} value is returned * if the service could not create the translator. */ public void createOnDeviceTranslator(@NonNull TranslationContext translationContext, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer callback) { Objects.requireNonNull(translationContext, "translationContext cannot be null"); Objects.requireNonNull(executor, "executor cannot be null"); Objects.requireNonNull(callback, "callback cannot be null"); synchronized (mLock) { int translatorId; do { translatorId = Math.abs(ID_GENERATOR.nextInt()); } while (translatorId == 0 || mTranslatorIds.indexOf(translatorId) >= 0); final int tId = translatorId; new Translator(mContext, translationContext, tId, this, mHandler, mService, translator -> { if (translator == null) { Binder.withCleanCallingIdentity( () -> executor.execute(() -> callback.accept(null))); return; } synchronized (mLock) { mTranslatorIds.add(tId); } Binder.withCleanCallingIdentity( () -> executor.execute(() -> callback.accept(translator))); }); } } /** * Creates an on-device Translator for natural language translation. * *

NOTE: Call on a worker thread. * * @removed use {@link #createOnDeviceTranslator(TranslationContext, Executor, Consumer)} * instead. * * @param translationContext {@link TranslationContext} containing the specs for creating the * Translator. */ @Deprecated @Nullable @WorkerThread public Translator createOnDeviceTranslator(@NonNull TranslationContext translationContext) { Objects.requireNonNull(translationContext, "translationContext cannot be null"); synchronized (mLock) { int translatorId; do { translatorId = Math.abs(ID_GENERATOR.nextInt()); } while (translatorId == 0 || mTranslatorIds.indexOf(translatorId) >= 0); final Translator newTranslator = new Translator(mContext, translationContext, translatorId, this, mHandler, mService); // Start the Translator session and wait for the result newTranslator.start(); try { if (!newTranslator.isSessionCreated()) { return null; } mTranslatorIds.add(translatorId); return newTranslator; } catch (Translator.ServiceBinderReceiver.TimeoutException e) { // TODO(b/176464808): maybe make SyncResultReceiver.TimeoutException constructor // public and use it. Log.e(TAG, "Timed out getting create session: " + e); return null; } } } /** @removed Use {@link #createOnDeviceTranslator(TranslationContext)} */ @Deprecated @Nullable @WorkerThread public Translator createTranslator(@NonNull TranslationContext translationContext) { return createOnDeviceTranslator(translationContext); } /** * Returns a set of {@link TranslationCapability}s describing the capabilities for on-device * {@link Translator}s. * *

These translation capabilities contains a source and target {@link TranslationSpec} * representing the data expected for both ends of translation process. The capabilities * provides the information and limitations for generating a {@link TranslationContext}. * The context object can then be used by * {@link #createOnDeviceTranslator(TranslationContext, Executor, Consumer)} to obtain a * {@link Translator} for translations.

* *

NOTE: Call on a worker thread. * * @param sourceFormat data format for the input data to be translated. * @param targetFormat data format for the expected translated output data. * @return A set of {@link TranslationCapability}s. */ @NonNull @WorkerThread public Set getOnDeviceTranslationCapabilities( @TranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat) { try { final SynchronousResultReceiver receiver = new SynchronousResultReceiver(); mService.onTranslationCapabilitiesRequest(sourceFormat, targetFormat, receiver, mContext.getUserId()); final SynchronousResultReceiver.Result result = receiver.awaitResult(SYNC_CALLS_TIMEOUT_MS); if (result.resultCode != STATUS_SYNC_CALL_SUCCESS) { return Collections.emptySet(); } ParceledListSlice listSlice = result.bundle.getParcelable(EXTRA_CAPABILITIES, android.content.pm.ParceledListSlice.class); ArraySet capabilities = new ArraySet<>(listSlice == null ? null : listSlice.getList()); return capabilities; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (TimeoutException e) { Log.e(TAG, "Timed out getting supported translation capabilities: " + e); return Collections.emptySet(); } } /** @removed Use {@link #getOnDeviceTranslationCapabilities(int, int)} */ @Deprecated @NonNull @WorkerThread public Set getTranslationCapabilities( @TranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat) { return getOnDeviceTranslationCapabilities(sourceFormat, targetFormat); } /** * Adds a {@link TranslationCapability} Consumer to listen for updates on states of on-device * {@link TranslationCapability}s. * * @param capabilityListener a {@link TranslationCapability} Consumer to receive the updated * {@link TranslationCapability} from the on-device translation service. */ public void addOnDeviceTranslationCapabilityUpdateListener( @NonNull @CallbackExecutor Executor executor, @NonNull Consumer capabilityListener) { Objects.requireNonNull(executor, "executor should not be null"); Objects.requireNonNull(capabilityListener, "capability listener should not be null"); synchronized (mLock) { if (mCapabilityCallbacks.containsKey(capabilityListener)) { Log.w(TAG, "addOnDeviceTranslationCapabilityUpdateListener: the listener for " + capabilityListener + " already registered; ignoring."); return; } final IRemoteCallback remoteCallback = new TranslationCapabilityRemoteCallback(executor, capabilityListener); try { mService.registerTranslationCapabilityCallback(remoteCallback, mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } mCapabilityCallbacks.put(capabilityListener, remoteCallback); } } /** * @removed Use {@link TranslationManager#addOnDeviceTranslationCapabilityUpdateListener( * java.util.concurrent.Executor, java.util.function.Consumer)} */ @Deprecated public void addOnDeviceTranslationCapabilityUpdateListener( @TranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull PendingIntent pendingIntent) { Objects.requireNonNull(pendingIntent, "pending intent should not be null"); synchronized (mLock) { final Pair formatPair = new Pair<>(sourceFormat, targetFormat); mTranslationCapabilityUpdateListeners.computeIfAbsent(formatPair, (formats) -> new ArrayList<>()).add(pendingIntent); } } /** * @removed Use {@link TranslationManager#addOnDeviceTranslationCapabilityUpdateListener( * java.util.concurrent.Executor, java.util.function.Consumer)} */ @Deprecated public void addTranslationCapabilityUpdateListener( @TranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull PendingIntent pendingIntent) { addOnDeviceTranslationCapabilityUpdateListener(sourceFormat, targetFormat, pendingIntent); } /** * Removes a {@link TranslationCapability} Consumer to listen for updates on states of * on-device {@link TranslationCapability}s. * * @param capabilityListener the {@link TranslationCapability} Consumer to unregister */ public void removeOnDeviceTranslationCapabilityUpdateListener( @NonNull Consumer capabilityListener) { Objects.requireNonNull(capabilityListener, "capability callback should not be null"); synchronized (mLock) { final IRemoteCallback remoteCallback = mCapabilityCallbacks.get(capabilityListener); if (remoteCallback == null) { Log.w(TAG, "removeOnDeviceTranslationCapabilityUpdateListener: the capability " + "listener not found; ignoring."); return; } try { mService.unregisterTranslationCapabilityCallback(remoteCallback, mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } mCapabilityCallbacks.remove(capabilityListener); } } /** * @removed Use {@link #removeOnDeviceTranslationCapabilityUpdateListener( * java.util.function.Consumer)}. */ @Deprecated public void removeOnDeviceTranslationCapabilityUpdateListener( @TranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull PendingIntent pendingIntent) { Objects.requireNonNull(pendingIntent, "pending intent should not be null"); synchronized (mLock) { final Pair formatPair = new Pair<>(sourceFormat, targetFormat); if (mTranslationCapabilityUpdateListeners.containsKey(formatPair)) { final ArrayList intents = mTranslationCapabilityUpdateListeners.get(formatPair); if (intents.contains(pendingIntent)) { intents.remove(pendingIntent); } else { Log.w(TAG, "pending intent=" + pendingIntent + " does not exist in " + "mTranslationCapabilityUpdateListeners"); } } else { Log.w(TAG, "format pair=" + formatPair + " does not exist in " + "mTranslationCapabilityUpdateListeners"); } } } /** * @removed Use {@link #removeOnDeviceTranslationCapabilityUpdateListener( * java.util.function.Consumer)}. */ @Deprecated public void removeTranslationCapabilityUpdateListener( @TranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull PendingIntent pendingIntent) { removeOnDeviceTranslationCapabilityUpdateListener( sourceFormat, targetFormat, pendingIntent); } /** * Returns an immutable PendingIntent which can be used to launch an activity to view/edit * on-device translation settings. * * @return An immutable PendingIntent or {@code null} if one of reason met: *

    *
  • Device manufacturer (OEM) does not provide TranslationService.
  • *
  • The TranslationService doesn't provide the Settings.
  • *
**/ @Nullable public PendingIntent getOnDeviceTranslationSettingsActivityIntent() { final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); try { mService.getServiceSettingsActivity(resultReceiver, mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } try { return resultReceiver.getParcelableResult(); } catch (SyncResultReceiver.TimeoutException e) { Log.e(TAG, "Fail to get translation service settings activity."); return null; } } /** @removed Use {@link #getOnDeviceTranslationSettingsActivityIntent()} */ @Deprecated @Nullable public PendingIntent getTranslationSettingsActivityIntent() { return getOnDeviceTranslationSettingsActivityIntent(); } void removeTranslator(int id) { synchronized (mLock) { int index = mTranslatorIds.indexOf(id); if (index >= 0) { mTranslatorIds.remove(index); } } } AtomicInteger getAvailableRequestId() { synchronized (mLock) { return sAvailableRequestId; } } private static class TranslationCapabilityRemoteCallback extends IRemoteCallback.Stub { private final Executor mExecutor; private final Consumer mListener; TranslationCapabilityRemoteCallback(Executor executor, Consumer listener) { mExecutor = executor; mListener = listener; } @Override public void sendResult(Bundle bundle) { Binder.withCleanCallingIdentity( () -> mExecutor.execute(() -> onTranslationCapabilityUpdate(bundle))); } private void onTranslationCapabilityUpdate(Bundle bundle) { TranslationCapability capability = (TranslationCapability) bundle.getParcelable(EXTRA_CAPABILITIES, android.view.translation.TranslationCapability.class); mListener.accept(capability); } } }