| /* |
| * 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.translation.cts; |
| |
| import static android.content.Context.CONTENT_CAPTURE_MANAGER_SERVICE; |
| import static android.content.Context.TRANSLATION_MANAGER_SERVICE; |
| import static android.provider.Settings.Global.ANIMATOR_DURATION_SCALE; |
| import static android.translation.cts.Helper.ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_FINISH; |
| import static android.translation.cts.Helper.ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_PAUSE; |
| import static android.translation.cts.Helper.ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_RESUME; |
| import static android.translation.cts.Helper.ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_START; |
| import static android.translation.cts.Helper.ACTION_REGISTER_UI_TRANSLATION_CALLBACK; |
| import static android.translation.cts.Helper.ACTION_UNREGISTER_UI_TRANSLATION_CALLBACK; |
| import static android.translation.cts.Helper.EXTRA_FINISH_COMMAND; |
| import static android.translation.cts.Helper.EXTRA_SOURCE_LOCALE; |
| import static android.translation.cts.Helper.EXTRA_TARGET_LOCALE; |
| import static android.translation.cts.Helper.EXTRA_VERIFY_RESULT; |
| import static android.view.translation.TranslationResponseValue.STATUS_SUCCESS; |
| |
| import static com.android.compatibility.common.util.ShellUtils.runShellCommand; |
| import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.mockito.ArgumentMatchers.any; |
| |
| import android.app.PendingIntent; |
| import android.content.ComponentName; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.icu.util.ULocale; |
| import android.os.SystemClock; |
| import android.platform.test.annotations.AppModeFull; |
| import android.provider.Settings; |
| import android.service.contentcapture.ContentCaptureService; |
| import android.service.translation.TranslationService; |
| import android.translation.cts.views.CustomTextView; |
| import android.translation.cts.views.ResponseNotSetTextView; |
| import android.translation.cts.views.VirtualContainerView; |
| import android.util.Log; |
| import android.util.LongSparseArray; |
| import android.util.Pair; |
| import android.view.View; |
| import android.view.autofill.AutofillId; |
| import android.view.contentcapture.ContentCaptureContext; |
| import android.view.inputmethod.InputMethodManager; |
| import android.view.translation.TranslationRequest; |
| import android.view.translation.TranslationResponse; |
| import android.view.translation.TranslationResponseValue; |
| import android.view.translation.TranslationSpec; |
| import android.view.translation.UiTranslationManager; |
| import android.view.translation.UiTranslationSpec; |
| import android.view.translation.UiTranslationStateCallback; |
| import android.view.translation.ViewTranslationCallback; |
| import android.view.translation.ViewTranslationRequest; |
| import android.view.translation.ViewTranslationResponse; |
| import android.widget.TextView; |
| |
| import androidx.lifecycle.Lifecycle; |
| import androidx.test.core.app.ActivityScenario; |
| import androidx.test.core.app.ApplicationProvider; |
| import androidx.test.filters.FlakyTest; |
| import androidx.test.runner.AndroidJUnit4; |
| import androidx.test.uiautomator.UiObject2; |
| |
| import com.android.compatibility.common.util.BlockingBroadcastReceiver; |
| import com.android.compatibility.common.util.PollingCheck; |
| import com.android.compatibility.common.util.RequiredServiceRule; |
| import com.android.compatibility.common.util.SystemUtil; |
| |
| import org.junit.After; |
| import org.junit.AfterClass; |
| import org.junit.Before; |
| import org.junit.BeforeClass; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.Mockito; |
| |
| import java.util.List; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| |
| /** |
| * Tests for {@link UiTranslationManager} related APIs. |
| * |
| * <p> |
| * {@link UiTranslationManager} needs a token that reports by {@link ContentCaptureService}. We use |
| * a non pre-configured {@link ContentCaptureService} and a {@link TranslationService} temporary |
| * service for CTS tests that is set via shell command. The test will get the token from the |
| * {@link ContentCaptureService} then uses this token in {@link UiTranslationManager} APIs.</p> |
| */ |
| |
| @AppModeFull(reason = "TODO(b/182330968): disable instant mode. Re-enable after we decouple the " |
| + "service from the test package.") |
| @RunWith(AndroidJUnit4.class) |
| public class UiTranslationManagerTest { |
| |
| private static final String TAG = "UiTranslationManagerTest"; |
| |
| private static final long UI_WAIT_TIMEOUT = 2000; |
| |
| // TODO: Use fw definition when it becomes public or testapi |
| private static final String ID_CONTENT_DESCRIPTION = "android:content_description"; |
| |
| private static Context sContext; |
| private static CtsTranslationService.TranslationReplier sTranslationReplier; |
| |
| private CtsContentCaptureService.ServiceWatcher mContentCaptureServiceWatcher; |
| private CtsTranslationService.ServiceWatcher mTranslationServiceServiceWatcher; |
| private ActivityScenario<SimpleActivity> mActivityScenario; |
| private ActivityScenario<VirtualContainerViewActivity> mVirtualContainerViewActivityScenario; |
| private ActivityScenario<CustomTextViewActivity> mCustomTextViewActivityScenario; |
| |
| private VirtualContainerView mVirtualContainerView; |
| private ResponseNotSetTextView mResponseNotSetTextView; |
| private CustomTextView mCustomTextView; |
| private TextView mTextView; |
| private static String sOriginalLogTag; |
| |
| @Rule |
| public final RequiredServiceRule mContentCaptureServiceRule = |
| new RequiredServiceRule(CONTENT_CAPTURE_MANAGER_SERVICE); |
| |
| @Rule |
| public final RequiredServiceRule mTranslationServiceRule = |
| new RequiredServiceRule(TRANSLATION_MANAGER_SERVICE); |
| |
| @BeforeClass |
| public static void oneTimeSetup() { |
| sContext = ApplicationProvider.getApplicationContext(); |
| sTranslationReplier = CtsTranslationService.getTranslationReplier(); |
| sOriginalLogTag = Helper.enableDebugLog(); |
| |
| Helper.allowSelfForContentCapture(sContext); |
| Helper.setDefaultContentCaptureServiceEnabled(/* enabled= */ false); |
| } |
| |
| @AfterClass |
| public static void oneTimeReset() { |
| Helper.unAllowSelfForContentCapture(sContext); |
| Helper.setDefaultContentCaptureServiceEnabled(/* enabled= */ true); |
| Helper.disableDebugLog(sOriginalLogTag); |
| } |
| |
| @Before |
| public void setup() throws Exception { |
| CtsContentCaptureService.resetStaticState(); |
| CtsTranslationService.resetStaticState(); |
| } |
| |
| @After |
| public void cleanup() throws Exception { |
| if (mActivityScenario != null) { |
| mActivityScenario.moveToState(Lifecycle.State.DESTROYED); |
| } |
| if (mVirtualContainerViewActivityScenario != null) { |
| mVirtualContainerViewActivityScenario.moveToState(Lifecycle.State.DESTROYED); |
| } |
| Helper.resetTemporaryContentCaptureService(); |
| Helper.resetTemporaryTranslationService(); |
| } |
| |
| @Test |
| public void testUiTranslation() throws Throwable { |
| final Pair<List<AutofillId>, ContentCaptureContext> result = |
| enableServicesAndStartActivityForTranslation(); |
| |
| final CharSequence originalText = mTextView.getText(); |
| final List<AutofillId> views = result.first; |
| final ContentCaptureContext contentCaptureContext = result.second; |
| |
| final String translatedText = "success"; |
| final UiObject2 helloText = Helper.findObjectByResId(Helper.ACTIVITY_PACKAGE, |
| SimpleActivity.HELLO_TEXT_ID); |
| assertThat(helloText).isNotNull(); |
| // Set response |
| final TranslationResponse response = createViewsTranslationResponse(views, translatedText); |
| sTranslationReplier.addResponse(response); |
| |
| startUiTranslation(/* shouldPadContent */ false, views, contentCaptureContext); |
| |
| // Check request |
| final TranslationRequest request = sTranslationReplier.getNextTranslationRequest(); |
| final List<ViewTranslationRequest> requests = request.getViewTranslationRequests(); |
| final ViewTranslationRequest viewRequest = requests.get(0); |
| assertThat(viewRequest.getAutofillId()).isEqualTo(views.get(0)); |
| assertThat(viewRequest.getKeys().size()).isEqualTo(1); |
| assertThat(viewRequest.getKeys()).containsExactly(ViewTranslationRequest.ID_TEXT); |
| assertThat(viewRequest.getValue(ViewTranslationRequest.ID_TEXT).getText()) |
| .isEqualTo(originalText.toString()); |
| |
| assertThat(helloText.getText()).isEqualTo(translatedText); |
| assertThat(mTextView.getViewTranslationResponse()) |
| .isEqualTo(response.getViewTranslationResponses().get(0)); |
| |
| pauseUiTranslation(contentCaptureContext); |
| |
| assertThat(helloText.getText()).isEqualTo(originalText.toString()); |
| |
| resumeUiTranslation(contentCaptureContext); |
| |
| assertThat(helloText.getText()).isEqualTo(translatedText); |
| |
| finishUiTranslation(contentCaptureContext); |
| |
| assertThat(helloText.getText()).isEqualTo(originalText.toString()); |
| |
| // Check the Translation session is destroyed after calling finishTranslation() |
| CtsTranslationService translationService = |
| mTranslationServiceServiceWatcher.getService(); |
| translationService.awaitSessionDestroyed(); |
| |
| // Test re-translating. |
| sTranslationReplier.addResponse(createViewsTranslationResponse(views, translatedText)); |
| |
| startUiTranslation(/* shouldPadContent */ false, views, contentCaptureContext); |
| |
| assertThat(helloText.getText()).isEqualTo(translatedText); |
| |
| // Also make sure pausing still works. |
| pauseUiTranslation(contentCaptureContext); |
| |
| assertThat(helloText.getText()).isEqualTo(originalText.toString()); |
| } |
| |
| @Test |
| public void testUiTranslationWithoutAnimation() throws Throwable { |
| final float[] originalAnimationDurationScale = new float[1]; |
| try { |
| // Disable animation |
| SystemUtil.runWithShellPermissionIdentity(() -> { |
| ContentResolver resolver = |
| ApplicationProvider.getApplicationContext().getContentResolver(); |
| originalAnimationDurationScale[0] = |
| Settings.Global.getFloat(resolver, ANIMATOR_DURATION_SCALE, 1f); |
| Settings.Global.putFloat(resolver, ANIMATOR_DURATION_SCALE, 0); |
| }); |
| |
| final Pair<List<AutofillId>, ContentCaptureContext> result = |
| enableServicesAndStartActivityForTranslation(); |
| |
| final CharSequence originalText = mTextView.getText(); |
| final List<AutofillId> views = result.first; |
| final ContentCaptureContext contentCaptureContext = result.second; |
| |
| final String translatedText = "success"; |
| final UiObject2 helloText = Helper.findObjectByResId(Helper.ACTIVITY_PACKAGE, |
| SimpleActivity.HELLO_TEXT_ID); |
| assertThat(helloText).isNotNull(); |
| // Set response |
| final TranslationResponse response = |
| createViewsTranslationResponse(views, translatedText); |
| sTranslationReplier.addResponse(response); |
| |
| startUiTranslation(/* shouldPadContent */ false, views, contentCaptureContext); |
| |
| assertThat(helloText.getText()).isEqualTo(translatedText); |
| |
| pauseUiTranslation(contentCaptureContext); |
| |
| assertThat(helloText.getText()).isEqualTo(originalText.toString()); |
| |
| resumeUiTranslation(contentCaptureContext); |
| |
| assertThat(helloText.getText()).isEqualTo(translatedText); |
| |
| finishUiTranslation(contentCaptureContext); |
| |
| assertThat(helloText.getText()).isEqualTo(originalText.toString()); |
| } finally { |
| // restore animation |
| SystemUtil.runWithShellPermissionIdentity(() -> { |
| Settings.Global.putFloat( |
| ApplicationProvider.getApplicationContext().getContentResolver(), |
| ANIMATOR_DURATION_SCALE, originalAnimationDurationScale[0]); |
| }); |
| } |
| } |
| |
| @Test |
| public void testPauseUiTranslationThenStartUiTranslation() throws Throwable { |
| final Pair<List<AutofillId>, ContentCaptureContext> result = |
| enableServicesAndStartActivityForTranslation(); |
| |
| final CharSequence originalText = mTextView.getText(); |
| final List<AutofillId> views = result.first; |
| final ContentCaptureContext contentCaptureContext = result.second; |
| |
| final String translatedText = "success"; |
| final UiObject2 helloText = Helper.findObjectByResId(Helper.ACTIVITY_PACKAGE, |
| SimpleActivity.HELLO_TEXT_ID); |
| assertThat(helloText).isNotNull(); |
| // Set response |
| final TranslationResponse response = |
| createViewsTranslationResponse(views, translatedText); |
| sTranslationReplier.addResponse(response); |
| |
| startUiTranslation(/* shouldPadContent */ false, views, contentCaptureContext); |
| |
| assertThat(helloText.getText()).isEqualTo(translatedText); |
| |
| pauseUiTranslation(contentCaptureContext); |
| |
| assertThat(helloText.getText()).isEqualTo(originalText.toString()); |
| |
| sTranslationReplier.addResponse(createViewsTranslationResponse(views, translatedText)); |
| |
| startUiTranslation(/* shouldPadContent */ false, views, contentCaptureContext); |
| |
| assertThat(helloText.getText()).isEqualTo(translatedText); |
| } |
| |
| @Test |
| public void testUiTranslation_CustomViewTranslationCallback() throws Throwable { |
| final Pair<List<AutofillId>, ContentCaptureContext> result = |
| enableServicesAndStartActivityForTranslation(); |
| final List<AutofillId> views = result.first; |
| final ContentCaptureContext contentCaptureContext = result.second; |
| |
| // Set ViewTranslationCallback |
| ViewTranslationCallback mockCallback = Mockito.mock(ViewTranslationCallback.class); |
| mTextView.setViewTranslationCallback(mockCallback); |
| // Set response |
| sTranslationReplier.addResponse(createViewsTranslationResponse(views, "success")); |
| |
| startUiTranslation(/* shouldPadContent */ false, views, contentCaptureContext); |
| |
| ArgumentCaptor<View> viewArgumentCaptor = ArgumentCaptor.forClass(View.class); |
| Mockito.verify(mockCallback, Mockito.times(1)).onShowTranslation(viewArgumentCaptor.capture()); |
| TextView capturedView = (TextView) viewArgumentCaptor.getValue(); |
| assertThat(capturedView.getAutofillId()).isEqualTo(mTextView.getAutofillId()); |
| |
| pauseUiTranslation(contentCaptureContext); |
| |
| Mockito.verify(mockCallback, Mockito.times(1)).onHideTranslation(viewArgumentCaptor.capture()); |
| capturedView = (TextView) viewArgumentCaptor.getValue(); |
| assertThat(capturedView.getAutofillId()).isEqualTo(mTextView.getAutofillId()); |
| |
| resumeUiTranslation(contentCaptureContext); |
| |
| Mockito.verify(mockCallback, Mockito.times(2)).onShowTranslation(viewArgumentCaptor.capture()); |
| capturedView = (TextView) viewArgumentCaptor.getValue(); |
| assertThat(capturedView.getAutofillId()).isEqualTo(mTextView.getAutofillId()); |
| |
| // Clear callback |
| mTextView.clearViewTranslationCallback(); |
| |
| finishUiTranslation(contentCaptureContext); |
| |
| // Verify callback does not be called, keep the latest state |
| Mockito.verify(mockCallback, Mockito.never()).onClearTranslation(any(View.class)); |
| |
| // Finally, verify that no unexpected interactions occurred. We cannot use |
| // verifyNoMoreInteractions as the callback has some hidden methods. |
| Mockito.verify(mockCallback, Mockito.times(2)).onShowTranslation(any()); |
| Mockito.verify(mockCallback, Mockito.times(1)).onHideTranslation(any()); |
| } |
| |
| @Test |
| @FlakyTest(bugId = 192418800) |
| public void testUiTranslation_ViewTranslationCallback_paddingText() throws Throwable { |
| final Pair<List<AutofillId>, ContentCaptureContext> result = |
| enableServicesAndStartActivityForTranslation(); |
| final List<AutofillId> views = result.first; |
| final ContentCaptureContext contentCaptureContext = result.second; |
| |
| // Set response |
| final CharSequence originalText = mTextView.getText(); |
| final CharSequence translatedText = "Translated World"; |
| sTranslationReplier.addResponse( |
| createViewsTranslationResponse(views, translatedText.toString())); |
| |
| // Use TextView default ViewTranslationCallback implementation |
| startUiTranslation(/* shouldPadContent */ true, views, contentCaptureContext); |
| |
| CharSequence currentText = mTextView.getText(); |
| assertThat(currentText.length()).isNotEqualTo(originalText.length()); |
| assertThat(currentText.length()).isEqualTo(translatedText.length()); |
| |
| finishUiTranslation(contentCaptureContext); |
| |
| // Set Customized ViewTranslationCallback |
| ViewTranslationCallback mockCallback = Mockito.mock(ViewTranslationCallback.class); |
| mTextView.setViewTranslationCallback(mockCallback); |
| |
| startUiTranslation(/* shouldPadContent */ true, views, contentCaptureContext); |
| |
| assertThat(mTextView.getText().length()).isEqualTo(originalText.length()); |
| } |
| |
| @Test |
| public void testUiTranslation_hasContentDescription() throws Throwable { |
| final Pair<List<AutofillId>, ContentCaptureContext> result = |
| enableServicesAndStartActivityForTranslation(); |
| final List<AutofillId> views = result.first; |
| final ContentCaptureContext contentCaptureContext = result.second; |
| |
| // Set response |
| final CharSequence translatedText = "Translated World"; |
| final CharSequence originalDescription = "Hello Description"; |
| mActivityScenario.onActivity(activity -> { |
| mTextView.setContentDescription(originalDescription); |
| }); |
| sTranslationReplier.addResponse( |
| createViewsTranslationResponse(views, translatedText.toString())); |
| |
| // Use TextView default ViewTranslationCallback implementation |
| startUiTranslation(/* shouldPadContent */ false, views, contentCaptureContext); |
| |
| assertThat(mTextView.getContentDescription().toString()) |
| .isEqualTo(translatedText.toString()); |
| |
| // Check request to make sure the content description key doesn't be changed |
| final TranslationRequest request = sTranslationReplier.getNextTranslationRequest(); |
| final List<ViewTranslationRequest> requests = request.getViewTranslationRequests(); |
| final ViewTranslationRequest viewRequest = requests.get(0); |
| assertThat(viewRequest.getAutofillId()).isEqualTo(views.get(0)); |
| assertThat(viewRequest.getKeys().size()).isEqualTo(2); |
| assertThat(viewRequest.getKeys()).containsExactly(ID_CONTENT_DESCRIPTION, |
| ViewTranslationRequest.ID_TEXT); |
| assertThat(viewRequest.getValue(ID_CONTENT_DESCRIPTION).getText()) |
| .isEqualTo(originalDescription); |
| |
| pauseUiTranslation(contentCaptureContext); |
| |
| assertThat(mTextView.getContentDescription().toString()) |
| .isEqualTo(originalDescription.toString()); |
| |
| resumeUiTranslation(contentCaptureContext); |
| |
| assertThat(mTextView.getContentDescription().toString()) |
| .isEqualTo(translatedText.toString()); |
| |
| finishUiTranslation(contentCaptureContext); |
| |
| assertThat(mTextView.getContentDescription().toString()) |
| .isEqualTo(originalDescription.toString()); |
| } |
| |
| @Test |
| public void testIMEUiTranslationStateCallback() throws Throwable { |
| try (ImeSession imeSession = new ImeSession( |
| new ComponentName(CtsTestIme.IME_SERVICE_PACKAGE, CtsTestIme.class.getName()))) { |
| |
| final Pair<List<AutofillId>, ContentCaptureContext> result = |
| enableServicesAndStartActivityForTranslation(); |
| final List<AutofillId> views = result.first; |
| final ContentCaptureContext contentCaptureContext = result.second; |
| sTranslationReplier.addResponse(createViewsTranslationResponse(views, "success")); |
| |
| // Send broadcast to request IME to register callback |
| BlockingBroadcastReceiver registerResultReceiver = |
| sendCommandToIme(ACTION_REGISTER_UI_TRANSLATION_CALLBACK, false); |
| // Get result |
| registerResultReceiver.awaitForBroadcast(); |
| registerResultReceiver.unregisterQuietly(); |
| |
| startUiTranslation(/* shouldPadContent */ false, views, contentCaptureContext); |
| |
| // Send broadcast to request IME to check the onStarted() result |
| BlockingBroadcastReceiver onStartResultReceiver = sendCommandToIme( |
| ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_START, true); |
| // Get result to check the onStarted() was called |
| Intent onStartIntent = onStartResultReceiver.awaitForBroadcast(); |
| ULocale receivedSource = |
| (ULocale) onStartIntent.getSerializableExtra(EXTRA_SOURCE_LOCALE); |
| ULocale receivedTarget = |
| (ULocale) onStartIntent.getSerializableExtra(EXTRA_TARGET_LOCALE); |
| assertThat(receivedSource).isEqualTo(ULocale.ENGLISH); |
| assertThat(receivedTarget).isEqualTo(ULocale.FRENCH); |
| onStartResultReceiver.unregisterQuietly(); |
| |
| pauseUiTranslation(contentCaptureContext); |
| |
| // Send broadcast to request IME to check the onPaused() result |
| BlockingBroadcastReceiver onPausedResultReceiver = sendCommandToIme( |
| ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_PAUSE, true); |
| // Get result to check the onPaused() was called |
| Intent onPausedIntent = onPausedResultReceiver.awaitForBroadcast(); |
| boolean onPausedVerifyResult = |
| onPausedIntent.getBooleanExtra(EXTRA_VERIFY_RESULT, false); |
| assertThat(onPausedVerifyResult).isTrue(); |
| onPausedResultReceiver.unregisterQuietly(); |
| |
| resumeUiTranslation(contentCaptureContext); |
| |
| // Send broadcast to request IME to check the onResumed result |
| BlockingBroadcastReceiver onResumedResultReceiver = sendCommandToIme( |
| ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_RESUME, true); |
| // Get result to check the onResumed was called |
| Intent onResumedIntent = onResumedResultReceiver.awaitForBroadcast(); |
| boolean onResumedVerifyResult = |
| onResumedIntent.getBooleanExtra(EXTRA_VERIFY_RESULT, false); |
| assertThat(onResumedVerifyResult).isTrue(); |
| onResumedResultReceiver.unregisterQuietly(); |
| |
| // Send broadcast to request IME to unregister callback |
| BlockingBroadcastReceiver unRegisterResultReceiver |
| = sendCommandToIme(ACTION_UNREGISTER_UI_TRANSLATION_CALLBACK, false); |
| unRegisterResultReceiver.awaitForBroadcast(); |
| unRegisterResultReceiver.unregisterQuietly(); |
| |
| finishUiTranslation(contentCaptureContext); |
| |
| BlockingBroadcastReceiver onFinishResultReceiver = |
| sendCommandToIme(ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_FINISH, true); |
| // Get result to check onFinish() didn't be called. |
| Intent onFinishIntent = onFinishResultReceiver.awaitForBroadcast(); |
| boolean onFinishVerifyResult = |
| onFinishIntent.getBooleanExtra(EXTRA_VERIFY_RESULT, true); |
| assertThat(onFinishVerifyResult).isFalse(); |
| onFinishResultReceiver.unregisterQuietly(); |
| |
| // TODO(b/191417938): add tests for the Activity destroyed for IME package callback |
| } |
| } |
| |
| @Test |
| public void testNonIMEUiTranslationStateCallback() throws Throwable { |
| final Pair<List<AutofillId>, ContentCaptureContext> result = |
| enableServicesAndStartActivityForTranslation(); |
| |
| final List<AutofillId> views = result.first; |
| final ContentCaptureContext contentCaptureContext = result.second; |
| |
| UiTranslationManager manager = |
| sContext.getSystemService(UiTranslationManager.class); |
| // Set response |
| sTranslationReplier.addResponse(createViewsTranslationResponse(views, "success")); |
| |
| // Register callback |
| final Executor executor = Executors.newSingleThreadExecutor(); |
| UiTranslationStateCallback mockCallback = Mockito.mock(UiTranslationStateCallback.class); |
| manager.registerUiTranslationStateCallback(executor, mockCallback); |
| |
| startUiTranslation(/* shouldPadContent */ false, views, contentCaptureContext); |
| |
| // TODO(b/191417938): add tests for the Activity isn't the same package of the |
| // registered callback app |
| Mockito.verify(mockCallback, Mockito.times(1)) |
| .onStarted(any(ULocale.class), any(ULocale.class)); |
| |
| finishUiTranslation(contentCaptureContext); |
| |
| Mockito.verify(mockCallback, Mockito.times(1)) |
| .onFinished(); |
| |
| // Make sure onFinished will not be called twice. |
| mActivityScenario.moveToState(Lifecycle.State.DESTROYED); |
| mActivityScenario = null; |
| Mockito.verify(mockCallback, Mockito.times(1)) |
| .onFinished(); |
| |
| // TODO(b/191417938): add a test to verify startUiTranslation + Activity destroyed. |
| } |
| |
| @Test |
| public void testVirtualViewUiTranslation() throws Throwable { |
| // Enable CTS ContentCaptureService |
| CtsContentCaptureService contentcaptureService = enableContentCaptureService(); |
| |
| // Start Activity and get needed information |
| final List<AutofillId> views = startVirtualContainerViewActivityAndGetViewsForTranslation(); |
| ViewTranslationCallback mockCallback = Mockito.mock(ViewTranslationCallback.class); |
| mVirtualContainerView.setViewTranslationCallback(mockCallback); |
| |
| // Wait session created and get the ContentCaptureContext from ContentCaptureService |
| final ContentCaptureContext contentCaptureContext = |
| getContentCaptureContextFromContentCaptureService(contentcaptureService); |
| |
| // enable CTS TranslationService |
| mTranslationServiceServiceWatcher = CtsTranslationService.setServiceWatcher(); |
| Helper.setTemporaryTranslationService(CtsTranslationService.SERVICE_NAME); |
| |
| final String translatedText = "success"; |
| final UiTranslationManager manager = sContext.getSystemService(UiTranslationManager.class); |
| // Set response |
| final TranslationResponse expectedResponse = |
| createViewsTranslationResponse(views, translatedText); |
| sTranslationReplier.addResponse(expectedResponse); |
| |
| startUiTranslation(/* shouldPadContent */ false, views, contentCaptureContext); |
| |
| // Check request |
| final TranslationRequest request = sTranslationReplier.getNextTranslationRequest(); |
| final List<ViewTranslationRequest> requests = request.getViewTranslationRequests(); |
| assertThat(requests.size()).isEqualTo(views.size()); |
| // 1st virtual child in container |
| final ViewTranslationRequest viewRequest1 = requests.get(0); |
| assertThat(viewRequest1.getAutofillId()).isEqualTo(views.get(0)); |
| assertThat(viewRequest1.getKeys()).containsExactly(ViewTranslationRequest.ID_TEXT); |
| assertThat(viewRequest1.getValue(ViewTranslationRequest.ID_TEXT).getText().toString()) |
| .isEqualTo("Hello 0"); |
| // 2nd virtual child in container |
| final ViewTranslationRequest viewRequest2 = requests.get(1); |
| assertThat(viewRequest2.getAutofillId()).isEqualTo(views.get(1)); |
| assertThat(viewRequest2.getKeys()).containsExactly(ViewTranslationRequest.ID_TEXT); |
| assertThat(viewRequest2.getValue(ViewTranslationRequest.ID_TEXT).getText().toString()) |
| .isEqualTo("Hello 1"); |
| |
| // Check responses |
| final LongSparseArray<ViewTranslationResponse> responses |
| = mVirtualContainerView.getViewTranslationResponseForCustomView(); |
| assertThat(responses).isNotNull(); |
| assertThat(responses.size()).isEqualTo(2); |
| assertThat(responses.valueAt(0)) |
| .isEqualTo(expectedResponse.getViewTranslationResponses().valueAt(0)); |
| assertThat(responses.valueAt(1)) |
| .isEqualTo(expectedResponse.getViewTranslationResponses().valueAt(1)); |
| |
| ArgumentCaptor<View> viewArgumentCaptor = ArgumentCaptor.forClass(View.class); |
| Mockito.verify(mockCallback, Mockito.times(1)) |
| .onShowTranslation(viewArgumentCaptor.capture()); |
| VirtualContainerView capturedView = (VirtualContainerView) viewArgumentCaptor.getValue(); |
| assertThat(capturedView.getAutofillId()).isEqualTo(mVirtualContainerView.getAutofillId()); |
| |
| pauseUiTranslation(contentCaptureContext); |
| |
| Mockito.verify(mockCallback, Mockito.times(1)).onHideTranslation(any(View.class)); |
| |
| finishUiTranslation(contentCaptureContext); |
| |
| Mockito.verify(mockCallback, Mockito.times(1)).onClearTranslation(any(View.class)); |
| } |
| |
| @Test |
| public void testUiTranslation_translationResponseNotSetForCustomTextView() throws Throwable { |
| // Enable CTS ContentCaptureService |
| CtsContentCaptureService contentcaptureService = enableContentCaptureService(); |
| // Start Activity and get needed information |
| final List<AutofillId> views = startCustomTextViewActivityAndGetViewsForTranslation(); |
| |
| // Wait session created and get the ConttCaptureContext from ContentCaptureService |
| final ContentCaptureContext contentCaptureContext = |
| getContentCaptureContextFromContentCaptureService(contentcaptureService); |
| |
| // enable CTS TranslationService |
| mTranslationServiceServiceWatcher = CtsTranslationService.setServiceWatcher(); |
| Helper.setTemporaryTranslationService(CtsTranslationService.SERVICE_NAME); |
| |
| // Set response |
| final TranslationResponse expectedResponse = |
| createViewsTranslationResponse(views, "success"); |
| sTranslationReplier.addResponse(expectedResponse); |
| |
| startUiTranslation(/* shouldPadContent */ false, views, contentCaptureContext); |
| |
| // Verify result. Translation response doesn't set, it should show original text |
| assertThat(mResponseNotSetTextView.getSavedResponse()).isNotNull(); |
| final UiObject2 responseNotSetText = Helper.findObjectByResId(Helper.ACTIVITY_PACKAGE, |
| CustomTextViewActivity.ID_RESPONSE_NOT_SET_TEXT); |
| assertThat(responseNotSetText).isNotNull(); |
| assertThat(responseNotSetText.getText()).isEqualTo("Hello World 1"); |
| } |
| |
| @Test |
| @FlakyTest(bugId = 192418800) |
| public void testUiTranslation_customTextView() throws Throwable { |
| // Enable CTS ContentCaptureService |
| CtsContentCaptureService contentcaptureService = enableContentCaptureService(); |
| // Start Activity and get needed information |
| final List<AutofillId> views = startCustomTextViewActivityAndGetViewsForTranslation(); |
| |
| // Wait session created and get the ConttCaptureContext from ContentCaptureService |
| final ContentCaptureContext contentCaptureContext = |
| getContentCaptureContextFromContentCaptureService(contentcaptureService); |
| |
| // enable CTS TranslationService |
| mTranslationServiceServiceWatcher = CtsTranslationService.setServiceWatcher(); |
| Helper.setTemporaryTranslationService(CtsTranslationService.SERVICE_NAME); |
| |
| final String translatedText = "success"; |
| // Set response |
| final TranslationResponse expectedResponse = |
| createViewsTranslationResponse(views, translatedText); |
| sTranslationReplier.addResponse(expectedResponse); |
| |
| startUiTranslation(/* shouldPadContent */ false, views, contentCaptureContext); |
| |
| // Verify result. |
| assertThat(mCustomTextView.isMyTagTranslationSupported()).isTrue(); |
| final UiObject2 customText = Helper.findObjectByResId(Helper.ACTIVITY_PACKAGE, |
| CustomTextViewActivity.ID_CUSTOM_TEXT); |
| assertThat(customText).isNotNull(); |
| assertThat(customText.getText()).isEqualTo(translatedText); |
| |
| finishUiTranslation(contentCaptureContext); |
| |
| assertThat(customText.getText()).isEqualTo("Hello World 2"); |
| } |
| |
| private void startUiTranslation(boolean shouldPadContent, List<AutofillId> views, |
| ContentCaptureContext contentCaptureContext) { |
| final UiTranslationManager manager = sContext.getSystemService(UiTranslationManager.class); |
| runWithShellPermissionIdentity(() -> { |
| // Call startTranslation API |
| manager.startTranslation( |
| new TranslationSpec(ULocale.ENGLISH, |
| TranslationSpec.DATA_FORMAT_TEXT), |
| new TranslationSpec(ULocale.FRENCH, |
| TranslationSpec.DATA_FORMAT_TEXT), |
| views, contentCaptureContext.getActivityId(), |
| shouldPadContent ? new UiTranslationSpec.Builder().setShouldPadContentForCompat( |
| true).build() : new UiTranslationSpec.Builder().build()); |
| SystemClock.sleep(UI_WAIT_TIMEOUT); |
| }); |
| } |
| |
| private void pauseUiTranslation(ContentCaptureContext contentCaptureContext) { |
| final UiTranslationManager manager = sContext.getSystemService(UiTranslationManager.class); |
| runWithShellPermissionIdentity(() -> { |
| // Call pauseTranslation API |
| manager.pauseTranslation(contentCaptureContext.getActivityId()); |
| SystemClock.sleep(UI_WAIT_TIMEOUT); |
| }); |
| } |
| |
| private void resumeUiTranslation(ContentCaptureContext contentCaptureContext) { |
| final UiTranslationManager manager = sContext.getSystemService(UiTranslationManager.class); |
| // Call resume Translation API |
| runWithShellPermissionIdentity(() -> { |
| manager.resumeTranslation(contentCaptureContext.getActivityId()); |
| SystemClock.sleep(UI_WAIT_TIMEOUT); |
| }); |
| } |
| |
| private void finishUiTranslation(ContentCaptureContext contentCaptureContext) { |
| final UiTranslationManager manager = sContext.getSystemService(UiTranslationManager.class); |
| runWithShellPermissionIdentity(() -> { |
| // Call finishTranslation API |
| manager.finishTranslation(contentCaptureContext.getActivityId()); |
| SystemClock.sleep(UI_WAIT_TIMEOUT); |
| }); |
| } |
| |
| private List<AutofillId> startCustomTextViewActivityAndGetViewsForTranslation() { |
| // Start CustomTextViewActivity and get needed information |
| Intent intent = new Intent(sContext, CustomTextViewActivity.class) |
| .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| AtomicReference<List<AutofillId>> viewAutofillIdsRef = new AtomicReference<>(); |
| |
| mCustomTextViewActivityScenario = ActivityScenario.launch(intent); |
| mCustomTextViewActivityScenario.onActivity(activity -> { |
| mResponseNotSetTextView = activity.getResponseNotSetText(); |
| mCustomTextView = activity.getCustomText(); |
| // Get the views that need to be translated. |
| viewAutofillIdsRef.set(activity.getViewsForTranslation()); |
| }); |
| return viewAutofillIdsRef.get(); |
| } |
| |
| private List<AutofillId> startVirtualContainerViewActivityAndGetViewsForTranslation() { |
| // Start VirtualContainerViewActivity and get needed information |
| Intent intent = new Intent(sContext, VirtualContainerViewActivity.class) |
| .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| AtomicReference<List<AutofillId>> viewAutofillIdsRef = new AtomicReference<>(); |
| |
| mVirtualContainerViewActivityScenario = ActivityScenario.launch(intent); |
| mVirtualContainerViewActivityScenario.onActivity(activity -> { |
| mVirtualContainerView = activity.getVirtualContainerView(); |
| // Get the views that need to be translated. |
| viewAutofillIdsRef.set(activity.getViewsForTranslation()); |
| }); |
| return viewAutofillIdsRef.get(); |
| } |
| |
| private BlockingBroadcastReceiver sendCommandToIme(String action, boolean mutable) { |
| final String actionImeServiceCommandDone = action + "_" + SystemClock.uptimeMillis(); |
| final BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver(sContext, |
| actionImeServiceCommandDone); |
| receiver.register(); |
| final Intent commandIntent = new Intent(action); |
| final PendingIntent pendingIntent = |
| PendingIntent.getBroadcast( |
| sContext, |
| 0, |
| new Intent(actionImeServiceCommandDone), |
| mutable ? PendingIntent.FLAG_MUTABLE : PendingIntent.FLAG_IMMUTABLE); |
| commandIntent.putExtra(EXTRA_FINISH_COMMAND, pendingIntent); |
| sContext.sendBroadcast(commandIntent); |
| return receiver; |
| } |
| |
| private CtsContentCaptureService enableContentCaptureService() throws Exception { |
| mContentCaptureServiceWatcher = CtsContentCaptureService.setServiceWatcher(); |
| Helper.setTemporaryContentCaptureService(CtsContentCaptureService.SERVICE_NAME); |
| mContentCaptureServiceWatcher.setAllowSelf(); |
| return mContentCaptureServiceWatcher.waitOnConnected(); |
| } |
| |
| private ContentCaptureContext getContentCaptureContextFromContentCaptureService( |
| CtsContentCaptureService service) { |
| service.awaitSessionCreated(CtsContentCaptureService.GENERIC_TIMEOUT_MS); |
| final ContentCaptureContext contentCaptureContext = service.getContentCaptureContext(); |
| Log.d(TAG, "contentCaptureContext = " + contentCaptureContext); |
| |
| assertThat(contentCaptureContext).isNotNull(); |
| assertThat(contentCaptureContext.getActivityId()).isNotNull(); |
| |
| return contentCaptureContext; |
| } |
| |
| private TranslationResponse createViewsTranslationResponse(List<AutofillId> viewAutofillIds, |
| String translatedText) { |
| final TranslationResponse.Builder responseBuilder = |
| new TranslationResponse.Builder(TranslationResponse.TRANSLATION_STATUS_SUCCESS); |
| for (int i = 0; i < viewAutofillIds.size(); i++) { |
| ViewTranslationResponse.Builder responseDataBuilder = |
| new ViewTranslationResponse.Builder(viewAutofillIds.get(i)) |
| .setValue(ViewTranslationRequest.ID_TEXT, |
| new TranslationResponseValue.Builder(STATUS_SUCCESS) |
| .setText(translatedText).build()) |
| .setValue(Helper.CUSTOM_TRANSLATION_ID_MY_TAG, |
| new TranslationResponseValue.Builder(STATUS_SUCCESS) |
| .setText(translatedText).build()) |
| .setValue(ID_CONTENT_DESCRIPTION, |
| new TranslationResponseValue.Builder(STATUS_SUCCESS) |
| .setText(translatedText).build()); |
| responseBuilder.setViewTranslationResponse(i, responseDataBuilder.build()); |
| } |
| return responseBuilder.build(); |
| } |
| |
| private Pair<List<AutofillId>, ContentCaptureContext> |
| enableServicesAndStartActivityForTranslation() throws Exception { |
| // Enable CTS ContentCaptureService |
| CtsContentCaptureService contentcaptureService = enableContentCaptureService(); |
| |
| // Start Activity and get needed information |
| Intent intent = new Intent(sContext, SimpleActivity.class) |
| .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| AtomicReference<CharSequence> originalTextRef = new AtomicReference<>(); |
| AtomicReference<List<AutofillId>> viewAutofillIdsRef = new AtomicReference<>(); |
| |
| mActivityScenario = ActivityScenario.launch(intent); |
| mActivityScenario.onActivity(activity -> { |
| mTextView = activity.getHelloText(); |
| originalTextRef.set(activity.getHelloText().getText()); |
| viewAutofillIdsRef.set(activity.getViewsForTranslation()); |
| }); |
| CharSequence originalText = originalTextRef.get(); |
| // Get the views that need to be translated. |
| List<AutofillId> views = viewAutofillIdsRef.get(); |
| |
| // Wait session created and get the ContentCaptureContext from ContentCaptureService |
| ContentCaptureContext contentCaptureContext = |
| getContentCaptureContextFromContentCaptureService(contentcaptureService); |
| |
| // enable CTS TranslationService |
| mTranslationServiceServiceWatcher = CtsTranslationService.setServiceWatcher(); |
| Helper.setTemporaryTranslationService(CtsTranslationService.SERVICE_NAME); |
| |
| // TODO(b/184617863): use separate methods not use Pair here. |
| return new Pair(views, contentCaptureContext); |
| } |
| |
| private static class ImeSession implements AutoCloseable { |
| |
| private static final long TIMEOUT = 2000; |
| private final ComponentName mImeName; |
| |
| ImeSession(ComponentName ime) throws Exception { |
| mImeName = ime; |
| runShellCommand("ime reset"); |
| // TODO(b/184617863): get IME component from InputMethodManager#getInputMethodList |
| runShellCommand("ime enable " + ime.flattenToShortString()); |
| runShellCommand("ime set " + ime.flattenToShortString()); |
| PollingCheck.check("Make sure that MockIME becomes available", TIMEOUT, |
| () -> ime.equals(getCurrentInputMethodId())); |
| } |
| |
| @Override |
| public void close() throws Exception { |
| runShellCommand("ime reset"); |
| PollingCheck.check("Make sure that MockIME becomes unavailable", TIMEOUT, () -> |
| sContext.getSystemService(InputMethodManager.class) |
| .getEnabledInputMethodList() |
| .stream() |
| .noneMatch(info -> mImeName.equals(info.getComponent()))); |
| } |
| |
| private ComponentName getCurrentInputMethodId() { |
| return ComponentName.unflattenFromString( |
| Settings.Secure.getString(sContext.getContentResolver(), |
| Settings.Secure.DEFAULT_INPUT_METHOD)); |
| } |
| } |
| } |