Commit f715b16c authored by Michael Bai's avatar Michael Bai Committed by Commit Bot

ContentCapture: Suppress platform exception

Framework has a bug which causes a exception in a race condition,
the exception make the app crash.

This patch catches this specific exception to prevent crash.

Adds a new set of tests for NotificationTask, its subclasses and
PlatformAPIWrapper to verify the capture exception working
correctly.

Bug: 1131430
Change-Id: Ia6ef93c47421700c0e52b1de267c6be00d0dd51f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2521224Reviewed-by: default avatarXianzhu Wang <wangxianzhu@chromium.org>
Reviewed-by: default avatarChangwan Ryu <changwan@chromium.org>
Commit-Queue: Michael Bai <michaelbai@chromium.org>
Cr-Commit-Position: refs/heads/master@{#826020}
parent 62f1fc8e
...@@ -794,6 +794,7 @@ if (is_android) { ...@@ -794,6 +794,7 @@ if (is_android) {
"//components/browser_ui/util/android:junit", "//components/browser_ui/util/android:junit",
"//components/browser_ui/webshare/android:junit", "//components/browser_ui/webshare/android:junit",
"//components/browser_ui/widget/android:junit", "//components/browser_ui/widget/android:junit",
"//components/content_capture/android/junit:components_content_capture_junit_tests",
"//components/embedder_support/android:components_embedder_support_junit_tests", "//components/embedder_support/android:components_embedder_support_junit_tests",
"//components/gcm_driver/android:components_gcm_driver_junit_tests", "//components/gcm_driver/android:components_gcm_driver_junit_tests",
"//components/media_router/browser/android:junit", "//components/media_router/browser/android:junit",
......
...@@ -41,6 +41,8 @@ android_library("java") { ...@@ -41,6 +41,8 @@ android_library("java") {
"java/src/org/chromium/components/content_capture/ExperimentContentCaptureConsumer.java", "java/src/org/chromium/components/content_capture/ExperimentContentCaptureConsumer.java",
"java/src/org/chromium/components/content_capture/FrameSession.java", "java/src/org/chromium/components/content_capture/FrameSession.java",
"java/src/org/chromium/components/content_capture/NotificationTask.java", "java/src/org/chromium/components/content_capture/NotificationTask.java",
"java/src/org/chromium/components/content_capture/PlatformAPIWrapper.java",
"java/src/org/chromium/components/content_capture/PlatformAPIWrapperImpl.java",
"java/src/org/chromium/components/content_capture/PlatformSession.java", "java/src/org/chromium/components/content_capture/PlatformSession.java",
"java/src/org/chromium/components/content_capture/ProcessContentCaptureDataTask.java", "java/src/org/chromium/components/content_capture/ProcessContentCaptureDataTask.java",
"java/src/org/chromium/components/content_capture/SessionRemovedTask.java", "java/src/org/chromium/components/content_capture/SessionRemovedTask.java",
......
...@@ -19,16 +19,16 @@ class ContentRemovedTask extends NotificationTask { ...@@ -19,16 +19,16 @@ class ContentRemovedTask extends NotificationTask {
} }
@Override @Override
protected Boolean doInBackground() { protected void runTask() {
removeContent(); removeContent();
return true;
} }
private void removeContent() { private void removeContent() {
log("ContentRemovedTask.removeContent"); log("ContentRemovedTask.removeContent");
PlatformSessionData platformSessionData = buildCurrentSession(); PlatformSessionData platformSessionData = buildCurrentSession();
if (platformSessionData == null) return; if (platformSessionData == null) return;
platformSessionData.contentCaptureSession.notifyViewsDisappeared( PlatformAPIWrapper.getInstance().notifyViewsDisappeared(
platformSessionData.contentCaptureSession,
mPlatformSession.getRootPlatformSessionData().autofillId, mRemovedIds); mPlatformSession.getRootPlatformSessionData().autofillId, mRemovedIds);
} }
} }
...@@ -25,10 +25,11 @@ class ContentUpdateTask extends ProcessContentCaptureDataTask { ...@@ -25,10 +25,11 @@ class ContentUpdateTask extends ProcessContentCaptureDataTask {
private AutofillId notifyViewTextChanged( private AutofillId notifyViewTextChanged(
PlatformSessionData parentPlatformSessionData, ContentCaptureData data) { PlatformSessionData parentPlatformSessionData, ContentCaptureData data) {
AutofillId autofillId = parentPlatformSessionData.contentCaptureSession.newAutofillId( AutofillId autofillId = PlatformAPIWrapper.getInstance().newAutofillId(
parentPlatformSessionData.contentCaptureSession,
mPlatformSession.getRootPlatformSessionData().autofillId, data.getId()); mPlatformSession.getRootPlatformSessionData().autofillId, data.getId());
parentPlatformSessionData.contentCaptureSession.notifyViewTextChanged( PlatformAPIWrapper.getInstance().notifyViewTextChanged(
autofillId, data.getValue()); parentPlatformSessionData.contentCaptureSession, autofillId, data.getValue());
return autofillId; return autofillId;
} }
} }
...@@ -5,15 +5,15 @@ ...@@ -5,15 +5,15 @@
package org.chromium.components.content_capture; package org.chromium.components.content_capture;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.LocusId;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Build; import android.os.Build;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.ViewStructure; import android.view.ViewStructure;
import android.view.autofill.AutofillId; import android.view.autofill.AutofillId;
import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureSession; import android.view.contentcapture.ContentCaptureSession;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.base.annotations.VerifiesOnQ; import org.chromium.base.annotations.VerifiesOnQ;
import org.chromium.base.task.AsyncTask; import org.chromium.base.task.AsyncTask;
...@@ -31,6 +31,26 @@ abstract class NotificationTask extends AsyncTask<Boolean> { ...@@ -31,6 +31,26 @@ abstract class NotificationTask extends AsyncTask<Boolean> {
protected final FrameSession mSession; protected final FrameSession mSession;
protected final PlatformSession mPlatformSession; protected final PlatformSession mPlatformSession;
private boolean mHasPlatformExceptionForTesting;
/**
* A specific framework ContentCapture exception in Android Q and R shall be caught to prevent
* the crash, the current NotificationTask can't be recovered from exception and has to
* exit, the next task shall continue to run even it could cause the inconsistent state in
* Android framework and aiai service who shall bear with it.
*
* Refer to crbug.com/1131430 for details.
*/
private static boolean isMainContentCaptureSesionSentEventException(NullPointerException e) {
for (StackTraceElement s : e.getStackTrace()) {
if (s.getClassName().startsWith("android.view.contentcapture.MainContentCaptureSession")
&& s.getMethodName().startsWith("sendEvent")) {
return true;
}
}
return false;
}
public NotificationTask(FrameSession session, PlatformSession platformSession) { public NotificationTask(FrameSession session, PlatformSession platformSession) {
mSession = session; mSession = session;
mPlatformSession = platformSession; mPlatformSession = platformSession;
...@@ -54,15 +74,16 @@ abstract class NotificationTask extends AsyncTask<Boolean> { ...@@ -54,15 +74,16 @@ abstract class NotificationTask extends AsyncTask<Boolean> {
protected AutofillId notifyViewAppeared( protected AutofillId notifyViewAppeared(
PlatformSessionData parentPlatformSessionData, ContentCaptureData data) { PlatformSessionData parentPlatformSessionData, ContentCaptureData data) {
ViewStructure viewStructure = ViewStructure viewStructure = PlatformAPIWrapper.getInstance().newVirtualViewStructure(
parentPlatformSessionData.contentCaptureSession.newVirtualViewStructure( parentPlatformSessionData.contentCaptureSession,
parentPlatformSessionData.autofillId, data.getId()); parentPlatformSessionData.autofillId, data.getId());
if (!data.hasChildren()) viewStructure.setText(data.getValue()); if (!data.hasChildren()) viewStructure.setText(data.getValue());
Rect rect = data.getBounds(); Rect rect = data.getBounds();
// Always set scroll as (0, 0). // Always set scroll as (0, 0).
viewStructure.setDimens(rect.left, rect.top, 0, 0, rect.width(), rect.height()); viewStructure.setDimens(rect.left, rect.top, 0, 0, rect.width(), rect.height());
parentPlatformSessionData.contentCaptureSession.notifyViewAppeared(viewStructure); PlatformAPIWrapper.getInstance().notifyViewAppeared(
parentPlatformSessionData.contentCaptureSession, viewStructure);
return viewStructure.getAutofillId(); return viewStructure.getAutofillId();
} }
...@@ -72,10 +93,10 @@ abstract class NotificationTask extends AsyncTask<Boolean> { ...@@ -72,10 +93,10 @@ abstract class NotificationTask extends AsyncTask<Boolean> {
mPlatformSession.getFrameIdToPlatformSessionData().get(frame.getId()); mPlatformSession.getFrameIdToPlatformSessionData().get(frame.getId());
if (platformSessionData == null && !TextUtils.isEmpty(frame.getValue())) { if (platformSessionData == null && !TextUtils.isEmpty(frame.getValue())) {
ContentCaptureSession session = ContentCaptureSession session =
parentPlatformSessionData.contentCaptureSession.createContentCaptureSession( PlatformAPIWrapper.getInstance().createContentCaptureSession(
new ContentCaptureContext.Builder(new LocusId(frame.getValue())) parentPlatformSessionData.contentCaptureSession, frame.getValue());
.build()); AutofillId autofillId = PlatformAPIWrapper.getInstance().newAutofillId(
AutofillId autofillId = parentPlatformSessionData.contentCaptureSession.newAutofillId( parentPlatformSessionData.contentCaptureSession,
mPlatformSession.getRootPlatformSessionData().autofillId, frame.getId()); mPlatformSession.getRootPlatformSessionData().autofillId, frame.getId());
autofillId = notifyViewAppeared(parentPlatformSessionData, frame); autofillId = notifyViewAppeared(parentPlatformSessionData, frame);
platformSessionData = new PlatformSessionData(session, autofillId); platformSessionData = new PlatformSessionData(session, autofillId);
...@@ -85,10 +106,32 @@ abstract class NotificationTask extends AsyncTask<Boolean> { ...@@ -85,10 +106,32 @@ abstract class NotificationTask extends AsyncTask<Boolean> {
return platformSessionData; return platformSessionData;
} }
@VisibleForTesting
public boolean hasPlatformExceptionForTesting() {
return mHasPlatformExceptionForTesting;
}
protected void log(String message) { protected void log(String message) {
if (sDump.booleanValue()) Log.i(TAG, message); if (sDump.booleanValue()) Log.i(TAG, message);
} }
@Override @Override
protected void onPostExecute(Boolean result) {} protected void onPostExecute(Boolean result) {}
@Override
public final Boolean doInBackground() {
try {
runTask();
} catch (NullPointerException e) {
if (isMainContentCaptureSesionSentEventException(e)) {
mHasPlatformExceptionForTesting = true;
Log.e(TAG, "PlatformException", e);
} else {
throw e;
}
}
return true;
}
protected abstract void runTask();
} }
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.content_capture;
import android.annotation.TargetApi;
import android.os.Build;
import android.view.ViewStructure;
import android.view.autofill.AutofillId;
import android.view.contentcapture.ContentCaptureSession;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.annotations.VerifiesOnQ;
/**
* The class to wrap ContentCapture platform APIs, catches the exception from platform, and
* re-throws with PlatformAPIException, so the call sites can catch platform exception to avoid
* the crash.
*/
@VerifiesOnQ
@TargetApi(Build.VERSION_CODES.Q)
public abstract class PlatformAPIWrapper {
private static PlatformAPIWrapper sImpl;
public static PlatformAPIWrapper getInstance() {
if (sImpl == null) {
sImpl = new PlatformAPIWrapperImpl();
}
return sImpl;
}
public abstract ContentCaptureSession createContentCaptureSession(
ContentCaptureSession parent, String url);
public abstract void destroyContentCaptureSession(ContentCaptureSession session);
public abstract AutofillId newAutofillId(
ContentCaptureSession parent, AutofillId rootAutofillId, long id);
public abstract ViewStructure newVirtualViewStructure(
ContentCaptureSession parent, AutofillId parentAutofillId, long id);
public abstract void notifyViewAppeared(
ContentCaptureSession session, ViewStructure viewStructure);
public abstract void notifyViewDisappeared(
ContentCaptureSession session, AutofillId autofillId);
public abstract void notifyViewsDisappeared(
ContentCaptureSession session, AutofillId autofillId, long[] ids);
public abstract void notifyViewTextChanged(
ContentCaptureSession session, AutofillId autofillId, String newContent);
@VisibleForTesting
public static void setPlatformAPIWrapperImplForTesting(PlatformAPIWrapper impl) {
sImpl = impl;
}
}
\ No newline at end of file
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.content_capture;
import android.annotation.TargetApi;
import android.content.LocusId;
import android.os.Build;
import android.view.ViewStructure;
import android.view.autofill.AutofillId;
import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureSession;
import org.chromium.base.annotations.VerifiesOnQ;
/**
* The implementation of PlatformAPIWrapper.
*/
@VerifiesOnQ
@TargetApi(Build.VERSION_CODES.Q)
public class PlatformAPIWrapperImpl extends PlatformAPIWrapper {
@Override
public ContentCaptureSession createContentCaptureSession(
ContentCaptureSession parent, String url) {
return parent.createContentCaptureSession(
new ContentCaptureContext.Builder(new LocusId(url)).build());
}
@Override
public void destroyContentCaptureSession(ContentCaptureSession session) {
session.destroy();
}
@Override
public AutofillId newAutofillId(
ContentCaptureSession parent, AutofillId rootAutofillId, long id) {
return parent.newAutofillId(rootAutofillId, id);
}
@Override
public ViewStructure newVirtualViewStructure(
ContentCaptureSession parent, AutofillId parentAutofillId, long id) {
return parent.newVirtualViewStructure(parentAutofillId, id);
}
@Override
public void notifyViewAppeared(ContentCaptureSession session, ViewStructure viewStructure) {
session.notifyViewAppeared(viewStructure);
}
@Override
public void notifyViewDisappeared(ContentCaptureSession session, AutofillId autofillId) {
session.notifyViewDisappeared(autofillId);
}
@Override
public void notifyViewsDisappeared(
ContentCaptureSession session, AutofillId autofillId, long[] ids) {
session.notifyViewsDisappeared(autofillId, ids);
}
@Override
public void notifyViewTextChanged(
ContentCaptureSession session, AutofillId autofillId, String newContent) {
session.notifyViewTextChanged(autofillId, newContent);
}
}
...@@ -27,9 +27,8 @@ abstract class ProcessContentCaptureDataTask extends NotificationTask { ...@@ -27,9 +27,8 @@ abstract class ProcessContentCaptureDataTask extends NotificationTask {
} }
@Override @Override
protected Boolean doInBackground() { protected void runTask() {
processContent(); processContent();
return true;
} }
private void processContent() { private void processContent() {
......
...@@ -15,9 +15,8 @@ class SessionRemovedTask extends NotificationTask { ...@@ -15,9 +15,8 @@ class SessionRemovedTask extends NotificationTask {
} }
@Override @Override
protected Boolean doInBackground() { protected void runTask() {
removeSession(); removeSession();
return true;
} }
private void removeSession() { private void removeSession() {
...@@ -25,7 +24,8 @@ class SessionRemovedTask extends NotificationTask { ...@@ -25,7 +24,8 @@ class SessionRemovedTask extends NotificationTask {
PlatformSessionData removedPlatformSessionData = PlatformSessionData removedPlatformSessionData =
mPlatformSession.getFrameIdToPlatformSessionData().remove(mSession.get(0).getId()); mPlatformSession.getFrameIdToPlatformSessionData().remove(mSession.get(0).getId());
if (removedPlatformSessionData == null) return; if (removedPlatformSessionData == null) return;
removedPlatformSessionData.contentCaptureSession.destroy(); PlatformAPIWrapper.getInstance().destroyContentCaptureSession(
removedPlatformSessionData.contentCaptureSession);
PlatformSessionData parentPlatformSessionData = PlatformSessionData parentPlatformSessionData =
mPlatformSession.getRootPlatformSessionData(); mPlatformSession.getRootPlatformSessionData();
// We need to notify the view disappeared through the removed session's parent, // We need to notify the view disappeared through the removed session's parent,
...@@ -36,7 +36,8 @@ class SessionRemovedTask extends NotificationTask { ...@@ -36,7 +36,8 @@ class SessionRemovedTask extends NotificationTask {
mPlatformSession.getFrameIdToPlatformSessionData().get(mSession.get(1).getId()); mPlatformSession.getFrameIdToPlatformSessionData().get(mSession.get(1).getId());
} }
if (parentPlatformSessionData == null) return; if (parentPlatformSessionData == null) return;
parentPlatformSessionData.contentCaptureSession.notifyViewDisappeared( PlatformAPIWrapper.getInstance().notifyViewDisappeared(
parentPlatformSessionData.contentCaptureSession,
removedPlatformSessionData.autofillId); removedPlatformSessionData.autofillId);
} }
} }
# Copyright 2020 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("//build/config/android/config.gni")
import("//build/config/android/rules.gni")
java_library("components_content_capture_junit_tests") {
# Platform checks are broken for Robolectric. See https://crbug.com/1071638.
bypass_platform_checks = true
testonly = true
sources = [
"src/org/chromium/components/content_capture/PlatformAPIWrapperTest.java",
]
deps = [
"//base:base_java",
"//base:base_java_test_support",
"//base:base_junit_test_support",
"//components/content_capture/android:java",
"//content/public/android:content_java",
"//third_party/android_deps:robolectric_all_java",
"//third_party/junit",
"//third_party/mockito:mockito_java",
]
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.content_capture;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.graphics.Rect;
import android.view.ViewStructure;
import android.view.autofill.AutofillId;
import android.view.contentcapture.ContentCaptureSession;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
import java.util.ArrayList;
import java.util.Arrays;
/**
* The unit tests for PlatformAPIWrapper, NotificationTask and its subclasses. It runs the
* ContentCapturedTask, ContentUpdateTask, ContentRemovedTask and SessionRemovedTask, then
* verifies if the PlatformAPIWrapper get the expected parameters.
*
* This test also verifies that the covered code path won't call ContentCaptureSession API
* directly, and verified that all tasks catch the PlatformAPIException correctly.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class PlatformAPIWrapperTest {
/**
* This class implements the methods we care, mockito will mock other method
* for us. By declaring the method be final, it won't be mocked by mockito.
*/
private abstract static class ViewStructureTestHelper extends ViewStructure {
private CharSequence mText;
private Rect mDimens;
@Override
public final void setText(CharSequence text) {
mText = text;
}
@Override
public final CharSequence getText() {
return mText;
}
@Override
public final void setDimens(
int left, int top, int scrollX, int scrollY, int width, int height) {
mDimens = new Rect(left, top, left + width, top + height);
}
public final Rect getDimens() {
return mDimens;
}
}
/**
* This class records the invoked method and the passing parameter in sequence for
* verification later.
*/
private static class PlatformAPIWrapperTestHelper extends PlatformAPIWrapper {
// The Id of API call.
public static final int CREATE_CONTENT_CAPTURE_SESSION = 1;
public static final int NEW_AUTOFILL_ID = 2;
public static final int NEW_VIRTUAL_VIEW_STRUCTURE = 3;
public static final int NOTIFY_VIEW_APPEARED = 4;
public static final int NOTIFY_VIEW_DISAPPEARED = 5;
public static final int NOTIFY_VIEWS_DISAPPEARED = 6;
public static final int NOTIFY_VIEW_TEXT_CHANGED = 7;
public static final int DESTROY_CONTENT_CAPTURE_SESSION = 8;
// The array for objects returned by the mocked APIs
public final ArrayList<ContentCaptureSession> mCreatedContentCaptureSessions =
new ArrayList<ContentCaptureSession>();
public final ArrayList<AutofillId> mCreatedAutofilIds = new ArrayList<AutofillId>();
public final ArrayList<ViewStructureTestHelper> mCreatedViewStructures =
new ArrayList<ViewStructureTestHelper>();
public final ArrayList<AutofillId> mCreatedViewStructuresAutofilIds =
new ArrayList<AutofillId>();
// Array to record the API calls in sequence.
private volatile ArrayList<Integer> mCallbacks = new ArrayList<Integer>();
// Mock a ContentCaptureSession which will throw a exception if all its public
// method is called because this test use the mocked PlatformAPIWrapper, the methods
// of ContentCaptureSession will never be called.
private ContentCaptureSession createMockedContentCaptureSession() {
ContentCaptureSession mockedContentCaptureSession =
Mockito.mock(ContentCaptureSession.class);
// Prevent the below methods being called from the class other than PlatformAPIWrapper.
final String errorMsg = "Shall be called from PlatformAPIWrapper.";
doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
.when(mockedContentCaptureSession)
.createContentCaptureSession(ArgumentMatchers.any());
doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
.when(mockedContentCaptureSession)
.newAutofillId(ArgumentMatchers.any(), ArgumentMatchers.anyLong());
doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
.when(mockedContentCaptureSession)
.destroy();
doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
.when(mockedContentCaptureSession)
.getContentCaptureContext();
doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
.when(mockedContentCaptureSession)
.getContentCaptureSessionId();
doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
.when(mockedContentCaptureSession)
.newViewStructure(ArgumentMatchers.any());
doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
.when(mockedContentCaptureSession)
.newVirtualViewStructure(ArgumentMatchers.any(), ArgumentMatchers.anyLong());
doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
.when(mockedContentCaptureSession)
.notifyViewAppeared(ArgumentMatchers.any());
doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
.when(mockedContentCaptureSession)
.notifyViewDisappeared(ArgumentMatchers.any());
doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
.when(mockedContentCaptureSession)
.notifyViewTextChanged(ArgumentMatchers.any(), ArgumentMatchers.any());
doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
.when(mockedContentCaptureSession)
.notifyViewsDisappeared(ArgumentMatchers.any(), ArgumentMatchers.any());
doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
.when(mockedContentCaptureSession)
.setContentCaptureContext(ArgumentMatchers.any());
// TODO(crbug.com/1145784): Enable below once mockito support them.
// doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
// .when(mockedContentCaptureSession)
// .notifySessionPaused();
// doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
// .when(mockedContentCaptureSession)
// .notifySessionResumed();
// doThrow(new RuntimeException("Shall be called from PlatformAPIWrapper."))
// .when(mockedContentCaptureSession)
// .notifyViewInsetsChanged(ArgumentMatchers.any());
return mockedContentCaptureSession;
}
@Override
public ContentCaptureSession createContentCaptureSession(
ContentCaptureSession parent, String url) {
mCallbacks.add(CREATE_CONTENT_CAPTURE_SESSION);
ContentCaptureSession mockedContentCaptureSession = createMockedContentCaptureSession();
mCreatedContentCaptureSessions.add(mockedContentCaptureSession);
return mockedContentCaptureSession;
}
@Override
public void destroyContentCaptureSession(ContentCaptureSession session) {
mCallbacks.add(DESTROY_CONTENT_CAPTURE_SESSION);
}
@Override
public AutofillId newAutofillId(
ContentCaptureSession parent, AutofillId rootAutofillId, long id) {
mCallbacks.add(NEW_AUTOFILL_ID);
AutofillId mockedAutofillId = Mockito.mock(AutofillId.class);
mCreatedAutofilIds.add(mockedAutofillId);
return mockedAutofillId;
}
@Override
public ViewStructure newVirtualViewStructure(
ContentCaptureSession parent, AutofillId parentAutofillId, long id) {
mCallbacks.add(NEW_VIRTUAL_VIEW_STRUCTURE);
ViewStructureTestHelper mockedViewStructure =
Mockito.mock(ViewStructureTestHelper.class);
AutofillId mockedAutofilId = Mockito.mock(AutofillId.class);
mCreatedViewStructuresAutofilIds.add(mockedAutofilId);
when(mockedViewStructure.getAutofillId()).thenReturn(mockedAutofilId);
mCreatedViewStructures.add(mockedViewStructure);
return mockedViewStructure;
}
@Override
public void notifyViewAppeared(ContentCaptureSession session, ViewStructure viewStructure) {
mCallbacks.add(NOTIFY_VIEW_APPEARED);
}
@Override
public void notifyViewDisappeared(ContentCaptureSession parent, AutofillId autofillId) {
mCallbacks.add(NOTIFY_VIEW_DISAPPEARED);
}
@Override
public void notifyViewsDisappeared(
ContentCaptureSession session, AutofillId autofillId, long[] ids) {
mCallbacks.add(NOTIFY_VIEWS_DISAPPEARED);
}
@Override
public void notifyViewTextChanged(
ContentCaptureSession session, AutofillId autofillId, String newContent) {
mCallbacks.add(NOTIFY_VIEW_TEXT_CHANGED);
}
public void reset() {
mCallbacks.clear();
}
public int[] getCallbacks() {
int[] result = new int[mCallbacks.size()];
int index = 0;
for (Integer c : mCallbacks) {
result[index++] = c;
}
return result;
}
}
private static final String MAIN_URL = "http://main.domain.com";
private static final long MAIN_ID = 4;
private static final Rect MAIN_FRAME_RECT = new Rect(0, 0, 200, 200);
private static final String CHILD_URL = "http://test.domain.com";
private static final long CHILD_FRAME_ID = 1;
private static final Rect CHILD_FRAME_RECT = new Rect(0, 0, 100, 100);
private static final long CHILD1_ID = 2;
private static final String CHILD1_TEXT = "Hello";
private static final Rect CHILD1_RECT = new Rect(10, 10, 20, 20);
private static final long CHILD2_ID = 3;
private static final String CHILD2_TEXT = "World";
private static final Rect CHILD2_RECT = new Rect(20, 20, 20, 20);
private static final String CHILD2_NEW_TEXT = " world!";
private static final long[] REMOVED_IDS = {CHILD1_ID, CHILD2_ID};
private ContentCaptureSession mMockedRootContentCaptureSession;
private AutofillId mMockedRootAutofillId;
private PlatformSession mRootPlatformSession;
private PlatformAPIWrapperTestHelper mPlatformAPIWrapperTestHelper;
private PlatformAPIWrapperTestHelper mPlatformAPIWrapperTestHelperSpy;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mMockedRootContentCaptureSession = Mockito.mock(ContentCaptureSession.class);
mMockedRootAutofillId = Mockito.mock(AutofillId.class);
mRootPlatformSession =
new PlatformSession(mMockedRootContentCaptureSession, mMockedRootAutofillId);
mPlatformAPIWrapperTestHelper = new PlatformAPIWrapperTestHelper();
mPlatformAPIWrapperTestHelperSpy = spy(mPlatformAPIWrapperTestHelper);
PlatformAPIWrapper.setPlatformAPIWrapperImplForTesting(mPlatformAPIWrapperTestHelperSpy);
}
private static void verifyCallbacks(int[] expectedCallbacks, int[] results) {
Assert.assertArrayEquals("Expect: " + Arrays.toString(expectedCallbacks)
+ " Result: " + Arrays.toString(results),
expectedCallbacks, results);
}
private static int[] toIntArray(int... callbacks) {
return callbacks;
}
private void runTaskAndVerifyCallback(NotificationTask task, int[] expectedCallbacks)
throws Exception {
mPlatformAPIWrapperTestHelper.reset();
task.doInBackground();
verifyCallbacks(expectedCallbacks, mPlatformAPIWrapperTestHelper.getCallbacks());
}
private void runTaskAndVerifyCallbackWithException(
NotificationTask task, int[] expectedCallbacks) throws Exception {
runTaskAndVerifyCallback(task, expectedCallbacks);
Assert.assertTrue(task.hasPlatformExceptionForTesting());
}
private void verifyVirtualViewStructure(String expectedText, Rect expectedRect, int index) {
Assert.assertEquals("Index:" + index, expectedText,
mPlatformAPIWrapperTestHelper.mCreatedViewStructures.get(index).getText());
Assert.assertEquals("Index:" + index, expectedRect,
mPlatformAPIWrapperTestHelper.mCreatedViewStructures.get(index).getDimens());
}
private FrameSession createFrameSession() {
FrameSession frameSession = new FrameSession(1);
frameSession.add(ContentCaptureData.createContentCaptureData(null, MAIN_ID, MAIN_URL,
MAIN_FRAME_RECT.left, MAIN_FRAME_RECT.top, MAIN_FRAME_RECT.width(),
MAIN_FRAME_RECT.height()));
return frameSession;
}
private FrameSession createFrameSessionForRemoveTask() {
FrameSession frameSessionForRemoveTask = createFrameSession();
frameSessionForRemoveTask.add(0,
ContentCaptureData.createContentCaptureData(null, CHILD_FRAME_ID, CHILD_URL,
CHILD_FRAME_RECT.left, CHILD_FRAME_RECT.top, CHILD_FRAME_RECT.width(),
CHILD_FRAME_RECT.height()));
return frameSessionForRemoveTask;
}
// The below createFooTask() create the tasks for tests.
private ContentCapturedTask createContentCapturedTask() {
FrameSession frameSession = createFrameSession();
ContentCaptureData data = ContentCaptureData.createContentCaptureData(null, CHILD_FRAME_ID,
CHILD_URL, CHILD_FRAME_RECT.left, CHILD_FRAME_RECT.top, CHILD_FRAME_RECT.width(),
CHILD_FRAME_RECT.height());
ContentCaptureData.createContentCaptureData(data, CHILD1_ID, CHILD1_TEXT, CHILD1_RECT.left,
CHILD1_RECT.top, CHILD1_RECT.width(), CHILD1_RECT.height());
ContentCaptureData.createContentCaptureData(data, CHILD2_ID, CHILD2_TEXT, CHILD2_RECT.left,
CHILD2_RECT.top, CHILD2_RECT.width(), CHILD2_RECT.height());
return new ContentCapturedTask(frameSession, data, mRootPlatformSession);
}
private ContentUpdateTask createChild2ContentUpdateTask() {
// Modifies child2
ContentCaptureData changeTextData = ContentCaptureData.createContentCaptureData(null,
CHILD_FRAME_ID, CHILD_URL, CHILD_FRAME_RECT.left, CHILD_FRAME_RECT.top,
CHILD_FRAME_RECT.width(), CHILD_FRAME_RECT.height());
ContentCaptureData.createContentCaptureData(changeTextData, CHILD2_ID, CHILD2_NEW_TEXT,
CHILD2_RECT.left, CHILD2_RECT.top, CHILD2_RECT.width(), CHILD2_RECT.height());
return new ContentUpdateTask(createFrameSession(), changeTextData, mRootPlatformSession);
}
private ContentRemovedTask createRemoveChildrenTask() {
// Removes the child1 and child2
return new ContentRemovedTask(
createFrameSessionForRemoveTask(), REMOVED_IDS, mRootPlatformSession);
}
private SessionRemovedTask createSessionRemovedTask() {
return new SessionRemovedTask(createFrameSessionForRemoveTask(), mRootPlatformSession);
}
private void runContentCapturedTask() throws Exception {
runTaskAndVerifyCallback(createContentCapturedTask(),
toIntArray(PlatformAPIWrapperTestHelper.CREATE_CONTENT_CAPTURE_SESSION,
PlatformAPIWrapperTestHelper.NEW_AUTOFILL_ID,
PlatformAPIWrapperTestHelper.NEW_VIRTUAL_VIEW_STRUCTURE,
PlatformAPIWrapperTestHelper.NOTIFY_VIEW_APPEARED,
PlatformAPIWrapperTestHelper.CREATE_CONTENT_CAPTURE_SESSION,
PlatformAPIWrapperTestHelper.NEW_AUTOFILL_ID,
PlatformAPIWrapperTestHelper.NEW_VIRTUAL_VIEW_STRUCTURE,
PlatformAPIWrapperTestHelper.NOTIFY_VIEW_APPEARED,
PlatformAPIWrapperTestHelper.NEW_VIRTUAL_VIEW_STRUCTURE,
PlatformAPIWrapperTestHelper.NOTIFY_VIEW_APPEARED,
PlatformAPIWrapperTestHelper.NEW_VIRTUAL_VIEW_STRUCTURE,
PlatformAPIWrapperTestHelper.NOTIFY_VIEW_APPEARED));
}
private NullPointerException createMainContentCaptureSessionException() {
NullPointerException e = new NullPointerException() {
@Override
public StackTraceElement[] getStackTrace() {
StackTraceElement[] stack = new StackTraceElement[super.getStackTrace().length + 1];
int index = 0;
for (StackTraceElement s : super.getStackTrace()) {
stack[index++] = s;
}
stack[index] = new StackTraceElement(
"android.view.contentcapture.MainContentCaptureSession", "sendEvent",
"MainContentCaptureSession.java", 349);
return stack;
}
};
return e;
}
@Test
public void testTypicalLifecycle() throws Throwable {
runContentCapturedTask();
// Verifies main frame.
InOrder inOrder = Mockito.inOrder(mPlatformAPIWrapperTestHelperSpy);
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.createContentCaptureSession(mMockedRootContentCaptureSession, MAIN_URL);
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.newAutofillId(mMockedRootContentCaptureSession, mMockedRootAutofillId, MAIN_ID);
// Verifies the ViewStructure for the main frame.
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.newVirtualViewStructure(
mMockedRootContentCaptureSession, mMockedRootAutofillId, MAIN_ID);
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.notifyViewAppeared(mMockedRootContentCaptureSession,
mPlatformAPIWrapperTestHelper.mCreatedViewStructures.get(0));
verifyVirtualViewStructure(MAIN_URL, MAIN_FRAME_RECT, 0);
// Verifies the child frame.
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.createContentCaptureSession(
mPlatformAPIWrapperTestHelper.mCreatedContentCaptureSessions.get(0),
CHILD_URL);
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.newAutofillId(mPlatformAPIWrapperTestHelper.mCreatedContentCaptureSessions.get(0),
mMockedRootAutofillId, CHILD_FRAME_ID);
// Verifies the ViewStructure for the child frame.
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.newVirtualViewStructure(
mPlatformAPIWrapperTestHelper.mCreatedContentCaptureSessions.get(0),
mPlatformAPIWrapperTestHelper.mCreatedViewStructuresAutofilIds.get(0),
CHILD_FRAME_ID);
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.notifyViewAppeared(
mPlatformAPIWrapperTestHelper.mCreatedContentCaptureSessions.get(0),
mPlatformAPIWrapperTestHelper.mCreatedViewStructures.get(1));
verifyVirtualViewStructure(null, CHILD_FRAME_RECT, 1);
// Verifies child1
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.newVirtualViewStructure(
mPlatformAPIWrapperTestHelper.mCreatedContentCaptureSessions.get(1),
mPlatformAPIWrapperTestHelper.mCreatedViewStructuresAutofilIds.get(1),
CHILD1_ID);
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.notifyViewAppeared(
mPlatformAPIWrapperTestHelper.mCreatedContentCaptureSessions.get(1),
mPlatformAPIWrapperTestHelper.mCreatedViewStructures.get(2));
verifyVirtualViewStructure(CHILD1_TEXT, CHILD1_RECT, 2);
// Verifies child2
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.newVirtualViewStructure(
mPlatformAPIWrapperTestHelper.mCreatedContentCaptureSessions.get(1),
mPlatformAPIWrapperTestHelper.mCreatedViewStructuresAutofilIds.get(1),
CHILD2_ID);
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.notifyViewAppeared(
mPlatformAPIWrapperTestHelper.mCreatedContentCaptureSessions.get(1),
mPlatformAPIWrapperTestHelper.mCreatedViewStructures.get(3));
verifyVirtualViewStructure(CHILD2_TEXT, CHILD2_RECT, 3);
// Modifies child2
runTaskAndVerifyCallback(createChild2ContentUpdateTask(),
toIntArray(PlatformAPIWrapperTestHelper.NEW_AUTOFILL_ID,
PlatformAPIWrapperTestHelper.NOTIFY_VIEW_TEXT_CHANGED));
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.notifyViewTextChanged(
mPlatformAPIWrapperTestHelper.mCreatedContentCaptureSessions.get(1),
mPlatformAPIWrapperTestHelper.mCreatedAutofilIds.get(2), CHILD2_NEW_TEXT);
// Removes the child1 and child2
runTaskAndVerifyCallback(createRemoveChildrenTask(),
toIntArray(PlatformAPIWrapperTestHelper.NOTIFY_VIEWS_DISAPPEARED));
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.notifyViewsDisappeared(
mPlatformAPIWrapperTestHelper.mCreatedContentCaptureSessions.get(1),
mMockedRootAutofillId, REMOVED_IDS);
// Remove the child frame
runTaskAndVerifyCallback(createSessionRemovedTask(),
toIntArray(PlatformAPIWrapperTestHelper.DESTROY_CONTENT_CAPTURE_SESSION,
PlatformAPIWrapperTestHelper.NOTIFY_VIEW_DISAPPEARED));
inOrder.verify(mPlatformAPIWrapperTestHelperSpy)
.destroyContentCaptureSession(
mPlatformAPIWrapperTestHelper.mCreatedContentCaptureSessions.get(1));
}
// The below testFooException() tests mock the specific method to throw exception, then verify
// if it is caught by NotificationTask.
@Test
public void testCreateContentCaptureSessionException() throws Throwable {
PlatformAPIWrapperTestHelper mockedApiWrapperTestHelper =
Mockito.spy(mPlatformAPIWrapperTestHelper);
PlatformAPIWrapper.setPlatformAPIWrapperImplForTesting(mockedApiWrapperTestHelper);
doThrow(createMainContentCaptureSessionException())
.when(mockedApiWrapperTestHelper)
.createContentCaptureSession(ArgumentMatchers.any(), ArgumentMatchers.any());
runTaskAndVerifyCallbackWithException(createContentCapturedTask(), toIntArray());
}
@Test
public void testNewAutofillIdException() throws Throwable {
PlatformAPIWrapperTestHelper mockedApiWrapperTestHelper =
Mockito.spy(mPlatformAPIWrapperTestHelper);
PlatformAPIWrapper.setPlatformAPIWrapperImplForTesting(mockedApiWrapperTestHelper);
doThrow(createMainContentCaptureSessionException())
.when(mockedApiWrapperTestHelper)
.newAutofillId(
ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.anyLong());
runTaskAndVerifyCallbackWithException(createContentCapturedTask(),
toIntArray(PlatformAPIWrapperTestHelper.CREATE_CONTENT_CAPTURE_SESSION));
}
@Test
public void testNewVirtualViewStructureException() throws Throwable {
PlatformAPIWrapperTestHelper mockedApiWrapperTestHelper =
Mockito.spy(mPlatformAPIWrapperTestHelper);
PlatformAPIWrapper.setPlatformAPIWrapperImplForTesting(mockedApiWrapperTestHelper);
doThrow(createMainContentCaptureSessionException())
.when(mockedApiWrapperTestHelper)
.newVirtualViewStructure(
ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.anyLong());
runTaskAndVerifyCallbackWithException(createContentCapturedTask(),
toIntArray(PlatformAPIWrapperTestHelper.CREATE_CONTENT_CAPTURE_SESSION,
PlatformAPIWrapperTestHelper.NEW_AUTOFILL_ID));
}
@Test
public void testNotifyViewAppearException() throws Throwable {
PlatformAPIWrapperTestHelper mockedApiWrapperTestHelper =
Mockito.spy(mPlatformAPIWrapperTestHelper);
PlatformAPIWrapper.setPlatformAPIWrapperImplForTesting(mockedApiWrapperTestHelper);
doThrow(createMainContentCaptureSessionException())
.when(mockedApiWrapperTestHelper)
.notifyViewAppeared(ArgumentMatchers.any(), ArgumentMatchers.any());
runTaskAndVerifyCallbackWithException(createContentCapturedTask(),
toIntArray(PlatformAPIWrapperTestHelper.CREATE_CONTENT_CAPTURE_SESSION,
PlatformAPIWrapperTestHelper.NEW_AUTOFILL_ID,
PlatformAPIWrapperTestHelper.NEW_VIRTUAL_VIEW_STRUCTURE));
}
@Test
public void testNotifyViewTextChangedException() throws Throwable {
PlatformAPIWrapperTestHelper mockedApiWrapperTestHelper =
Mockito.spy(mPlatformAPIWrapperTestHelper);
PlatformAPIWrapper.setPlatformAPIWrapperImplForTesting(mockedApiWrapperTestHelper);
doThrow(createMainContentCaptureSessionException())
.when(mockedApiWrapperTestHelper)
.notifyViewTextChanged(
ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any());
runContentCapturedTask();
// Modifies child2
runTaskAndVerifyCallbackWithException(createChild2ContentUpdateTask(),
toIntArray(PlatformAPIWrapperTestHelper.NEW_AUTOFILL_ID));
}
@Test
public void testNotifyViewsDisappearedException() throws Throwable {
PlatformAPIWrapperTestHelper mockedApiWrapperTestHelper =
Mockito.spy(mPlatformAPIWrapperTestHelper);
PlatformAPIWrapper.setPlatformAPIWrapperImplForTesting(mockedApiWrapperTestHelper);
doThrow(createMainContentCaptureSessionException())
.when(mockedApiWrapperTestHelper)
.notifyViewsDisappeared(
ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any());
runContentCapturedTask();
// Modifies child2
runTaskAndVerifyCallbackWithException(createRemoveChildrenTask(), toIntArray());
}
@Test
public void testDestroyContentCaptureSessionException() throws Throwable {
PlatformAPIWrapperTestHelper mockedApiWrapperTestHelper =
Mockito.spy(mPlatformAPIWrapperTestHelper);
PlatformAPIWrapper.setPlatformAPIWrapperImplForTesting(mockedApiWrapperTestHelper);
doThrow(createMainContentCaptureSessionException())
.when(mockedApiWrapperTestHelper)
.destroyContentCaptureSession(ArgumentMatchers.any());
runContentCapturedTask();
// Modifies child2
runTaskAndVerifyCallbackWithException(createSessionRemovedTask(), toIntArray());
}
@Test
public void testNotifyViewDisappearedException() throws Throwable {
PlatformAPIWrapperTestHelper mockedApiWrapperTestHelper =
Mockito.spy(mPlatformAPIWrapperTestHelper);
PlatformAPIWrapper.setPlatformAPIWrapperImplForTesting(mockedApiWrapperTestHelper);
doThrow(createMainContentCaptureSessionException())
.when(mockedApiWrapperTestHelper)
.notifyViewDisappeared(ArgumentMatchers.any(), ArgumentMatchers.any());
runContentCapturedTask();
// Modifies child2
runTaskAndVerifyCallbackWithException(createSessionRemovedTask(),
toIntArray(PlatformAPIWrapperTestHelper.DESTROY_CONTENT_CAPTURE_SESSION));
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment