Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 30704c1b authored by Ahaan Ugale's avatar Ahaan Ugale
Browse files

Add UiTranslationStateCallback.

This is primarily intended for Input Methods, so they can offer
complementary translation experiences to the UI Translation.

But it could also be useful to the applications being translated too, so
it's added to UiTranslationManager.

The states are intentionally different from the ones in the View API -
these are about the UI Translation feature lifecycle, whereas the View
states are about the display states (shown/hidden).

Bug: 177500482
Test: manual - with a sample IME
CTS-Coverage-Bug: 182853869
Change-Id: I8255e18c29a229c8ea3b89098b76124ee8955bdc
parent 3a88d55f
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -52387,6 +52387,17 @@ package android.view.translation {
    method @Nullable @WorkerThread public android.view.translation.TranslationResponse translate(@NonNull android.view.translation.TranslationRequest);
  }
  public final class UiTranslationManager {
    method public void registerUiTranslationStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.view.translation.UiTranslationStateCallback);
    method public void unregisterUiTranslationStateCallback(@NonNull android.view.translation.UiTranslationStateCallback);
  }
  public interface UiTranslationStateCallback {
    method public void onFinished();
    method public void onPaused();
    method public void onStarted(@NonNull String, @NonNull String);
  }
  public final class ViewTranslationRequest implements android.os.Parcelable {
    method public int describeContents();
    method @NonNull public android.view.autofill.AutofillId getAutofillId();
+18 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.util.ArrayMap;
import android.util.Slog;

import java.io.PrintWriter;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

/**
@@ -353,6 +354,23 @@ public class RemoteCallbackList<E extends IInterface> {
        }
    }

    /**
     * Performs {@code action} on each callback and associated cookie, calling {@link
     * #beginBroadcast()}/{@link #finishBroadcast()} before/after looping.
     *
     * @hide
     */
    public <C> void broadcast(BiConsumer<E, C> action) {
        int itemCount = beginBroadcast();
        try {
            for (int i = 0; i < itemCount; i++) {
                action.accept(getBroadcastItem(i), (C) getBroadcastCookie(i));
            }
        } finally {
            finishBroadcast();
        }
    }

    /**
     * Returns the number of registered callbacks. Note that the number of registered
     * callbacks may differ from the value returned by {@link #beginBroadcast()} since
+4 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package android.view.translation;

import android.os.IBinder;
import android.os.IRemoteCallback;
import android.view.autofill.AutofillId;
import android.view.translation.TranslationSpec;
import com.android.internal.os.IResultReceiver;
@@ -40,4 +41,7 @@ oneway interface ITranslationManager {
    void updateUiTranslationStateByTaskId(int state, in TranslationSpec sourceSpec,
         in TranslationSpec destSpec, in List<AutofillId> viewIds, int taskId,
         int userId);

    void registerUiTranslationStateCallback(in IRemoteCallback callback, int userId);
    void unregisterUiTranslationStateCallback(in IRemoteCallback callback, int userId);
}
+143 −3
Original line number Diff line number Diff line
@@ -16,28 +16,36 @@

package android.view.translation;

import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.app.assist.ActivityId;
import android.content.Context;
import android.os.Binder;
import android.os.Bundle;
import android.os.IRemoteCallback;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Log;
import android.view.View;
import android.view.autofill.AutofillId;

import com.android.internal.annotations.GuardedBy;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;

// TODO(b/178044703): Describe what UI Translation is.
/**
 * The {@link UiTranslationManager} class provides ways for apps to use the ui translation
 * function in framework.
 *
 * @hide
 */
@SystemApi
public final class UiTranslationManager {

    private static final String TAG = "UiTranslationManager";
@@ -88,6 +96,14 @@ public final class UiTranslationManager {
    public @interface UiTranslationState {
    }

    // Keys for the data transmitted in the internal UI Translation state callback.
    /** @hide */
    public static final String EXTRA_STATE = "state";
    /** @hide */
    public static final String EXTRA_SOURCE_LOCALE = "source_locale";
    /** @hide */
    public static final String EXTRA_TARGET_LOCALE = "target_locale";

    @NonNull
    private final Context mContext;

@@ -111,9 +127,12 @@ public final class UiTranslationManager {
     * @param destSpec {@link TranslationSpec} for the translated data.
     * @param viewIds A list of the {@link View}'s {@link AutofillId} which needs to be translated
     * @param taskId the Activity Task id which needs ui translation
     *
     * @hide
     */
    // TODO, hide the APIs
    @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
    @SystemApi
    public void startTranslation(@NonNull TranslationSpec sourceSpec,
            @NonNull TranslationSpec destSpec, @NonNull List<AutofillId> viewIds,
            int taskId) {
@@ -141,8 +160,11 @@ public final class UiTranslationManager {
     * @throws IllegalArgumentException if the no {@link View}'s {@link AutofillId} in the list
     * @throws NullPointerException the sourceSpec, destSpec, viewIds, activityId or
     *         {@link android.app.assist.ActivityId#getToken()} is {@code null}
     *
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
    @SystemApi
    public void startTranslation(@NonNull TranslationSpec sourceSpec,
            @NonNull TranslationSpec destSpec, @NonNull List<AutofillId> viewIds,
            @NonNull ActivityId activityId) {
@@ -171,9 +193,12 @@ public final class UiTranslationManager {
     * NOTE: Please use {@code finishTranslation(ActivityId)} instead.
     *
     * @param taskId the Activity Task id which needs ui translation
     *
     * @hide
     */
    // TODO, hide the APIs
    @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
    @SystemApi
    public void finishTranslation(int taskId) {
        try {
            mService.updateUiTranslationStateByTaskId(STATE_UI_TRANSLATION_FINISHED,
@@ -191,8 +216,11 @@ public final class UiTranslationManager {
     * @param activityId the identifier for the Activity which needs ui translation
     * @throws NullPointerException the activityId or
     *         {@link android.app.assist.ActivityId#getToken()} is {@code null}
     *
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
    @SystemApi
    public void finishTranslation(@NonNull ActivityId activityId) {
        try {
            Objects.requireNonNull(activityId);
@@ -212,9 +240,12 @@ public final class UiTranslationManager {
     * NOTE: Please use {@code pauseTranslation(ActivityId)} instead.
     *
     * @param taskId the Activity Task id which needs ui translation
     *
     * @hide
     */
    // TODO, hide the APIs
    @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
    @SystemApi
    public void pauseTranslation(int taskId) {
        try {
            mService.updateUiTranslationStateByTaskId(STATE_UI_TRANSLATION_PAUSED,
@@ -232,8 +263,11 @@ public final class UiTranslationManager {
     * @param activityId the identifier for the Activity which needs ui translation
     * @throws NullPointerException the activityId or
     *         {@link android.app.assist.ActivityId#getToken()} is {@code null}
     *
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
    @SystemApi
    public void pauseTranslation(@NonNull ActivityId activityId) {
        try {
            Objects.requireNonNull(activityId);
@@ -253,9 +287,12 @@ public final class UiTranslationManager {
     * NOTE: Please use {@code resumeTranslation(ActivityId)} instead.
     *
     * @param taskId the Activity Task id which needs ui translation
     *
     * @hide
     */
    // TODO, hide the APIs
    @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
    @SystemApi
    public void resumeTranslation(int taskId) {
        try {
            mService.updateUiTranslationStateByTaskId(STATE_UI_TRANSLATION_RESUMED,
@@ -273,8 +310,11 @@ public final class UiTranslationManager {
     * @param activityId the identifier for the Activity which needs ui translation
     * @throws NullPointerException the activityId or
     *         {@link android.app.assist.ActivityId#getToken()} is {@code null}
     *
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
    @SystemApi
    public void resumeTranslation(@NonNull ActivityId activityId) {
        try {
            Objects.requireNonNull(activityId);
@@ -286,4 +326,104 @@ public final class UiTranslationManager {
            throw e.rethrowFromSystemServer();
        }
    }

    // TODO(b/178044703): Fix the View API link when it becomes public.
    /**
     * Register for notifications of UI Translation state changes on the foreground activity. This
     * is available to the owning application itself and also the current input method.
     * <p>
     * The application whose UI is being translated can use this to customize the UI Translation
     * behavior in ways that aren't made easy by methods like
     * View#onCreateTranslationRequest().
     * <p>
     * Input methods can use this to offer complementary features to UI Translation; for example,
     * enabling outgoing message translation when the system is translating incoming messages in a
     * communication app.
     *
     * @param callback the callback to register for receiving the state change
     *         notifications
     */
    public void registerUiTranslationStateCallback(
            @NonNull @CallbackExecutor Executor executor,
            @NonNull UiTranslationStateCallback callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);
        synchronized (mCallbacks) {
            if (mCallbacks.containsKey(callback)) {
                Log.w(TAG, "registerUiTranslationStateCallback: callback already registered;"
                        + " ignoring.");
                return;
            }
            final IRemoteCallback remoteCallback =
                    new UiTranslationStateRemoteCallback(executor, callback);
            try {
                mService.registerUiTranslationStateCallback(remoteCallback, mContext.getUserId());
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
            mCallbacks.put(callback, remoteCallback);
        }
    }

    /**
     * Unregister {@code callback}.
     *
     * @see #registerUiTranslationStateCallback(Executor, UiTranslationStateCallback)
     */
    public void unregisterUiTranslationStateCallback(@NonNull UiTranslationStateCallback callback) {
        Objects.requireNonNull(callback);

        synchronized (mCallbacks) {
            final IRemoteCallback remoteCallback = mCallbacks.get(callback);
            if (remoteCallback == null) {
                Log.w(TAG, "unregisterUiTranslationStateCallback: callback not found; ignoring.");
                return;
            }
            try {
                mService.unregisterUiTranslationStateCallback(remoteCallback, mContext.getUserId());
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
            mCallbacks.remove(callback);
        }
    }

    @NonNull
    @GuardedBy("mCallbacks")
    private final Map<UiTranslationStateCallback, IRemoteCallback> mCallbacks = new ArrayMap<>();

    private static class UiTranslationStateRemoteCallback extends IRemoteCallback.Stub {
        private final Executor mExecutor;
        private final UiTranslationStateCallback mCallback;

        UiTranslationStateRemoteCallback(Executor executor,
                UiTranslationStateCallback callback) {
            mExecutor = executor;
            mCallback = callback;
        }

        @Override
        public void sendResult(Bundle bundle) {
            Binder.clearCallingIdentity();
            mExecutor.execute(() -> {
                int state = bundle.getInt(EXTRA_STATE);
                switch (state) {
                    case STATE_UI_TRANSLATION_STARTED:
                    case STATE_UI_TRANSLATION_RESUMED:
                        mCallback.onStarted(
                                bundle.getString(EXTRA_SOURCE_LOCALE),
                                bundle.getString(EXTRA_TARGET_LOCALE));
                        break;
                    case STATE_UI_TRANSLATION_PAUSED:
                        mCallback.onPaused();
                        break;
                    case STATE_UI_TRANSLATION_FINISHED:
                        mCallback.onFinished();
                        break;
                    default:
                        Log.wtf(TAG, "Unexpected translation state:" + state);
                }
            });
        }
    }
}
+48 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.NonNull;

import java.util.concurrent.Executor;

/**
 * Callback for listening to UI Translation state changes. See {@link
 * UiTranslationManager#registerUiTranslationStateCallback(Executor, UiTranslationStateCallback)}.
 */
public interface UiTranslationStateCallback {

    /**
     * The system is requesting translation of the UI from {@code sourceLocale} to {@code
     * targetLocale}.
     * <p>
     * This is also called if either the requested {@code sourceLocale} or {@code targetLocale} has
     * changed; or called again after {@link #onPaused()}.
     */
    void onStarted(@NonNull String sourceLocale, @NonNull String targetLocale);

    /**
     * The system is requesting that the application temporarily show the UI contents in their
     * original language.
     */
    void onPaused();

    /**
     * The UI Translation session has ended.
     */
    void onFinished();
}
Loading