Merge "Use turn_slow_filters_off method to ensure frame rate can be test smoothly" into pie-cts-dev
diff --git a/apps/CameraITS/pymodules/its/caps.py b/apps/CameraITS/pymodules/its/caps.py
index 61ec7e1..d75532b 100644
--- a/apps/CameraITS/pymodules/its/caps.py
+++ b/apps/CameraITS/pymodules/its/caps.py
@@ -513,6 +513,19 @@
return False
+def backward_compatible(props):
+ """Returns whether a device supports BACKWARD_COMPATIBLE.
+
+ Args:
+ props: Camera properties object.
+
+ Returns:
+ Boolean.
+ """
+ return props.has_key("android.request.availableCapabilities") and \
+ 0 in props["android.request.availableCapabilities"]
+
+
class __UnitTest(unittest.TestCase):
"""Run a suite of unit tests on this module.
"""
diff --git a/apps/CameraITS/tests/scene0/test_burst_capture.py b/apps/CameraITS/tests/scene0/test_burst_capture.py
index f915a6a..c573584 100644
--- a/apps/CameraITS/tests/scene0/test_burst_capture.py
+++ b/apps/CameraITS/tests/scene0/test_burst_capture.py
@@ -12,13 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import its.image
-import its.device
-import its.objects
import os.path
+import its.caps
+import its.device
+import its.image
+import its.objects
+
+
def main():
"""Test capture a burst of full size images is fast enough to not timeout.
+
This test verify that entire capture pipeline can keep up the speed
of fullsize capture + CPU read for at least some time.
"""
@@ -27,6 +31,7 @@
with its.device.ItsSession() as cam:
props = cam.get_camera_properties()
+ its.caps.skip_unless(its.caps.backward_compatible(props))
req = its.objects.auto_capture_request()
caps = cam.do_capture([req]*NUM_TEST_FRAMES)
diff --git a/apps/CameraITS/tests/scene0/test_camera_properties.py b/apps/CameraITS/tests/scene0/test_camera_properties.py
index eb638f0..dbd528d 100644
--- a/apps/CameraITS/tests/scene0/test_camera_properties.py
+++ b/apps/CameraITS/tests/scene0/test_camera_properties.py
@@ -26,8 +26,6 @@
pprint.pprint(props)
- its.caps.skip_unless(its.caps.manual_sensor(props))
-
# Test that a handful of required keys are present.
assert(props.has_key('android.sensor.info.sensitivityRange'))
assert(props.has_key('android.sensor.orientation'))
diff --git a/apps/CameraITS/tests/scene0/test_metadata.py b/apps/CameraITS/tests/scene0/test_metadata.py
index 48ce28e..b8949b1 100644
--- a/apps/CameraITS/tests/scene0/test_metadata.py
+++ b/apps/CameraITS/tests/scene0/test_metadata.py
@@ -31,6 +31,7 @@
# Arbitrary capture request exposure values; image content is not
# important for this test, only the metadata.
props = cam.get_camera_properties()
+ its.caps.skip_unless(its.caps.backward_compatible(props))
auto_req = its.objects.auto_capture_request()
cap = cam.do_capture(auto_req)
md = cap["metadata"]
diff --git a/apps/CameraITS/tests/scene0/test_unified_timestamps.py b/apps/CameraITS/tests/scene0/test_unified_timestamps.py
index ae4583f..5a9228e 100644
--- a/apps/CameraITS/tests/scene0/test_unified_timestamps.py
+++ b/apps/CameraITS/tests/scene0/test_unified_timestamps.py
@@ -25,7 +25,8 @@
props = cam.get_camera_properties()
# Only run test if the appropriate caps are claimed.
- its.caps.skip_unless(its.caps.sensor_fusion(props))
+ its.caps.skip_unless(its.caps.sensor_fusion(props) and
+ its.caps.backward_compatible(props))
# Get the timestamp of a captured image.
if its.caps.manual_sensor(props):
diff --git a/apps/CameraITS/tests/scene4/test_aspect_ratio_and_crop.py b/apps/CameraITS/tests/scene4/test_aspect_ratio_and_crop.py
index 92dfd0d..a46d54c 100644
--- a/apps/CameraITS/tests/scene4/test_aspect_ratio_and_crop.py
+++ b/apps/CameraITS/tests/scene4/test_aspect_ratio_and_crop.py
@@ -334,8 +334,8 @@
h_iter = size_iter[1]
# Skip testing same format/size combination
# ITS does not handle that properly now
- if (dual_target and w_iter == size_cmpr[0]
- and h_iter == size_cmpr[1]
+ if (dual_target
+ and w_iter*h_iter == size_cmpr[0]*size_cmpr[1]
and fmt_iter == fmt_cmpr):
continue
out_surface = [{"width": w_iter,
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index b605cb4..7d72e2d 100755
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -1304,9 +1304,8 @@
<string name="no_camera_manager">
No camera manager exists! This test device is in a bad state.
</string>
- <string name="all_legacy_devices">
- All cameras on this device are LEGACY mode only - ITS tests are only required on LIMITED
- or better devices. Pass.
+ <string name="all_exempted_devices">
+ All cameras on this device are exempted from ITS - Pass.
</string>
<string name="its_test_passed">All Camera ITS tests passed. Pass button enabled!</string>
<string name="its_test_failed">Some Camera ITS tests failed.</string>
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
index db45452..fe1c0ed 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
@@ -346,17 +346,16 @@
}
}
- public void openCameraDevice(int cameraId) throws ItsException {
- Logt.i(TAG, String.format("Opening camera %d", cameraId));
+ public void openCameraDevice(String cameraId) throws ItsException {
+ Logt.i(TAG, String.format("Opening camera %s", cameraId));
- String[] devices;
try {
- devices = mCameraManager.getCameraIdList();
- if (devices == null || devices.length == 0) {
- throw new ItsException("No camera devices");
- }
if (mMemoryQuota == -1) {
// Initialize memory quota on this device
+ List<String> devices = ItsUtils.getItsCompatibleCameraIds(mCameraManager);
+ if (devices.size() == 0) {
+ throw new ItsException("No camera devices");
+ }
for (String camId : devices) {
CameraCharacteristics chars = mCameraManager.getCameraCharacteristics(camId);
Size maxYuvSize = ItsUtils.getMaxOutputSize(
@@ -373,10 +372,8 @@
}
try {
- mCamera = mBlockingCameraManager.openCamera(devices[cameraId],
- mCameraListener, mCameraHandler);
- mCameraCharacteristics = mCameraManager.getCameraCharacteristics(
- devices[cameraId]);
+ mCamera = mBlockingCameraManager.openCamera(cameraId, mCameraListener, mCameraHandler);
+ mCameraCharacteristics = mCameraManager.getCameraCharacteristics(cameraId);
mSocketQueueQuota = new Semaphore(mMemoryQuota, true);
} catch (CameraAccessException e) {
throw new ItsException("Failed to open camera", e);
@@ -646,7 +643,7 @@
JSONObject cmdObj = new JSONObject(cmd);
Logt.i(TAG, "Start processing command" + cmdObj.getString("cmdName"));
if ("open".equals(cmdObj.getString("cmdName"))) {
- int cameraId = cmdObj.getInt("cameraId");
+ String cameraId = cmdObj.getString("cameraId");
openCameraDevice(cameraId);
} else if ("close".equals(cmdObj.getString("cmdName"))) {
closeCameraDevice();
@@ -901,6 +898,9 @@
private void doGetPropsById(JSONObject params) throws ItsException {
String[] devices;
try {
+ // Intentionally not using ItsUtils.getItsCompatibleCameraIds here so it's possible to
+ // write some simple script to query camera characteristics even for devices exempted
+ // from ITS today.
devices = mCameraManager.getCameraIdList();
if (devices == null || devices.length == 0) {
throw new ItsException("No camera devices");
@@ -927,34 +927,21 @@
}
private void doGetCameraIds() throws ItsException {
- String[] devices;
- try {
- devices = mCameraManager.getCameraIdList();
- if (devices == null || devices.length == 0) {
- throw new ItsException("No camera devices");
- }
- } catch (CameraAccessException e) {
- throw new ItsException("Failed to get device ID list", e);
+ List<String> devices = ItsUtils.getItsCompatibleCameraIds(mCameraManager);
+ if (devices.size() == 0) {
+ throw new ItsException("No camera devices");
}
try {
JSONObject obj = new JSONObject();
JSONArray array = new JSONArray();
for (String id : devices) {
- CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(id);
- // Only supply camera Id for non-legacy cameras since legacy camera does not
- // support ITS
- if (characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) !=
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
- array.put(id);
- }
+ array.put(id);
}
obj.put("cameraIdArray", array);
mSocketRunnableObj.sendResponse("cameraIds", obj);
} catch (org.json.JSONException e) {
throw new ItsException("JSON error: ", e);
- } catch (android.hardware.camera2.CameraAccessException e) {
- throw new ItsException("Access error: ", e);
}
}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsTestActivity.java
index d6feb51..fd62ed2 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsTestActivity.java
@@ -21,7 +21,6 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
-import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.os.Bundle;
@@ -80,7 +79,7 @@
private boolean mReceiverRegistered = false;
// Initialized in onCreate
- ArrayList<String> mToBeTestedCameraIds = null;
+ List<String> mToBeTestedCameraIds = null;
// Scenes
private static final ArrayList<String> mSceneIds = new ArrayList<String> () { {
@@ -333,32 +332,20 @@
// Hide the test if all camera devices are legacy
CameraManager manager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE);
try {
- String[] cameraIds = manager.getCameraIdList();
- mToBeTestedCameraIds = new ArrayList<String>();
- for (String id : cameraIds) {
- CameraCharacteristics characteristics = manager.getCameraCharacteristics(id);
- int hwLevel = characteristics.get(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
- if (hwLevel
- != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY &&
- hwLevel
- != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL) {
- mToBeTestedCameraIds.add(id);
- }
- }
- if (mToBeTestedCameraIds.size() == 0) {
- showToast(R.string.all_legacy_devices);
- ItsTestActivity.this.getReportLog().setSummary(
- "PASS: all cameras on this device are LEGACY or EXTERNAL"
- , 1.0, ResultType.NEUTRAL, ResultUnit.NONE);
- setTestResultAndFinish(true);
- }
- } catch (CameraAccessException e) {
+ mToBeTestedCameraIds = ItsUtils.getItsCompatibleCameraIds(manager);
+ } catch (ItsException e) {
Toast.makeText(ItsTestActivity.this,
"Received error from camera service while checking device capabilities: "
+ e, Toast.LENGTH_SHORT).show();
}
+ if (mToBeTestedCameraIds.size() == 0) {
+ showToast(R.string.all_exempted_devices);
+ ItsTestActivity.this.getReportLog().setSummary(
+ "PASS: all cameras on this device are exempted from ITS"
+ , 1.0, ResultType.NEUTRAL, ResultUnit.NONE);
+ setTestResultAndFinish(true);
+ }
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsUtils.java b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsUtils.java
index 65e4970..41ae288 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsUtils.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsUtils.java
@@ -19,8 +19,10 @@
import android.content.Context;
import android.graphics.ImageFormat;
import android.graphics.Rect;
+import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.params.MeteringRectangle;
@@ -296,4 +298,47 @@
return false;
}
}
+
+ public static List<String> getItsCompatibleCameraIds(CameraManager manager)
+ throws ItsException {
+ if (manager == null) {
+ throw new IllegalArgumentException("CameraManager is null");
+ }
+
+ ArrayList<String> outList = new ArrayList<String>();
+ try {
+ String[] cameraIds = manager.getCameraIdList();
+ for (String id : cameraIds) {
+ CameraCharacteristics characteristics = manager.getCameraCharacteristics(id);
+ int[] actualCapabilities = characteristics.get(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
+ boolean haveBC = false;
+ final int BACKWARD_COMPAT =
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE;
+ for (int capability : actualCapabilities) {
+ if (capability == BACKWARD_COMPAT) {
+ haveBC = true;
+ break;
+ }
+ }
+
+ // Skip devices that does not support BACKWARD_COMPATIBLE capability
+ if (!haveBC) continue;
+
+ int hwLevel = characteristics.get(
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
+ if (hwLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY ||
+ hwLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL) {
+ // Skip LEGACY and EXTERNAL devices
+ continue;
+ }
+ outList.add(id);
+ }
+ } catch (CameraAccessException e) {
+ Logt.e(TAG,
+ "Received error from camera service while checking device capabilities: " + e);
+ throw new ItsException("Failed to get device ID list", e);
+ }
+ return outList;
+ }
}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/MotionIndicatorView.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/MotionIndicatorView.java
index 14784dd..4160572 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/MotionIndicatorView.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/MotionIndicatorView.java
@@ -26,6 +26,7 @@
import android.hardware.SensorManager;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.Surface;
import android.view.View;
/**
@@ -89,6 +90,8 @@
private boolean mXEnabled, mYEnabled, mZEnabled;
+ private boolean mIsDeviceRotated = false;
+
/**
* Constructor
* @param context
@@ -146,6 +149,15 @@
}
/**
+ * Set the device's current rotation
+ * @param rotation Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, or
+ * Surface.ROTATION_270
+ */
+ public void setDeviceRotation(int rotation) {
+ mIsDeviceRotated = (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270);
+ }
+
+ /**
* Set the active axis for display
*
* @param axis AXIS_X, AXIS_Y, AXIS_Z for x, y, z axis indicators, or AXIS_ALL for all three.
@@ -181,16 +193,22 @@
mXSize = w;
mYSize = h;
- mZBoundOut = new RectF(w/2-w/2.5f, h/2-w/2.5f, w/2+w/2.5f, h/2+w/2.5f);
+ float halfSideLength = 0.4f * Math.min(w, h);
+ float leftSide = w/2 - halfSideLength;
+ float topSide = h/2 - halfSideLength;
+ float rightSide = w/2 + halfSideLength;
+ float bottomSide = h/2 + halfSideLength;
+
+ mZBoundOut = new RectF(leftSide, topSide, rightSide, bottomSide);
mZBoundOut2 = new RectF(
- w/2-w/2.5f-ZRING_CURSOR_ADD, h/2-w/2.5f-ZRING_CURSOR_ADD,
- w/2+w/2.5f+ZRING_CURSOR_ADD, h/2+w/2.5f+ZRING_CURSOR_ADD);
+ leftSide-ZRING_CURSOR_ADD, topSide-ZRING_CURSOR_ADD,
+ rightSide+ZRING_CURSOR_ADD, bottomSide+ZRING_CURSOR_ADD);
mZBoundIn = new RectF(
- w/2-w/2.5f+ZRING_WIDTH, h/2-w/2.5f+ZRING_WIDTH,
- w/2+w/2.5f-ZRING_WIDTH, h/2+w/2.5f-ZRING_WIDTH);
+ leftSide+ZRING_WIDTH, topSide+ZRING_WIDTH,
+ rightSide-ZRING_WIDTH, bottomSide-ZRING_WIDTH);
mZBoundIn2 = new RectF(
- w/2-w/2.5f+ZRING_WIDTH+ZRING_CURSOR_ADD, h/2-w/2.5f+ZRING_WIDTH+ZRING_CURSOR_ADD,
- w/2+w/2.5f-ZRING_WIDTH-ZRING_CURSOR_ADD, h/2+w/2.5f-ZRING_WIDTH-ZRING_CURSOR_ADD);
+ leftSide+ZRING_WIDTH+ZRING_CURSOR_ADD, topSide+ZRING_WIDTH+ZRING_CURSOR_ADD,
+ rightSide-ZRING_WIDTH-ZRING_CURSOR_ADD, bottomSide-ZRING_WIDTH-ZRING_CURSOR_ADD);
if (LOCAL_LOGV) Log.v(TAG, "New view size = ("+w+", "+h+")");
}
@@ -209,8 +227,14 @@
p.setColor(Color.YELLOW);
canvas.drawRect(10,10, 50, 50, p);
- if (mXEnabled && mXCovered != null) {
- int xNStep = mXCovered.getNSteps() + 4; // two on each side as a buffer
+ // In order to determine which progress bar to draw, the device's rotation must be accounted
+ // for since the accelerometer rotates with the display.
+ boolean drawX = (mXEnabled && !mIsDeviceRotated) || (mYEnabled && mIsDeviceRotated);
+ boolean drawY = (mYEnabled && !mIsDeviceRotated) || (mXEnabled && mIsDeviceRotated);
+
+ if (drawX && mXCovered != null) {
+ RangeCoveredRegister covered = mIsDeviceRotated ? mYCovered : mXCovered;
+ int xNStep = covered.getNSteps() + 4; // two on each side as a buffer
int xStepSize = mXSize * 3/4 / xNStep;
int xLeft = mXSize * 1/8 + (mXSize * 3/4 % xNStep)/2;
@@ -219,8 +243,8 @@
xLeft+xStepSize*xNStep-1, XBAR_WIDTH+XBAR_MARGIN, mRangePaint);
// covered range
- for (i=0; i<mXCovered.getNSteps(); ++i) {
- if (mXCovered.isCovered(i)) {
+ for (i=0; i<covered.getNSteps(); ++i) {
+ if (covered.isCovered(i)) {
canvas.drawRect(
xLeft+xStepSize*(i+2), XBAR_MARGIN,
xLeft+xStepSize*(i+3)-1, XBAR_WIDTH + XBAR_MARGIN,
@@ -235,12 +259,14 @@
xLeft+xStepSize*(xNStep-2)+3, XBAR_WIDTH+XBAR_MARGIN, mLimitPaint);
// cursor
- t = (int)(xLeft+xStepSize*(mXCovered.getLastValue()+2));
+ t = (int)(xLeft+xStepSize*(covered.getLastValue()+2));
canvas.drawRect(t-4, XBAR_MARGIN-XBAR_CURSOR_ADD, t+3,
XBAR_WIDTH+XBAR_MARGIN+XBAR_CURSOR_ADD, mCursorPaint);
}
- if (mYEnabled && mYCovered != null) {
- int yNStep = mYCovered.getNSteps() + 4; // two on each side as a buffer
+
+ if (drawY && mYCovered != null) {
+ RangeCoveredRegister covered = mIsDeviceRotated ? mXCovered : mYCovered;
+ int yNStep = covered.getNSteps() + 4; // two on each side as a buffer
int yStepSize = mYSize * 3/4 / yNStep;
int yLeft = mYSize * 1/8 + (mYSize * 3/4 % yNStep)/2;
@@ -249,8 +275,8 @@
YBAR_WIDTH+YBAR_MARGIN, yLeft+yStepSize*yNStep-1, mRangePaint);
// covered range
- for (i=0; i<mYCovered.getNSteps(); ++i) {
- if (mYCovered.isCovered(i)) {
+ for (i=0; i<covered.getNSteps(); ++i) {
+ if (covered.isCovered(i)) {
canvas.drawRect(
YBAR_MARGIN, yLeft+yStepSize*(i+2),
YBAR_WIDTH + YBAR_MARGIN, yLeft+yStepSize*(i+3)-1,
@@ -265,7 +291,7 @@
YBAR_WIDTH + YBAR_MARGIN, yLeft + yStepSize * (yNStep - 2) + 3, mLimitPaint);
// cursor
- t = (int)(yLeft+yStepSize*(mYCovered.getLastValue()+2));
+ t = (int)(yLeft+yStepSize*(covered.getLastValue()+2));
canvas.drawRect( YBAR_MARGIN-YBAR_CURSOR_ADD, t-4,
YBAR_WIDTH+YBAR_MARGIN+YBAR_CURSOR_ADD, t+3, mCursorPaint);
}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/RVCVCameraPreview.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/RVCVCameraPreview.java
index bd463af..10d1865 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/RVCVCameraPreview.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/RVCVCameraPreview.java
@@ -21,9 +21,11 @@
import android.hardware.Camera;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;
+import android.view.WindowManager;
import java.io.IOException;
import java.lang.Math;
@@ -36,10 +38,11 @@
private static final String TAG = "RVCVCameraPreview";
private static final boolean LOCAL_LOGD = true;
+ private Context mContext = null;
private SurfaceHolder mHolder;
private Camera mCamera;
- private float mAspect;
- private int mRotation;
+ private float mCameraAspectRatio = 0;
+ private int mCameraRotation = 0;
private boolean mCheckStartTest = false;
private boolean mPreviewStarted = false;
@@ -51,6 +54,7 @@
*/
public RVCVCameraPreview(Context context) {
super(context);
+ mContext = context;
mCamera = null;
initSurface();
}
@@ -62,12 +66,13 @@
*/
public RVCVCameraPreview(Context context, AttributeSet attrs) {
super(context, attrs);
+ mContext = context;
}
public void init(Camera camera, float aspectRatio, int rotation) {
this.mCamera = camera;
- mAspect = aspectRatio;
- mRotation = rotation;
+ mCameraAspectRatio = aspectRatio;
+ mCameraRotation = rotation;
initSurface();
}
@@ -111,7 +116,11 @@
// preview surface or camera does not exist
return;
}
- if (adjustLayoutParamsIfNeeded()) {
+
+ int totalRotation = getRequiredRotation();
+ mCamera.setDisplayOrientation(totalRotation);
+
+ if (adjustLayoutParamsIfNeeded(totalRotation)) {
// Wait on next surfaceChanged() call before proceeding
Log.d(TAG, "Waiting on surface change before starting preview");
return;
@@ -127,7 +136,6 @@
}
mCheckStartTest = false;
- mCamera.setDisplayOrientation(mRotation);
try {
mCamera.setPreviewDisplay(holder);
mCamera.startPreview();
@@ -142,23 +150,70 @@
}
/**
+ * Determine the rotation required to display the camera's preview on the screen as large as
+ * possible. This function combines the device's current rotation from its default orientation
+ * and the rotation of the camera.
+ */
+ private int getRequiredRotation() {
+ WindowManager windowManager =
+ (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
+ int deviceRotation = 0;
+ if (windowManager != null) {
+ switch (windowManager.getDefaultDisplay().getRotation()) {
+ case Surface.ROTATION_0:
+ deviceRotation = 0;
+ break;
+ case Surface.ROTATION_90:
+ deviceRotation = 270;
+ break;
+ case Surface.ROTATION_180:
+ deviceRotation = 180;
+ break;
+ case Surface.ROTATION_270:
+ deviceRotation = 90;
+ break;
+ default:
+ deviceRotation = 0;
+ break;
+ }
+ } else {
+ Log.w(TAG, "Unable to get device rotation, preview may be skewed.");
+ }
+
+ return (mCameraRotation + deviceRotation) % 360;
+ }
+
+ /**
* Resize the layout to more closely match the desired aspect ratio, if necessary.
*
* @return true if we updated the layout params, false if the params look good
*/
- private boolean adjustLayoutParamsIfNeeded() {
+ private boolean adjustLayoutParamsIfNeeded(int totalRotation) {
+ // Determine the maximum size layout that maintains the camera's preview aspect ratio
+ float cameraAspect = mCameraAspectRatio;
+
+ // Check the camera and device rotation and invert the aspect ratio if the device is not
+ // rotated at 0 or 180 degrees.
+ if (totalRotation % 180 != 0) {
+ // The device is rotated, so the screen should be the inverse of the aspect ratio
+ cameraAspect = 1.0f / mCameraAspectRatio;
+ }
+
+ // Only adjust if there is at least 1% error between the aspects
ViewGroup.LayoutParams layoutParams = getLayoutParams();
int curWidth = getWidth();
int curHeight = getHeight();
- float curAspect = (float)curHeight / (float)curWidth;
- float aspectDelta = Math.abs(mAspect - curAspect);
- if ((aspectDelta / mAspect) >= 0.01) {
- if (curAspect > mAspect) {
- layoutParams.height = (int)Math.round(curWidth * mAspect);
+ float curAspect = (float)curWidth / (float)curHeight;
+ float aspectDelta = Math.abs(cameraAspect - curAspect);
+ if ((aspectDelta / cameraAspect) >= 0.01) {
+ if (cameraAspect > curAspect) {
+ // Camera preview is wider than the current layout. Need to shorten the current layout
layoutParams.width = curWidth;
+ layoutParams.height = (int)(curWidth / cameraAspect);
} else {
+ // Camera preview taller than the current layout. Need to narrow the current layout
+ layoutParams.width = (int)(curHeight * cameraAspect);
layoutParams.height = curHeight;
- layoutParams.width = (int)Math.round(curHeight / mAspect);
}
if (layoutParams.height != curHeight || layoutParams.width != curWidth) {
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/RVCVRecordActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/RVCVRecordActivity.java
index 8199736..4f92b64 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/RVCVRecordActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/RVCVRecordActivity.java
@@ -33,7 +33,9 @@
import android.os.Environment;
import android.util.JsonWriter;
import android.util.Log;
+import android.view.Surface;
import android.view.Window;
+import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.Toast;
@@ -70,6 +72,7 @@
private RVSensorLogger mRVSensorLogger;
private CoverageManager mCoverManager;
private CameraContext mCameraContext;
+ private int mDeviceRotation = Surface.ROTATION_0;
public static final int AXIS_NONE = 0;
public static final int AXIS_ALL = SensorManager.AXIS_X +
@@ -119,6 +122,12 @@
// locate views
mIndicatorView = (MotionIndicatorView) findViewById(R.id.cam_indicator);
+ WindowManager windowManager =
+ (WindowManager)getSystemService(Context.WINDOW_SERVICE);
+ if (windowManager != null) {
+ mDeviceRotation = windowManager.getDefaultDisplay().getRotation();
+ mIndicatorView.setDeviceRotation(mDeviceRotation);
+ }
initStoragePath();
}
@@ -224,6 +233,9 @@
if (axis >=SensorManager.AXIS_X && axis <=SensorManager.AXIS_Z) {
imageView.setImageResource(prompts[axis-1]);
+ if (mDeviceRotation != Surface.ROTATION_0 && mDeviceRotation != Surface.ROTATION_180) {
+ imageView.setRotation(90);
+ }
mIndicatorView.enableAxis(axis);
mRVSensorLogger.updateRegister(mCoverManager.getAxis(axis), axis);
notifyPrompt(axis);
diff --git a/common/device-side/util/src/com/android/compatibility/common/util/BlockingBroadcastReceiver.java b/common/device-side/util/src/com/android/compatibility/common/util/BlockingBroadcastReceiver.java
old mode 100644
new mode 100755
index 301a626..c26ddd0
--- a/common/device-side/util/src/com/android/compatibility/common/util/BlockingBroadcastReceiver.java
+++ b/common/device-side/util/src/com/android/compatibility/common/util/BlockingBroadcastReceiver.java
@@ -43,7 +43,7 @@
public class BlockingBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "BlockingBroadcast";
- private static final int DEFAULT_TIMEOUT_SECONDS = 10;
+ private static final int DEFAULT_TIMEOUT_SECONDS = 30;
private final BlockingQueue<Intent> mBlockingQueue;
private final String mExpectedAction;
@@ -68,7 +68,7 @@
/**
* Wait until the broadcast and return the received broadcast intent. {@code null} is returned
- * if no broadcast with expected action is received within 10 seconds.
+ * if no broadcast with expected action is received within 30 seconds.
*/
public @Nullable Intent awaitForBroadcast() {
try {
diff --git a/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/DocumentsClientTestCase.java b/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/DocumentsClientTestCase.java
index d2eea34..afcaae0 100644
--- a/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/DocumentsClientTestCase.java
+++ b/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/DocumentsClientTestCase.java
@@ -146,7 +146,8 @@
protected boolean supportedHardwareForScopedDirectoryAccess() {
final PackageManager pm = getInstrumentation().getContext().getPackageManager();
- if (pm.hasSystemFeature("android.hardware.type.watch")) {
+ if (pm.hasSystemFeature("android.hardware.type.watch")
+ || pm.hasSystemFeature("android.hardware.type.automotive")) {
return false;
}
return true;
diff --git a/hostsidetests/appsecurity/test-apps/UsePermissionApp23/src/com/android/cts/usepermission/BasePermissionsTest.java b/hostsidetests/appsecurity/test-apps/UsePermissionApp23/src/com/android/cts/usepermission/BasePermissionsTest.java
index 62a05c8..a54b9b5 100755
--- a/hostsidetests/appsecurity/test-apps/UsePermissionApp23/src/com/android/cts/usepermission/BasePermissionsTest.java
+++ b/hostsidetests/appsecurity/test-apps/UsePermissionApp23/src/com/android/cts/usepermission/BasePermissionsTest.java
@@ -64,8 +64,8 @@
public abstract class BasePermissionsTest {
private static final String PLATFORM_PACKAGE_NAME = "android";
- private static final long IDLE_TIMEOUT_MILLIS = 500;
- private static final long GLOBAL_TIMEOUT_MILLIS = 5000;
+ private static final long IDLE_TIMEOUT_MILLIS = 1000;
+ private static final long GLOBAL_TIMEOUT_MILLIS = 10000;
private static final long RETRY_TIMEOUT = 3 * GLOBAL_TIMEOUT_MILLIS;
private static final String LOG_TAG = "BasePermissionsTest";
@@ -557,7 +557,9 @@
throws Exception {
AccessibilityNodeInfo result = current;
while (result != null) {
- if (result.getCollectionItemInfo() != null) {
+ // Nodes that are in the hierarchy but not yet on screen may not have collection item
+ // info populated. Use a parent with collection info as an indicator in those cases.
+ if (result.getCollectionItemInfo() != null || hasCollectionAsParent(result)) {
return result;
}
result = result.getParent();
@@ -565,6 +567,10 @@
return null;
}
+ private static boolean hasCollectionAsParent(AccessibilityNodeInfo node) {
+ return node.getParent() != null && node.getParent().getCollectionInfo() != null;
+ }
+
private static AccessibilityNodeInfo findSwitch(AccessibilityNodeInfo root) throws Exception {
if (Switch.class.getName().equals(root.getClassName().toString())) {
return root;
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/PrimaryUserAdminHelper.java b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/PrimaryUserAdminHelper.java
index e092888..f20edcc 100644
--- a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/PrimaryUserAdminHelper.java
+++ b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/PrimaryUserAdminHelper.java
@@ -41,8 +41,8 @@
ComponentName cn = PrimaryUserDeviceAdmin.ADMIN_RECEIVER_COMPONENT;
if (mDpm.isAdminActive(cn)) {
mDpm.removeActiveAdmin(cn);
- // Wait until device admin is not active (with 2 minutes timeout).
- for (int i = 0; i < 2 * 60 && mDpm.isAdminActive(cn); i++) {
+ // Wait until device admin is not active (with 5 minutes timeout).
+ for (int i = 0; i < 5 * 60 && mDpm.isAdminActive(cn); i++) {
Thread.sleep(1000); // 1 second.
}
}
diff --git a/hostsidetests/net/app/src/com/android/cts/net/hostside/DataSaverModeTest.java b/hostsidetests/net/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
index 599a31c..c3962fb 100644
--- a/hostsidetests/net/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
+++ b/hostsidetests/net/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
@@ -22,6 +22,9 @@
import android.util.Log;
+import com.android.compatibility.common.util.CddTest;
+
+@CddTest(requirement="7.4.7/C-1-1,H-1-1,C-2-1")
public class DataSaverModeTest extends AbstractRestrictBackgroundNetworkTestCase {
private static final String[] REQUIRED_WHITELISTED_PACKAGES = {
diff --git a/hostsidetests/numberblocking/app/Android.mk b/hostsidetests/numberblocking/app/Android.mk
index 08bf132..a84d11c 100644
--- a/hostsidetests/numberblocking/app/Android.mk
+++ b/hostsidetests/numberblocking/app/Android.mk
@@ -27,7 +27,7 @@
LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_STATIC_JAVA_LIBRARIES := ctstestrunner-axt androidx.test.rules
+LOCAL_STATIC_JAVA_LIBRARIES := ctstestrunner-axt androidx.test.rules compatibility-device-util
LOCAL_JAVA_LIBRARIES := android.test.base.stubs
diff --git a/hostsidetests/numberblocking/app/src/com/android/cts/numberblocking/hostside/CallBlockingTest.java b/hostsidetests/numberblocking/app/src/com/android/cts/numberblocking/hostside/CallBlockingTest.java
index 674dab2..c23ba84 100644
--- a/hostsidetests/numberblocking/app/src/com/android/cts/numberblocking/hostside/CallBlockingTest.java
+++ b/hostsidetests/numberblocking/app/src/com/android/cts/numberblocking/hostside/CallBlockingTest.java
@@ -29,6 +29,8 @@
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
+import com.android.compatibility.common.util.CddTest;
+
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -36,6 +38,7 @@
/**
* Tests call blocking in a multi-user environment.
*/
+@CddTest(requirement="7.4.1.1/C-1-1")
public class CallBlockingTest extends BaseNumberBlockingClientTest {
private static final String QUERY_CALL_THROUGH_OUR_CONNECTION_SERVICE = CallLog.Calls.NUMBER
+ " = ? AND " + CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME + " = ?";
@@ -65,6 +68,7 @@
assertNull(mTelecomManager.getPhoneAccount(getPhoneAccountHandle()));
}
+ @CddTest(requirement="7.4.1.1/C-1-3,C-1-4")
public void testIncomingCallFromBlockedNumberIsRejected() throws Exception {
// Make sure no lingering values from previous runs.
cleanupCall(false /* verifyNoCallLogsWritten */);
diff --git a/hostsidetests/numberblocking/src/com/android/cts/numberblocking/hostside/NumberBlockingTest.java b/hostsidetests/numberblocking/src/com/android/cts/numberblocking/hostside/NumberBlockingTest.java
index 1718ab7..6d5c336 100644
--- a/hostsidetests/numberblocking/src/com/android/cts/numberblocking/hostside/NumberBlockingTest.java
+++ b/hostsidetests/numberblocking/src/com/android/cts/numberblocking/hostside/NumberBlockingTest.java
@@ -119,7 +119,7 @@
runTestAsPrimaryUser(CALL_BLOCKING_TEST_CLASS_NAME, "testUnregisterPhoneAccount");
// Run tests as secondary user.
- assertTrue(getDevice().startUser(mSecondaryUserId));
+ startUserAndWait(mSecondaryUserId);
// Ensure that a privileged app cannot block numbers when the current user is a
// secondary user.
@@ -142,6 +142,29 @@
}
}
+ /** Starts user {@code userId} and waits until it is in state RUNNING_UNLOCKED. */
+ protected void startUserAndWait(int userId) throws Exception {
+ getDevice().startUser(userId);
+
+ final String desiredState = "RUNNING_UNLOCKED";
+ final long USER_STATE_TIMEOUT_MS = 60_0000; // 1 minute
+ final long timeout = System.currentTimeMillis() + USER_STATE_TIMEOUT_MS;
+ final String command = String.format("am get-started-user-state %d", userId);
+ String output = "";
+ while (System.currentTimeMillis() <= timeout) {
+ output = getDevice().executeShellCommand(command);
+ if (output.contains(desiredState)) {
+ return;
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ // Do nothing.
+ }
+ }
+ fail("User state of " + userId + " was '" + output + "' rather than " + desiredState);
+ }
+
private void createSecondaryUser() throws Exception {
mSecondaryUserId = getDevice().createUser(SECONDARY_USER_NAME);
getDevice().waitForDeviceAvailable();
diff --git a/hostsidetests/statsd/src/android/cts/statsd/atom/AtomTestCase.java b/hostsidetests/statsd/src/android/cts/statsd/atom/AtomTestCase.java
index 02bc735..657c0a6 100644
--- a/hostsidetests/statsd/src/android/cts/statsd/atom/AtomTestCase.java
+++ b/hostsidetests/statsd/src/android/cts/statsd/atom/AtomTestCase.java
@@ -19,6 +19,7 @@
import static android.cts.statsd.atom.DeviceAtomTestCase.DEVICE_SIDE_TEST_PACKAGE;
import android.os.BatteryStatsProto;
+import android.service.battery.BatteryServiceDumpProto;
import android.service.batterystats.BatteryStatsServiceDumpProto;
import android.view.DisplayStateEnum;
@@ -70,6 +71,7 @@
public static final String UPDATE_CONFIG_CMD = "cmd stats config update";
public static final String DUMP_REPORT_CMD = "cmd stats dump-report";
+ public static final String DUMP_BATTERY_CMD = "dumpsys battery";
public static final String DUMP_BATTERYSTATS_CMD = "dumpsys batterystats";
public static final String REMOVE_CONFIG_CMD = "cmd stats config remove";
public static final String CONFIG_UID = "1000";
@@ -502,6 +504,14 @@
}
protected void unplugDevice() throws Exception {
+ // On batteryless devices on Android P or above, the 'unplug' command
+ // alone does not simulate the really unplugged state.
+ //
+ // This is because charging state is left as "unknown". Unless a valid
+ // state like 3 = BatteryManager.BATTERY_STATUS_DISCHARGING is set,
+ // framework does not consider the device as running on battery.
+ setChargingState(3);
+
getDevice().executeShellCommand("cmd battery unplug");
}
@@ -590,7 +600,7 @@
}
protected void turnBatterySaverOn() throws Exception {
- getDevice().executeShellCommand("cmd battery unplug");
+ unplugDevice();
getDevice().executeShellCommand("settings put global low_power 1");
}
@@ -655,6 +665,21 @@
}
/**
+ * Determines if the device has a battery.
+ */
+ protected boolean hasBattery() throws Exception {
+ try {
+ BatteryServiceDumpProto batteryProto = getDump(BatteryServiceDumpProto.parser(),
+ String.join(" ", DUMP_BATTERY_CMD, "--proto"));
+ LogUtil.CLog.d("Got battery service dump:\n " + batteryProto.toString());
+ return batteryProto.getIsPresent();
+ } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+ LogUtil.CLog.e("Failed to dump batteryservice proto");
+ throw (e);
+ }
+ }
+
+ /**
* Determines if the device has |file|.
*/
protected boolean doesFileExist(String file) throws Exception {
diff --git a/hostsidetests/statsd/src/android/cts/statsd/atom/HostAtomTests.java b/hostsidetests/statsd/src/android/cts/statsd/atom/HostAtomTests.java
index cc3b47c..34adcd1 100644
--- a/hostsidetests/statsd/src/android/cts/statsd/atom/HostAtomTests.java
+++ b/hostsidetests/statsd/src/android/cts/statsd/atom/HostAtomTests.java
@@ -338,6 +338,7 @@
return;
}
if (!hasFeature(FEATURE_WATCH, false)) return;
+ if (!hasBattery()) return;
StatsdConfig.Builder config = getPulledConfig();
FieldMatcher.Builder dimension = FieldMatcher.newBuilder()
.setField(Atom.REMAINING_BATTERY_CAPACITY_FIELD_NUMBER)
@@ -365,6 +366,7 @@
return;
}
if (!hasFeature(FEATURE_WATCH, false)) return;
+ if (!hasBattery()) return;
StatsdConfig.Builder config = getPulledConfig();
FieldMatcher.Builder dimension = FieldMatcher.newBuilder()
.setField(Atom.FULL_BATTERY_CAPACITY_FIELD_NUMBER)
diff --git a/hostsidetests/usb/src/com/android/cts/usb/TestUsbTest.java b/hostsidetests/usb/src/com/android/cts/usb/TestUsbTest.java
index 01b5d88..9df2d5c 100644
--- a/hostsidetests/usb/src/com/android/cts/usb/TestUsbTest.java
+++ b/hostsidetests/usb/src/com/android/cts/usb/TestUsbTest.java
@@ -36,6 +36,8 @@
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.RunUtil;
+import com.android.compatibility.common.util.CddTest;
+
import java.io.File;
import java.io.FileNotFoundException;
import java.util.regex.Matcher;
@@ -129,6 +131,7 @@
* Check if adb serial number, USB serial number, ro.serialno, and android.os.Build.SERIAL
* all matches and meets the format requirement [a-zA-Z0-9]{6,20}
*/
+ @CddTest(requirement="7.7.1/C-1-2")
@AppModeFull(reason = "serial can not be read by instant apps")
public void testUsbSerialReadOnDeviceMatches() throws Exception {
installApp(false);
diff --git a/tests/AlarmManager/src/android/alarmmanager/cts/AppStandbyTests.java b/tests/AlarmManager/src/android/alarmmanager/cts/AppStandbyTests.java
index 8d8adb1..8838492 100644
--- a/tests/AlarmManager/src/android/alarmmanager/cts/AppStandbyTests.java
+++ b/tests/AlarmManager/src/android/alarmmanager/cts/AppStandbyTests.java
@@ -299,9 +299,12 @@
private void setBatteryCharging(final boolean charging) throws Exception {
final BatteryManager bm = mContext.getSystemService(BatteryManager.class);
- final String cmd = "dumpsys battery " + (charging ? "reset" : "unplug");
- executeAndLog(cmd);
- if (!charging) {
+ if (charging) {
+ executeAndLog("dumpsys battery reset");
+ } else {
+ executeAndLog("dumpsys battery unplug");
+ executeAndLog("dumpsys battery set status " +
+ BatteryManager.BATTERY_STATUS_DISCHARGING);
assertTrue("Battery could not be unplugged", waitUntil(() -> !bm.isCharging(), 5_000));
}
}
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/BatteryConstraintTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/BatteryConstraintTest.java
index dd855da..cf9ec4c 100644
--- a/tests/JobScheduler/src/android/jobscheduler/cts/BatteryConstraintTest.java
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/BatteryConstraintTest.java
@@ -92,6 +92,16 @@
}
}
+ boolean hasBattery() throws Exception {
+ Intent batteryInfo = getContext().registerReceiver(
+ null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+ boolean present = batteryInfo.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);
+ if (!present) {
+ Log.i(TAG, "Device doesn't have a battery.");
+ }
+ return present;
+ }
+
void setBatteryState(boolean plugged, int level) throws Exception {
if (plugged) {
SystemUtil.runShellCommand(getInstrumentation(), "cmd battery set ac 1");
@@ -210,6 +220,11 @@
* not plugged in but has sufficient power.
*/
public void testBatteryNotLowConstraintExecutes_withoutPower() throws Exception {
+ // "Without power" test case is valid only for devices with a battery.
+ if (!hasBattery()) {
+ return;
+ }
+
setBatteryState(false, 100);
waitFor(2_000);
verifyChargingState(false);
@@ -234,6 +249,11 @@
* the device is not on power.
*/
public void testChargingConstraintFails() throws Exception {
+ // "Without power" test case is valid only for devices with a battery.
+ if (!hasBattery()) {
+ return;
+ }
+
setBatteryState(false, 100);
verifyChargingState(false);
@@ -273,6 +293,11 @@
* the battery level is critical and not on power.
*/
public void testBatteryNotLowConstraintFails_withoutPower() throws Exception {
+ // "Without power" test case is valid only for devices with a battery.
+ if (!hasBattery()) {
+ return;
+ }
+
setBatteryState(false, mLowBatteryWarningLevel);
// setBatteryState() waited for the charging/not-charging state to formally settle,
// but battery level reporting lags behind that. wait a moment to let that happen
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityEndToEndTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityEndToEndTest.java
index d535fab..e16c8e8 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityEndToEndTest.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityEndToEndTest.java
@@ -67,11 +67,14 @@
import java.util.List;
import java.util.concurrent.TimeoutException;
+import com.android.compatibility.common.util.CddTest;
+
/**
* This class performs end-to-end testing of the accessibility feature by
* creating an {@link Activity} and poking around so {@link AccessibilityEvent}s
* are generated and their correct dispatch verified.
*/
+@CddTest(requirement="3.10/C-1-2,W-1-1")
public class AccessibilityEndToEndTest extends
AccessibilityActivityTestCase<AccessibilityEndToEndActivity> {
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilitySettingsTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilitySettingsTest.java
index f01251a..c6ec02e 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilitySettingsTest.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilitySettingsTest.java
@@ -27,10 +27,13 @@
import java.util.List;
+import com.android.compatibility.common.util.CddTest;
+
/**
* This test case is responsible to verify that the intent for launching
* accessibility settings has an activity that handles it.
*/
+@CddTest(requirement="3.10/C-1-3")
@Presubmit
public class AccessibilitySettingsTest extends AndroidTestCase {
diff --git a/tests/app/src/android/app/cts/ActivityManagerProcessStateTest.java b/tests/app/src/android/app/cts/ActivityManagerProcessStateTest.java
index 5beb1c4..4ce31a3 100644
--- a/tests/app/src/android/app/cts/ActivityManagerProcessStateTest.java
+++ b/tests/app/src/android/app/cts/ActivityManagerProcessStateTest.java
@@ -49,6 +49,7 @@
import androidx.test.InstrumentationRegistry;
+import com.android.compatibility.common.util.CddTest;
import com.android.compatibility.common.util.SystemUtil;
public class ActivityManagerProcessStateTest extends InstrumentationTestCase {
@@ -527,6 +528,7 @@
* Test that background check behaves correctly after a process is no longer foreground:
* first allowing a service to be started, then stopped by the system when idle.
*/
+ @CddTest(requirement="3.5/C-0-7")
public void testBackgroundCheckStopsService() throws Exception {
final Parcel data = Parcel.obtain();
ServiceConnectionHandler conn = new ServiceConnectionHandler(mContext, mServiceIntent,
diff --git a/tests/app/src/android/app/cts/ServiceTest.java b/tests/app/src/android/app/cts/ServiceTest.java
index 6c25a2dc..0b4f4fb 100644
--- a/tests/app/src/android/app/cts/ServiceTest.java
+++ b/tests/app/src/android/app/cts/ServiceTest.java
@@ -44,11 +44,13 @@
import androidx.test.InstrumentationRegistry;
+import com.android.compatibility.common.util.CddTest;
import com.android.compatibility.common.util.IBinderParcelable;
import com.android.compatibility.common.util.SystemUtil;
import java.util.List;
+@CddTest(requirement="3.5/C-0-2")
public class ServiceTest extends ActivityTestsBase {
private static final String TAG = "ServiceTest";
private static final String NOTIFICATION_CHANNEL_ID = TAG;
diff --git a/tests/app/src/android/app/cts/SystemFeaturesTest.java b/tests/app/src/android/app/cts/SystemFeaturesTest.java
index 6d22063..6dc1df7 100644
--- a/tests/app/src/android/app/cts/SystemFeaturesTest.java
+++ b/tests/app/src/android/app/cts/SystemFeaturesTest.java
@@ -43,6 +43,7 @@
import android.telephony.TelephonyManager;
import android.test.InstrumentationTestCase;
+import com.android.compatibility.common.util.CddTest;
import com.android.compatibility.common.util.PropertyUtil;
import java.lang.reflect.Field;
@@ -293,6 +294,7 @@
}
}
+ @CddTest(requirement="7.4.4/C-1-1,C-2-1")
public void testNfcFeatures() {
if (NfcAdapter.getDefaultAdapter(mContext) != null) {
// Watches MAY support all FEATURE_NFC features when an NfcAdapter is available, but
@@ -501,6 +503,7 @@
// TODO: Add tests for the other touchscreen features.
}
+ @CddTest(requirement="7.7.2/C-2-1")
public void testUsbAccessory() {
if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) &&
!mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) &&
diff --git a/tests/autofillservice/AndroidManifest.xml b/tests/autofillservice/AndroidManifest.xml
index 428403d..802b081 100644
--- a/tests/autofillservice/AndroidManifest.xml
+++ b/tests/autofillservice/AndroidManifest.xml
@@ -29,8 +29,7 @@
<uses-library android:name="android.test.runner" />
- <activity android:name=".LoginActivity"
- android:screenOrientation="portrait">
+ <activity android:name=".LoginActivity" >
<intent-filter>
<!-- This intent filter is not really needed by CTS, but it maks easier to launch
this app during CTS development... -->
@@ -141,6 +140,24 @@
<action android:name="android.service.autofill.AutofillService" />
</intent-filter>
</service>
+
+ <!-- Mock IME -->
+ <service
+ android:name="com.android.cts.mockime.MockIme"
+ android:label="Mock IME"
+ android:permission="android.permission.BIND_INPUT_METHOD">
+ <intent-filter>
+ <action android:name="android.view.InputMethod" />
+ </intent-filter>
+ <meta-data
+ android:name="android.view.im"
+ android:resource="@xml/method" />
+ </service>
+ <provider
+ android:authorities="com.android.cts.mockime.provider"
+ android:name="com.android.cts.mockime.SettingsProvider">
+ </provider>
+
</application>
<instrumentation
diff --git a/tests/autofillservice/res/xml/method.xml b/tests/autofillservice/res/xml/method.xml
new file mode 100644
index 0000000..7f8b13a
--- /dev/null
+++ b/tests/autofillservice/res/xml/method.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 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.
+-->
+
+<input-method xmlns:android="http://schemas.android.com/apk/res/android">
+</input-method>
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AutoFillServiceTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/AutoFillServiceTestCase.java
index 7eb8bcf..8ea2c14 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/AutoFillServiceTestCase.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/AutoFillServiceTestCase.java
@@ -35,6 +35,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.RequiredFeatureRule;
+import com.android.cts.mockime.MockImeSessionRule;
import org.junit.After;
import org.junit.Before;
@@ -76,6 +77,10 @@
}
};
+ @ClassRule
+ public static final MockImeSessionRule sMockImeSessionRule =
+ new MockImeSessionRule(/* ignoreInitException= */ true);
+
@Rule
public final RetryRule mRetryRule = new RetryRule(2);
diff --git a/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionWithLinkTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionWithLinkTestCase.java
index f5a4f89..02b2e13 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionWithLinkTestCase.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionWithLinkTestCase.java
@@ -71,28 +71,13 @@
public final void testTapLink_changeOrientationThenTapBack() throws Exception {
assumeTrue("Rotation is supported", Helper.isRotationSupported(mContext));
- final int width = mUiBot.getDevice().getDisplayWidth();
- final int heigth = mUiBot.getDevice().getDisplayHeight();
- final int min = Math.min(width, heigth);
-
- assumeTrue("Screen size is too small (" + width + "x" + heigth + ")", min >= 500);
- Log.d(TAG, "testTapLink_changeOrientationThenTapBack(): screen size is "
- + width + "x" + heigth);
-
mUiBot.setScreenOrientation(UiBot.PORTRAIT);
try {
- runShellCommand("wm size 1080x1920");
- runShellCommand("wm density 320");
saveUiRestoredAfterTappingLinkTest(
PostSaveLinkTappedAction.ROTATE_THEN_TAP_BACK_BUTTON);
} finally {
mUiBot.setScreenOrientation(UiBot.PORTRAIT);
- try {
- cleanUpAfterScreenOrientationIsBackToPortrait();
- } finally {
- runShellCommand("wm density reset");
- runShellCommand("wm size reset");
- }
+ cleanUpAfterScreenOrientationIsBackToPortrait();
}
}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DatasetFilteringTest.java b/tests/autofillservice/src/android/autofillservice/cts/DatasetFilteringTest.java
index 81b3fa2..87b0ff7 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/DatasetFilteringTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/DatasetFilteringTest.java
@@ -19,9 +19,21 @@
import static android.autofillservice.cts.Helper.ID_USERNAME;
import static android.autofillservice.cts.common.ShellHelper.runShellCommand;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.expectBindInput;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
+
import android.autofillservice.cts.CannedFillResponse.CannedDataset;
import android.content.IntentSender;
+import android.os.Process;
import android.platform.test.annotations.AppModeFull;
+import android.view.KeyEvent;
+import android.view.View;
+
+import com.android.cts.mockime.ImeCommand;
+import com.android.cts.mockime.ImeEventStream;
+import com.android.cts.mockime.MockImeSession;
import org.junit.AfterClass;
import org.junit.BeforeClass;
@@ -31,6 +43,8 @@
public class DatasetFilteringTest extends AbstractLoginActivityTestCase {
+ private static final long MOCK_IME_TIMEOUT_MS = 5_000;
+
private static String sMaxDatasets;
@BeforeClass
@@ -108,7 +122,7 @@
}
@Test
- public void testFilter_usingKeyboard() throws Exception {
+ public void testFilter_ejectingEvents() throws Exception {
final String aa = "Two A's";
final String ab = "A and B";
final String b = "Only B";
@@ -165,6 +179,74 @@
@Test
@AppModeFull // testFilter() is enough to test ephemeral apps support
+ public void testFilter_usingKeyboard() throws Exception {
+ final String aa = "Two A's";
+ final String ab = "A and B";
+ final String b = "Only B";
+
+ final MockImeSession mockImeSession = sMockImeSessionRule.getMockImeSession();
+
+ enableService();
+
+ // Set expectations.
+ sReplier.addResponse(new CannedFillResponse.Builder()
+ .addDataset(new CannedDataset.Builder()
+ .setField(ID_USERNAME, "aa")
+ .setPresentation(createPresentation(aa))
+ .build())
+ .addDataset(new CannedDataset.Builder()
+ .setField(ID_USERNAME, "ab")
+ .setPresentation(createPresentation(ab))
+ .build())
+ .addDataset(new CannedDataset.Builder()
+ .setField(ID_USERNAME, "b")
+ .setPresentation(createPresentation(b))
+ .build())
+ .build());
+
+ final ImeEventStream stream = mockImeSession.openEventStream();
+
+ // Trigger auto-fill.
+ mActivity.onUsername(View::requestFocus);
+
+ // Wait until the MockIme gets bound to the TestActivity.
+ expectBindInput(stream, Process.myPid(), MOCK_IME_TIMEOUT_MS);
+ expectEvent(stream, editorMatcher("onStartInput", mActivity.getUsername().getId()),
+ MOCK_IME_TIMEOUT_MS);
+
+ sReplier.getNextFillRequest();
+
+ // With no filter text all datasets should be shown
+ mUiBot.assertDatasets(aa, ab, b);
+
+ // Only two datasets start with 'a'
+ final ImeCommand cmd1 = mockImeSession.callCommitText("a", 1);
+ expectCommand(stream, cmd1, MOCK_IME_TIMEOUT_MS);
+ mUiBot.assertDatasets(aa, ab);
+
+ // Only one dataset start with 'aa'
+ final ImeCommand cmd2 = mockImeSession.callCommitText("a", 1);
+ expectCommand(stream, cmd2, MOCK_IME_TIMEOUT_MS);
+ mUiBot.assertDatasets(aa);
+
+ // Only two datasets start with 'a'
+ sendKeyEvents("KEYCODE_DEL"); // TODO: add new method on MockIme for it
+ mUiBot.assertDatasets(aa, ab);
+
+ // With no filter text all datasets should be shown
+ sendKeyEvents("KEYCODE_DEL"); // TODO: add new method on MockIme for it
+ mUiBot.assertDatasets(aa, ab, b);
+
+ // No dataset start with 'aaa'
+ final MyAutofillCallback callback = mActivity.registerCallback();
+ final ImeCommand cmd5 = mockImeSession.callCommitText("aaa", 1);
+ expectCommand(stream, cmd5, MOCK_IME_TIMEOUT_MS);
+ callback.assertUiHiddenEvent(mActivity.getUsername());
+ mUiBot.assertNoDatasets();
+ }
+
+ @Test
+ @AppModeFull // testFilter() is enough to test ephemeral apps support
public void testFilter_nullValuesAlwaysMatched() throws Exception {
final String aa = "Two A's";
final String ab = "A and B";
diff --git a/tests/autofillservice/src/android/autofillservice/cts/Helper.java b/tests/autofillservice/src/android/autofillservice/cts/Helper.java
index 2037259..a9ba097 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/Helper.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/Helper.java
@@ -802,7 +802,20 @@
* Checks if screen orientation can be changed.
*/
public static boolean isRotationSupported(Context context) {
- return !context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
+ final PackageManager packageManager = context.getPackageManager();
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
+ Log.v(TAG, "isRotationSupported(): is auto");
+ return false;
+ }
+ if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
+ Log.v(TAG, "isRotationSupported(): has leanback feature");
+ return false;
+ }
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_PC)) {
+ Log.v(TAG, "isRotationSupported(): is PC");
+ return false;
+ }
+ return true;
}
/**
diff --git a/tests/autofillservice/src/android/autofillservice/cts/LoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/LoginActivityTest.java
index c476c7c..d52bcf6 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/LoginActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/LoginActivityTest.java
@@ -86,6 +86,10 @@
import android.view.autofill.AutofillManager;
import android.widget.RemoteViews;
+import com.android.cts.mockime.ImeCommand;
+import com.android.cts.mockime.ImeEventStream;
+import com.android.cts.mockime.MockImeSession;
+
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
diff --git a/tests/autofillservice/src/android/autofillservice/cts/UiBot.java b/tests/autofillservice/src/android/autofillservice/cts/UiBot.java
index 90358ab..1fe998e 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/UiBot.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/UiBot.java
@@ -93,8 +93,6 @@
"autofill_picker_accessibility_title";
private static final String RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE =
"autofill_save_accessibility_title";
- private static final String RESOURCE_BOOLEAN_CONFIG_FORCE_DEFAULT_ORIENTATION =
- "config_forceDefaultOrientation";
static final BySelector DATASET_PICKER_SELECTOR = By.res("android", RESOURCE_ID_DATASET_PICKER);
@@ -874,18 +872,4 @@
final int booleanId = resources.getIdentifier(id, "bool", "android");
return resources.getBoolean(booleanId);
}
-
- /**
- * Returns {@code true} if display rotation is supported, {@code false} otherwise.
- */
- public boolean isScreenRotationSupported() {
- try {
- return !getBoolean(RESOURCE_BOOLEAN_CONFIG_FORCE_DEFAULT_ORIENTATION);
- } catch (Resources.NotFoundException e) {
- Log.d(TAG, "Resource not found: "
- + RESOURCE_BOOLEAN_CONFIG_FORCE_DEFAULT_ORIENTATION
- + ". Assume rotation supported");
- return true;
- }
- }
-}
\ No newline at end of file
+}
diff --git a/tests/autofillservice/src/com/android/cts/mockime/ImeCommand.java b/tests/autofillservice/src/com/android/cts/mockime/ImeCommand.java
new file mode 100644
index 0000000..ad81eb5
--- /dev/null
+++ b/tests/autofillservice/src/com/android/cts/mockime/ImeCommand.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.mockime;
+
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+
+public final class ImeCommand {
+
+ private static final String NAME_KEY = "name";
+ private static final String ID_KEY = "id";
+ private static final String DISPATCH_TO_MAIN_THREAD_KEY = "dispatchToMainThread";
+ private static final String EXTRA_KEY = "extra";
+
+ @NonNull
+ private final String mName;
+ private final long mId;
+ private final boolean mDispatchToMainThread;
+ @NonNull
+ private final Bundle mExtras;
+
+ ImeCommand(@NonNull String name, long id, boolean dispatchToMainThread,
+ @NonNull Bundle extras) {
+ mName = name;
+ mId = id;
+ mDispatchToMainThread = dispatchToMainThread;
+ mExtras = extras;
+ }
+
+ private ImeCommand(@NonNull Bundle bundle) {
+ mName = bundle.getString(NAME_KEY);
+ mId = bundle.getLong(ID_KEY);
+ mDispatchToMainThread = bundle.getBoolean(DISPATCH_TO_MAIN_THREAD_KEY);
+ mExtras = bundle.getParcelable(EXTRA_KEY);
+ }
+
+ static ImeCommand fromBundle(@NonNull Bundle bundle) {
+ return new ImeCommand(bundle);
+ }
+
+ Bundle toBundle() {
+ final Bundle bundle = new Bundle();
+ bundle.putString(NAME_KEY, mName);
+ bundle.putLong(ID_KEY, mId);
+ bundle.putBoolean(DISPATCH_TO_MAIN_THREAD_KEY, mDispatchToMainThread);
+ bundle.putParcelable(EXTRA_KEY, mExtras);
+ return bundle;
+ }
+
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public boolean shouldDispatchToMainThread() {
+ return mDispatchToMainThread;
+ }
+
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+}
diff --git a/tests/autofillservice/src/com/android/cts/mockime/ImeEvent.java b/tests/autofillservice/src/com/android/cts/mockime/ImeEvent.java
new file mode 100644
index 0000000..d4090fb
--- /dev/null
+++ b/tests/autofillservice/src/com/android/cts/mockime/ImeEvent.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.mockime;
+
+import android.inputmethodservice.AbstractInputMethodService;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import android.view.View;
+
+/**
+ * An immutable object that stores event happened in the {@link MockIme}.
+ */
+public final class ImeEvent {
+
+ private enum ReturnType {
+ Null,
+ KnownUnsupportedType,
+ Boolean,
+ }
+
+ private static ReturnType getReturnTypeFromObject(@Nullable Object object) {
+ if (object == null) {
+ return ReturnType.Null;
+ }
+ if (object instanceof AbstractInputMethodService.AbstractInputMethodImpl) {
+ return ReturnType.KnownUnsupportedType;
+ }
+ if (object instanceof View) {
+ return ReturnType.KnownUnsupportedType;
+ }
+ if (object instanceof Boolean) {
+ return ReturnType.Boolean;
+ }
+ throw new UnsupportedOperationException("Unsupported return type=" + object);
+ }
+
+ ImeEvent(@NonNull String eventName, int nestLevel, @NonNull String threadName, int threadId,
+ boolean isMainThread, long enterTimestamp, long exitTimestamp, long enterWallTime,
+ long exitWallTime, @NonNull ImeState enterState, @Nullable ImeState exitState,
+ @NonNull Bundle arguments, @Nullable Object returnValue) {
+ this(eventName, nestLevel, threadName, threadId, isMainThread, enterTimestamp,
+ exitTimestamp, enterWallTime, exitWallTime, enterState, exitState, arguments,
+ returnValue, getReturnTypeFromObject(returnValue));
+ }
+
+ private ImeEvent(@NonNull String eventName, int nestLevel, @NonNull String threadName,
+ int threadId, boolean isMainThread, long enterTimestamp, long exitTimestamp,
+ long enterWallTime, long exitWallTime, @NonNull ImeState enterState,
+ @Nullable ImeState exitState, @NonNull Bundle arguments, @Nullable Object returnValue,
+ @NonNull ReturnType returnType) {
+ mEventName = eventName;
+ mNestLevel = nestLevel;
+ mThreadName = threadName;
+ mThreadId = threadId;
+ mIsMainThread = isMainThread;
+ mEnterTimestamp = enterTimestamp;
+ mExitTimestamp = exitTimestamp;
+ mEnterWallTime = enterWallTime;
+ mExitWallTime = exitWallTime;
+ mEnterState = enterState;
+ mExitState = exitState;
+ mArguments = arguments;
+ mReturnValue = returnValue;
+ mReturnType = returnType;
+ }
+
+ @NonNull
+ Bundle toBundle() {
+ final Bundle bundle = new Bundle();
+ bundle.putString("mEventName", mEventName);
+ bundle.putInt("mNestLevel", mNestLevel);
+ bundle.putString("mThreadName", mThreadName);
+ bundle.putInt("mThreadId", mThreadId);
+ bundle.putBoolean("mIsMainThread", mIsMainThread);
+ bundle.putLong("mEnterTimestamp", mEnterTimestamp);
+ bundle.putLong("mExitTimestamp", mExitTimestamp);
+ bundle.putLong("mEnterWallTime", mEnterWallTime);
+ bundle.putLong("mExitWallTime", mExitWallTime);
+ bundle.putBundle("mEnterState", mEnterState.toBundle());
+ bundle.putBundle("mExitState", mExitState != null ? mExitState.toBundle() : null);
+ bundle.putBundle("mArguments", mArguments);
+ bundle.putString("mReturnType", mReturnType.name());
+ switch (mReturnType) {
+ case Null:
+ case KnownUnsupportedType:
+ break;
+ case Boolean:
+ bundle.putBoolean("mReturnValue", getReturnBooleanValue());
+ break;
+ default:
+ throw new UnsupportedOperationException("Unsupported type=" + mReturnType);
+ }
+ return bundle;
+ }
+
+ @NonNull
+ static ImeEvent fromBundle(@NonNull Bundle bundle) {
+ final String eventName = bundle.getString("mEventName");
+ final int nestLevel = bundle.getInt("mNestLevel");
+ final String threadName = bundle.getString("mThreadName");
+ final int threadId = bundle.getInt("mThreadId");
+ final boolean isMainThread = bundle.getBoolean("mIsMainThread");
+ final long enterTimestamp = bundle.getLong("mEnterTimestamp");
+ final long exitTimestamp = bundle.getLong("mExitTimestamp");
+ final long enterWallTime = bundle.getLong("mEnterWallTime");
+ final long exitWallTime = bundle.getLong("mExitWallTime");
+ final ImeState enterState = ImeState.fromBundle(bundle.getBundle("mEnterState"));
+ final ImeState exitState = ImeState.fromBundle(bundle.getBundle("mExitState"));
+ final Bundle arguments = bundle.getBundle("mArguments");
+ final Object result;
+ final ReturnType returnType = ReturnType.valueOf(bundle.getString("mReturnType"));
+ switch (returnType) {
+ case Null:
+ case KnownUnsupportedType:
+ result = null;
+ break;
+ case Boolean:
+ result = bundle.getBoolean("mReturnValue");
+ break;
+ default:
+ throw new UnsupportedOperationException("Unsupported type=" + returnType);
+ }
+ return new ImeEvent(eventName, nestLevel, threadName,
+ threadId, isMainThread, enterTimestamp, exitTimestamp, enterWallTime, exitWallTime,
+ enterState, exitState, arguments, result, returnType);
+ }
+
+ /**
+ * Returns a string that represents the type of this event.
+ *
+ * <p>Examples: "onCreate", "onStartInput", ...</p>
+ *
+ * <p>TODO: Use enum type or something like that instead of raw String type.</p>
+ * @return A string that represents the type of this event.
+ */
+ @NonNull
+ public String getEventName() {
+ return mEventName;
+ }
+
+ /**
+ * Returns the nest level of this event.
+ *
+ * <p>For instance, when "showSoftInput" internally calls
+ * "onStartInputView", the event for "onStartInputView" has 1 level higher
+ * nest level than "showSoftInput".</p>
+ */
+ public int getNestLevel() {
+ return mNestLevel;
+ }
+
+ /**
+ * @return Name of the thread, where the event was consumed.
+ */
+ @NonNull
+ public String getThreadName() {
+ return mThreadName;
+ }
+
+ /**
+ * @return Thread ID (TID) of the thread, where the event was consumed.
+ */
+ public int getThreadId() {
+ return mThreadId;
+ }
+
+ /**
+ * @return {@code true} if the event was being consumed in the main thread.
+ */
+ public boolean isMainThread() {
+ return mIsMainThread;
+ }
+
+ /**
+ * @return Monotonic time measured by {@link android.os.SystemClock#elapsedRealtimeNanos()} when
+ * the corresponding event handler was called back.
+ */
+ public long getEnterTimestamp() {
+ return mEnterTimestamp;
+ }
+
+ /**
+ * @return Monotonic time measured by {@link android.os.SystemClock#elapsedRealtimeNanos()} when
+ * the corresponding event handler finished.
+ */
+ public long getExitTimestamp() {
+ return mExitTimestamp;
+ }
+
+ /**
+ * @return Wall-clock time measured by {@link System#currentTimeMillis()} when the corresponding
+ * event handler was called back.
+ */
+ public long getEnterWallTime() {
+ return mEnterWallTime;
+ }
+
+ /**
+ * @return Wall-clock time measured by {@link System#currentTimeMillis()} when the corresponding
+ * event handler finished.
+ */
+ public long getExitWallTime() {
+ return mExitWallTime;
+ }
+
+ /**
+ * @return IME state snapshot taken when the corresponding event handler was called back.
+ */
+ @NonNull
+ public ImeState getEnterState() {
+ return mEnterState;
+ }
+
+ /**
+ * @return IME state snapshot taken when the corresponding event handler finished.
+ */
+ @Nullable
+ public ImeState getExitState() {
+ return mExitState;
+ }
+
+ /**
+ * @return {@link Bundle} that stores parameters passed to the corresponding event handler.
+ */
+ @NonNull
+ public Bundle getArguments() {
+ return mArguments;
+ }
+
+ /**
+ * @return result value of this event.
+ * @throws NullPointerException if the return value is {@code null}
+ * @throws ClassCastException if the return value is non-{@code null} object that is different
+ * from {@link Boolean}
+ */
+ public boolean getReturnBooleanValue() {
+ if (mReturnType == ReturnType.Null) {
+ throw new NullPointerException();
+ }
+ if (mReturnType != ReturnType.Boolean) {
+ throw new ClassCastException();
+ }
+ return (Boolean) mReturnValue;
+ }
+
+ /**
+ * @return {@code true} if the event is issued when the event starts, not when the event
+ * finishes.
+ */
+ public boolean isEnterEvent() {
+ return mExitState == null;
+ }
+
+ @NonNull
+ private final String mEventName;
+ private final int mNestLevel;
+ @NonNull
+ private final String mThreadName;
+ private final int mThreadId;
+ private final boolean mIsMainThread;
+ private final long mEnterTimestamp;
+ private final long mExitTimestamp;
+ private final long mEnterWallTime;
+ private final long mExitWallTime;
+ @NonNull
+ private final ImeState mEnterState;
+ @Nullable
+ private final ImeState mExitState;
+ @NonNull
+ private final Bundle mArguments;
+ @Nullable
+ private final Object mReturnValue;
+ @NonNull
+ private final ReturnType mReturnType;
+}
diff --git a/tests/autofillservice/src/com/android/cts/mockime/ImeEventStream.java b/tests/autofillservice/src/com/android/cts/mockime/ImeEventStream.java
new file mode 100644
index 0000000..d866da0
--- /dev/null
+++ b/tests/autofillservice/src/com/android/cts/mockime/ImeEventStream.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.mockime;
+
+import android.os.Bundle;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import android.view.inputmethod.EditorInfo;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * A utility class that provides basic query operations and wait primitives for a series of
+ * {@link ImeEvent} sent from the {@link MockIme}.
+ *
+ * <p>All public methods are not thread-safe.</p>
+ */
+public final class ImeEventStream {
+
+ private static final String LONG_LONG_SPACES = " ";
+
+ private static DateTimeFormatter sSimpleDateTimeFormatter =
+ DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.systemDefault());
+
+ @NonNull
+ private final Supplier<ImeEventArray> mEventSupplier;
+ private int mCurrentPosition;
+
+ ImeEventStream(@NonNull Supplier<ImeEventArray> supplier) {
+ this(supplier, 0 /* position */);
+ }
+
+ private ImeEventStream(@NonNull Supplier<ImeEventArray> supplier, int position) {
+ mEventSupplier = supplier;
+ mCurrentPosition = position;
+ }
+
+ /**
+ * Create a copy that starts from the same event position of this stream. Once a copy is created
+ * further event position change on this stream will not affect the copy.
+ *
+ * @return A new copy of this stream
+ */
+ public ImeEventStream copy() {
+ return new ImeEventStream(mEventSupplier, mCurrentPosition);
+ }
+
+ /**
+ * Advances the current event position by skipping events.
+ *
+ * @param length number of events to be skipped
+ * @throws IllegalArgumentException {@code length} is negative
+ */
+ public void skip(@IntRange(from = 0) int length) {
+ if (length < 0) {
+ throw new IllegalArgumentException("length cannot be negative: " + length);
+ }
+ mCurrentPosition += length;
+ }
+
+ /**
+ * Advances the current event position to the next to the last position.
+ */
+ public void skipAll() {
+ mCurrentPosition = mEventSupplier.get().mLength;
+ }
+
+ /**
+ * Find the first event that matches the given condition from the current position.
+ *
+ * <p>If there is such an event, this method returns such an event without moving the current
+ * event position.</p>
+ *
+ * <p>If there is such an event, this method returns {@link Optional#empty()} without moving the
+ * current event position.</p>
+ *
+ * @param condition the event condition to be matched
+ * @return {@link Optional#empty()} if there is no such an event. Otherwise the matched event is
+ * returned
+ */
+ @NonNull
+ public Optional<ImeEvent> findFirst(Predicate<ImeEvent> condition) {
+ final ImeEventArray latest = mEventSupplier.get();
+ int index = mCurrentPosition;
+ while (true) {
+ if (index >= latest.mLength) {
+ return Optional.empty();
+ }
+ if (condition.test(latest.mArray[index])) {
+ return Optional.of(latest.mArray[index]);
+ }
+ ++index;
+ }
+ }
+
+ /**
+ * Find the first event that matches the given condition from the current position.
+ *
+ * <p>If there is such an event, this method returns such an event and set the current event
+ * position to that event.</p>
+ *
+ * <p>If there is such an event, this method returns {@link Optional#empty()} without moving the
+ * current event position.</p>
+ *
+ * @param condition the event condition to be matched
+ * @return {@link Optional#empty()} if there is no such an event. Otherwise the matched event is
+ * returned
+ */
+ @NonNull
+ public Optional<ImeEvent> seekToFirst(Predicate<ImeEvent> condition) {
+ final ImeEventArray latest = mEventSupplier.get();
+ while (true) {
+ if (mCurrentPosition >= latest.mLength) {
+ return Optional.empty();
+ }
+ if (condition.test(latest.mArray[mCurrentPosition])) {
+ return Optional.of(latest.mArray[mCurrentPosition]);
+ }
+ ++mCurrentPosition;
+ }
+ }
+
+ private static void dumpEvent(@NonNull StringBuilder sb, @NonNull ImeEvent event,
+ boolean fused) {
+ final String indentation = getWhiteSpaces(event.getNestLevel() * 2 + 2);
+ final long wallTime =
+ fused ? event.getEnterWallTime() :
+ event.isEnterEvent() ? event.getEnterWallTime() : event.getExitWallTime();
+ sb.append(sSimpleDateTimeFormatter.format(Instant.ofEpochMilli(wallTime)))
+ .append(" ")
+ .append(String.format("%5d", event.getThreadId()))
+ .append(indentation);
+ sb.append(fused ? "" : event.isEnterEvent() ? "[" : "]");
+ if (fused || event.isEnterEvent()) {
+ sb.append(event.getEventName())
+ .append(':')
+ .append(" args=");
+ dumpBundle(sb, event.getArguments());
+ }
+ sb.append('\n');
+ }
+
+ /**
+ * @return Debug info as a {@link String}.
+ */
+ public String dump() {
+ final ImeEventArray latest = mEventSupplier.get();
+ final StringBuilder sb = new StringBuilder();
+ sb.append("ImeEventStream:\n");
+ sb.append(" latest: array[").append(latest.mArray.length).append("] + {\n");
+ for (int i = 0; i < latest.mLength; ++i) {
+ // To compress the dump message, if the current event is an enter event and the next
+ // one is a corresponding exit event, we unify the output.
+ final boolean fused = areEnterExitPairedMessages(latest, i);
+ if (i == mCurrentPosition || (fused && ((i + 1) == mCurrentPosition))) {
+ sb.append(" ======== CurrentPosition ======== \n");
+ }
+ dumpEvent(sb, latest.mArray[fused ? ++i : i], fused);
+ }
+ if (mCurrentPosition >= latest.mLength) {
+ sb.append(" ======== CurrentPosition ======== \n");
+ }
+ sb.append("}\n");
+ return sb.toString();
+ }
+
+ /**
+ * @param array event array to be checked
+ * @param i index to be checked
+ * @return {@code true} if {@code array.mArray[i]} and {@code array.mArray[i + 1]} are two
+ * paired events.
+ */
+ private static boolean areEnterExitPairedMessages(@NonNull ImeEventArray array,
+ @IntRange(from = 0) int i) {
+ return array.mArray[i] != null
+ && array.mArray[i].isEnterEvent()
+ && (i + 1) < array.mLength
+ && array.mArray[i + 1] != null
+ && array.mArray[i].getEventName().equals(array.mArray[i + 1].getEventName())
+ && array.mArray[i].getEnterTimestamp() == array.mArray[i + 1].getEnterTimestamp();
+ }
+
+ /**
+ * @param length length of the requested white space string
+ * @return {@link String} object whose length is {@code length}
+ */
+ private static String getWhiteSpaces(@IntRange(from = 0) final int length) {
+ if (length < LONG_LONG_SPACES.length()) {
+ return LONG_LONG_SPACES.substring(0, length);
+ }
+ final char[] indentationChars = new char[length];
+ Arrays.fill(indentationChars, ' ');
+ return new String(indentationChars);
+ }
+
+ private static void dumpBundle(@NonNull StringBuilder sb, @NonNull Bundle bundle) {
+ sb.append('{');
+ boolean first = true;
+ for (String key : bundle.keySet()) {
+ if (first) {
+ first = false;
+ } else {
+ sb.append(' ');
+ }
+ final Object object = bundle.get(key);
+ sb.append(key);
+ sb.append('=');
+ if (object instanceof EditorInfo) {
+ final EditorInfo info = (EditorInfo) object;
+ sb.append("EditorInfo{packageName=").append(info.packageName);
+ sb.append(" fieldId=").append(info.fieldId);
+ sb.append(" hintText=").append(info.hintText);
+ sb.append(" privateImeOptions=").append(info.privateImeOptions);
+ sb.append("}");
+ } else {
+ sb.append(object);
+ }
+ }
+ sb.append('}');
+ }
+
+ static class ImeEventArray {
+ @NonNull
+ public final ImeEvent[] mArray;
+ public final int mLength;
+ ImeEventArray(ImeEvent[] array, int length) {
+ mArray = array;
+ mLength = length;
+ }
+ }
+}
diff --git a/tests/autofillservice/src/com/android/cts/mockime/ImeEventStreamTestUtils.java b/tests/autofillservice/src/com/android/cts/mockime/ImeEventStreamTestUtils.java
new file mode 100644
index 0000000..8497268
--- /dev/null
+++ b/tests/autofillservice/src/com/android/cts/mockime/ImeEventStreamTestUtils.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.mockime;
+
+import android.os.SystemClock;
+import androidx.annotation.NonNull;
+import android.text.TextUtils;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputBinding;
+
+import java.util.Optional;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
+
+/**
+ * A set of utility methods to avoid boilerplate code when writing end-to-end tests.
+ */
+public final class ImeEventStreamTestUtils {
+ private static final long TIME_SLICE = 50; // msec
+
+ /**
+ * Cannot be instantiated
+ */
+ private ImeEventStreamTestUtils() {}
+
+ /**
+ * Behavior mode of {@link #expectEvent(ImeEventStream, Predicate, EventFilterMode, long)}
+ */
+ public enum EventFilterMode {
+ /**
+ * All {@link ImeEvent} events should be checked
+ */
+ CHECK_ALL,
+ /**
+ * Only events that return {@code true} from {@link ImeEvent#isEnterEvent()} should be
+ * checked
+ */
+ CHECK_ENTER_EVENT_ONLY,
+ /**
+ * Only events that return {@code false} from {@link ImeEvent#isEnterEvent()} should be
+ * checked
+ */
+ CHECK_EXIT_EVENT_ONLY,
+ }
+
+ /**
+ * Wait until an event that matches the given {@code condition} is found in the stream.
+ *
+ * <p>When this method succeeds to find an event that matches the given {@code condition}, the
+ * stream position will be set to the next to the found object then the event found is returned.
+ * </p>
+ *
+ * <p>For convenience, this method automatically filter out exit events (events that return
+ * {@code false} from {@link ImeEvent#isEnterEvent()}.</p>
+ *
+ * <p>TODO: Consider renaming this to {@code expectEventEnter} or something like that.</p>
+ *
+ * @param stream {@link ImeEventStream} to be checked.
+ * @param condition the event condition to be matched
+ * @param timeout timeout in millisecond
+ * @return {@link ImeEvent} found
+ * @throws TimeoutException when the no event is matched to the given condition within
+ * {@code timeout}
+ */
+ @NonNull
+ public static ImeEvent expectEvent(@NonNull ImeEventStream stream,
+ @NonNull Predicate<ImeEvent> condition, long timeout) throws TimeoutException {
+ return expectEvent(stream, condition, EventFilterMode.CHECK_ENTER_EVENT_ONLY, timeout);
+ }
+
+ /**
+ * Wait until an event that matches the given {@code condition} is found in the stream.
+ *
+ * <p>When this method succeeds to find an event that matches the given {@code condition}, the
+ * stream position will be set to the next to the found object then the event found is returned.
+ * </p>
+ *
+ * @param stream {@link ImeEventStream} to be checked.
+ * @param condition the event condition to be matched
+ * @param filterMode controls how events are filtered out
+ * @param timeout timeout in millisecond
+ * @return {@link ImeEvent} found
+ * @throws TimeoutException when the no event is matched to the given condition within
+ * {@code timeout}
+ */
+ @NonNull
+ public static ImeEvent expectEvent(@NonNull ImeEventStream stream,
+ @NonNull Predicate<ImeEvent> condition, EventFilterMode filterMode, long timeout)
+ throws TimeoutException {
+ try {
+ Optional<ImeEvent> result;
+ while (true) {
+ if (timeout < 0) {
+ throw new TimeoutException(
+ "event not found within the timeout: " + stream.dump());
+ }
+ final Predicate<ImeEvent> combinedCondition;
+ switch (filterMode) {
+ case CHECK_ALL:
+ combinedCondition = condition;
+ break;
+ case CHECK_ENTER_EVENT_ONLY:
+ combinedCondition = event -> event.isEnterEvent() && condition.test(event);
+ break;
+ case CHECK_EXIT_EVENT_ONLY:
+ combinedCondition = event -> !event.isEnterEvent() && condition.test(event);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown filterMode " + filterMode);
+ }
+ result = stream.seekToFirst(combinedCondition);
+ if (result.isPresent()) {
+ break;
+ }
+ Thread.sleep(TIME_SLICE);
+ timeout -= TIME_SLICE;
+ }
+ final ImeEvent event = result.get();
+ if (event == null) {
+ throw new NullPointerException("found event is null: " + stream.dump());
+ }
+ stream.skip(1);
+ return event;
+ } catch (InterruptedException e) {
+ throw new RuntimeException("expectEvent failed: " + stream.dump(), e);
+ }
+ }
+
+ /**
+ * Checks if {@param eventName} has occurred on the EditText(or TextView) of the current
+ * activity.
+ * @param eventName event name to check
+ * @param marker Test marker set to {@link android.widget.EditText#setPrivateImeOptions(String)}
+ * @return true if event occurred.
+ */
+ public static Predicate<ImeEvent> editorMatcher(
+ @NonNull String eventName, @NonNull String marker) {
+ return event -> {
+ if (!TextUtils.equals(eventName, event.getEventName())) {
+ return false;
+ }
+ final EditorInfo editorInfo = event.getArguments().getParcelable("editorInfo");
+ return TextUtils.equals(marker, editorInfo.privateImeOptions);
+ };
+ }
+
+ /**
+ * Checks if {@code eventName} has occurred on the EditText(or TextView) of the current
+ * activity.
+ * @param eventName event name to check
+ * @param fieldId typically same as {@link android.view.View#getId()}.
+ * @return true if event occurred.
+ */
+ public static Predicate<ImeEvent> editorMatcher(@NonNull String eventName, int fieldId) {
+ return event -> {
+ if (!TextUtils.equals(eventName, event.getEventName())) {
+ return false;
+ }
+ final EditorInfo editorInfo = event.getArguments().getParcelable("editorInfo");
+ return fieldId == editorInfo.fieldId;
+ };
+ }
+
+ /**
+ * Wait until an event that matches the given command is consumed by the {@link MockIme}.
+ *
+ * <p>For convenience, this method automatically filter out enter events (events that return
+ * {@code true} from {@link ImeEvent#isEnterEvent()}.</p>
+ *
+ * <p>TODO: Consider renaming this to {@code expectCommandConsumed} or something like that.</p>
+ *
+ * @param stream {@link ImeEventStream} to be checked.
+ * @param command {@link ImeCommand} to be waited for.
+ * @param timeout timeout in millisecond
+ * @return {@link ImeEvent} found
+ * @throws TimeoutException when the no event is matched to the given condition within
+ * {@code timeout}
+ */
+ @NonNull
+ public static ImeEvent expectCommand(@NonNull ImeEventStream stream,
+ @NonNull ImeCommand command, long timeout) throws TimeoutException {
+ final Predicate<ImeEvent> predicate = event -> {
+ if (!TextUtils.equals("onHandleCommand", event.getEventName())) {
+ return false;
+ }
+ final ImeCommand eventCommand =
+ ImeCommand.fromBundle(event.getArguments().getBundle("command"));
+ return eventCommand.getId() == command.getId();
+ };
+ return expectEvent(stream, predicate, EventFilterMode.CHECK_EXIT_EVENT_ONLY, timeout);
+ }
+
+ /**
+ * Assert that an event that matches the given {@code condition} will no be found in the stream
+ * within the given {@code timeout}.
+ *
+ * <p>When this method succeeds, the stream position will not change.</p>
+ *
+ * <p>For convenience, this method automatically filter out exit events (events that return
+ * {@code false} from {@link ImeEvent#isEnterEvent()}.</p>
+ *
+ * <p>TODO: Consider renaming this to {@code notExpectEventEnter} or something like that.</p>
+ *
+ * @param stream {@link ImeEventStream} to be checked.
+ * @param condition the event condition to be matched
+ * @param timeout timeout in millisecond
+ * @throws AssertionError if such an event is found within the given {@code timeout}
+ */
+ public static void notExpectEvent(@NonNull ImeEventStream stream,
+ @NonNull Predicate<ImeEvent> condition, long timeout) {
+ notExpectEvent(stream, condition, EventFilterMode.CHECK_ENTER_EVENT_ONLY, timeout);
+ }
+
+ /**
+ * Assert that an event that matches the given {@code condition} will no be found in the stream
+ * within the given {@code timeout}.
+ *
+ * <p>When this method succeeds, the stream position will not change.</p>
+ *
+ * @param stream {@link ImeEventStream} to be checked.
+ * @param condition the event condition to be matched
+ * @param filterMode controls how events are filtered out
+ * @param timeout timeout in millisecond
+ * @throws AssertionError if such an event is found within the given {@code timeout}
+ */
+ public static void notExpectEvent(@NonNull ImeEventStream stream,
+ @NonNull Predicate<ImeEvent> condition, EventFilterMode filterMode, long timeout) {
+ final Predicate<ImeEvent> combinedCondition;
+ switch (filterMode) {
+ case CHECK_ALL:
+ combinedCondition = condition;
+ break;
+ case CHECK_ENTER_EVENT_ONLY:
+ combinedCondition = event -> event.isEnterEvent() && condition.test(event);
+ break;
+ case CHECK_EXIT_EVENT_ONLY:
+ combinedCondition = event -> !event.isEnterEvent() && condition.test(event);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown filterMode " + filterMode);
+ }
+ try {
+ while (true) {
+ if (timeout < 0) {
+ return;
+ }
+ if (stream.findFirst(combinedCondition).isPresent()) {
+ throw new AssertionError("notExpectEvent failed: " + stream.dump());
+ }
+ Thread.sleep(TIME_SLICE);
+ timeout -= TIME_SLICE;
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("notExpectEvent failed: " + stream.dump(), e);
+ }
+ }
+
+ /**
+ * A specialized version of {@link #expectEvent(ImeEventStream, Predicate, long)} to wait for
+ * {@link android.view.inputmethod.InputMethod#bindInput(InputBinding)}.
+ *
+ * @param stream {@link ImeEventStream} to be checked.
+ * @param targetProcessPid PID to be matched to {@link InputBinding#getPid()}
+ * @param timeout timeout in millisecond
+ * @throws TimeoutException when "bindInput" is not called within {@code timeout} msec
+ */
+ public static void expectBindInput(@NonNull ImeEventStream stream, int targetProcessPid,
+ long timeout) throws TimeoutException {
+ expectEvent(stream, event -> {
+ if (!TextUtils.equals("bindInput", event.getEventName())) {
+ return false;
+ }
+ final InputBinding binding = event.getArguments().getParcelable("binding");
+ return binding.getPid() == targetProcessPid;
+ }, EventFilterMode.CHECK_EXIT_EVENT_ONLY, timeout);
+ }
+
+ /**
+ * Waits until {@code MockIme} does not send {@code "onInputViewLayoutChanged"} event
+ * for a certain period of time ({@code stableThresholdTime} msec).
+ *
+ * <p>When this returns non-null {@link ImeLayoutInfo}, the stream position will be set to
+ * the next event of the returned layout event. Otherwise this method does not change stream
+ * position.</p>
+ * @param stream {@link ImeEventStream} to be checked.
+ * @param stableThresholdTime threshold time to consider that {@link MockIme}'s layout is
+ * stable, in millisecond
+ * @return last {@link ImeLayoutInfo} if {@link MockIme} sent one or more
+ * {@code "onInputViewLayoutChanged"} event. Otherwise {@code null}
+ */
+ public static ImeLayoutInfo waitForInputViewLayoutStable(@NonNull ImeEventStream stream,
+ long stableThresholdTime) {
+ ImeLayoutInfo lastLayout = null;
+ final Predicate<ImeEvent> layoutFilter = event ->
+ !event.isEnterEvent() && event.getEventName().equals("onInputViewLayoutChanged");
+ try {
+ long deadline = SystemClock.elapsedRealtime() + stableThresholdTime;
+ while (true) {
+ if (deadline < SystemClock.elapsedRealtime()) {
+ return lastLayout;
+ }
+ final Optional<ImeEvent> event = stream.seekToFirst(layoutFilter);
+ if (event.isPresent()) {
+ // Remember the last event and extend the deadline again.
+ lastLayout = ImeLayoutInfo.readFromBundle(event.get().getArguments());
+ deadline = SystemClock.elapsedRealtime() + stableThresholdTime;
+ stream.skip(1);
+ }
+ Thread.sleep(TIME_SLICE);
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("notExpectEvent failed: " + stream.dump(), e);
+ }
+ }
+}
diff --git a/tests/autofillservice/src/com/android/cts/mockime/ImeLayoutInfo.java b/tests/autofillservice/src/com/android/cts/mockime/ImeLayoutInfo.java
new file mode 100644
index 0000000..77718ea
--- /dev/null
+++ b/tests/autofillservice/src/com/android/cts/mockime/ImeLayoutInfo.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.mockime;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import android.view.Display;
+import android.view.View;
+import android.view.WindowInsets;
+
+/**
+ * A collection of layout-related information when
+ * {@link View.OnLayoutChangeListener#onLayoutChange(View, int, int, int, int, int, int, int, int)}
+ * is called back for the input view (the view returned from {@link MockIme#onCreateInputView()}).
+ */
+public final class ImeLayoutInfo {
+
+ private static final String NEW_LAYOUT_KEY = "newLayout";
+ private static final String OLD_LAYOUT_KEY = "oldLayout";
+ private static final String VIEW_ORIGIN_ON_SCREEN_KEY = "viewOriginOnScreen";
+ private static final String DISPLAY_SIZE_KEY = "displaySize";
+ private static final String SYSTEM_WINDOW_INSET_KEY = "systemWindowInset";
+ private static final String STABLE_INSET_KEY = "stableInset";
+
+ @NonNull
+ private final Rect mNewLayout;
+ @NonNull
+ private final Rect mOldLayout;
+ @Nullable
+ private Point mViewOriginOnScreen;
+ @Nullable
+ private Point mDisplaySize;
+ @Nullable
+ private Rect mSystemWindowInset;
+ @Nullable
+ private Rect mStableInset;
+
+ /**
+ * Returns the bounding box of the {@link View} passed to
+ * {@link android.inputmethodservice.InputMethodService#onCreateInputView()} in screen
+ * coordinates.
+ *
+ * <p>Currently this method assumes that no {@link View} in the hierarchy uses
+ * transformations such as {@link View#setRotation(float)}.</p>
+ *
+ * @return Region in screen coordinates.
+ */
+ @Nullable
+ public Rect getInputViewBoundsInScreen() {
+ return new Rect(
+ mViewOriginOnScreen.x, mViewOriginOnScreen.y,
+ mViewOriginOnScreen.x + mNewLayout.width(),
+ mViewOriginOnScreen.y + mNewLayout.height());
+ }
+
+ /**
+ * Returns the screen area in screen coordinates that does not overlap with the system
+ * window inset, which represents the area of a full-screen window that is partially or
+ * fully obscured by the status bar, navigation bar, IME or other system windows.
+ *
+ * <p>May return {@code null} when this information is not yet ready.</p>
+ *
+ * @return Region in screen coordinates. {@code null} when it is not available
+ *
+ * @see WindowInsets#hasSystemWindowInsets()
+ * @see WindowInsets#getSystemWindowInsetBottom()
+ * @see WindowInsets#getSystemWindowInsetLeft()
+ * @see WindowInsets#getSystemWindowInsetRight()
+ * @see WindowInsets#getSystemWindowInsetTop()
+ */
+ @Nullable
+ public Rect getScreenRectWithoutSystemWindowInset() {
+ if (mDisplaySize == null) {
+ return null;
+ }
+ if (mSystemWindowInset == null) {
+ return new Rect(0, 0, mDisplaySize.x, mDisplaySize.y);
+ }
+ return new Rect(mSystemWindowInset.left, mSystemWindowInset.top,
+ mDisplaySize.x - mSystemWindowInset.right,
+ mDisplaySize.y - mSystemWindowInset.bottom);
+ }
+
+ /**
+ * Returns the screen area in screen coordinates that does not overlap with the stable
+ * inset, which represents the area of a full-screen window that <b>may</b> be partially or
+ * fully obscured by the system UI elements.
+ *
+ * <p>May return {@code null} when this information is not yet ready.</p>
+ *
+ * @return Region in screen coordinates. {@code null} when it is not available
+ *
+ * @see WindowInsets#hasStableInsets()
+ * @see WindowInsets#getStableInsetBottom()
+ * @see WindowInsets#getStableInsetLeft()
+ * @see WindowInsets#getStableInsetRight()
+ * @see WindowInsets#getStableInsetTop()
+ */
+ @Nullable
+ public Rect getScreenRectWithoutStableInset() {
+ if (mDisplaySize == null) {
+ return null;
+ }
+ if (mStableInset == null) {
+ return new Rect(0, 0, mDisplaySize.x, mDisplaySize.y);
+ }
+ return new Rect(mStableInset.left, mStableInset.top,
+ mDisplaySize.x - mStableInset.right,
+ mDisplaySize.y - mStableInset.bottom);
+ }
+
+ ImeLayoutInfo(@NonNull Rect newLayout, @NonNull Rect oldLayout,
+ @NonNull Point viewOriginOnScreen, @Nullable Point displaySize,
+ @Nullable Rect systemWindowInset, @Nullable Rect stableInset) {
+ mNewLayout = new Rect(newLayout);
+ mOldLayout = new Rect(oldLayout);
+ mViewOriginOnScreen = new Point(viewOriginOnScreen);
+ mDisplaySize = new Point(displaySize);
+ mSystemWindowInset = systemWindowInset;
+ mStableInset = stableInset;
+ }
+
+ void writeToBundle(@NonNull Bundle bundle) {
+ bundle.putParcelable(NEW_LAYOUT_KEY, mNewLayout);
+ bundle.putParcelable(OLD_LAYOUT_KEY, mOldLayout);
+ bundle.putParcelable(VIEW_ORIGIN_ON_SCREEN_KEY, mViewOriginOnScreen);
+ bundle.putParcelable(DISPLAY_SIZE_KEY, mDisplaySize);
+ bundle.putParcelable(SYSTEM_WINDOW_INSET_KEY, mSystemWindowInset);
+ bundle.putParcelable(STABLE_INSET_KEY, mStableInset);
+ }
+
+ static ImeLayoutInfo readFromBundle(@NonNull Bundle bundle) {
+ final Rect newLayout = bundle.getParcelable(NEW_LAYOUT_KEY);
+ final Rect oldLayout = bundle.getParcelable(OLD_LAYOUT_KEY);
+ final Point viewOrigin = bundle.getParcelable(VIEW_ORIGIN_ON_SCREEN_KEY);
+ final Point displaySize = bundle.getParcelable(DISPLAY_SIZE_KEY);
+ final Rect systemWindowInset = bundle.getParcelable(SYSTEM_WINDOW_INSET_KEY);
+ final Rect stableInset = bundle.getParcelable(STABLE_INSET_KEY);
+
+ return new ImeLayoutInfo(newLayout, oldLayout, viewOrigin, displaySize, systemWindowInset,
+ stableInset);
+ }
+
+ static ImeLayoutInfo fromLayoutListenerCallback(View v, int left, int top, int right,
+ int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ final Rect newLayout = new Rect(left, top, right, bottom);
+ final Rect oldLayout = new Rect(oldLeft, oldTop, oldRight, oldBottom);
+ final int[] viewOriginArray = new int[2];
+ v.getLocationOnScreen(viewOriginArray);
+ final Point viewOrigin = new Point(viewOriginArray[0], viewOriginArray[1]);
+ final Display display = v.getDisplay();
+ final Point displaySize;
+ if (display != null) {
+ displaySize = new Point();
+ display.getRealSize(displaySize);
+ } else {
+ displaySize = null;
+ }
+ final WindowInsets windowInsets = v.getRootWindowInsets();
+ final Rect systemWindowInset;
+ if (windowInsets != null && windowInsets.hasSystemWindowInsets()) {
+ systemWindowInset = new Rect(
+ windowInsets.getSystemWindowInsetLeft(), windowInsets.getSystemWindowInsetTop(),
+ windowInsets.getSystemWindowInsetRight(),
+ windowInsets.getSystemWindowInsetBottom());
+ } else {
+ systemWindowInset = null;
+ }
+ final Rect stableInset;
+ if (windowInsets != null && windowInsets.hasStableInsets()) {
+ stableInset = new Rect(
+ windowInsets.getStableInsetLeft(), windowInsets.getStableInsetTop(),
+ windowInsets.getStableInsetRight(), windowInsets.getStableInsetBottom());
+ } else {
+ stableInset = null;
+ }
+ return new ImeLayoutInfo(newLayout, oldLayout, viewOrigin, displaySize, systemWindowInset,
+ stableInset);
+ }
+}
diff --git a/tests/autofillservice/src/com/android/cts/mockime/ImeSettings.java b/tests/autofillservice/src/com/android/cts/mockime/ImeSettings.java
new file mode 100644
index 0000000..21c25be
--- /dev/null
+++ b/tests/autofillservice/src/com/android/cts/mockime/ImeSettings.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.mockime;
+
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * An immutable data store to control the behavior of {@link MockIme}.
+ */
+public class ImeSettings {
+
+ @NonNull
+ private final String mClientPackageName;
+
+ @NonNull
+ private final String mEventCallbackActionName;
+
+ private static final String EVENT_CALLBACK_INTENT_ACTION_KEY = "eventCallbackActionName";
+ private static final String DATA_KEY = "data";
+
+ private static final String BACKGROUND_COLOR_KEY = "BackgroundColor";
+ private static final String NAVIGATION_BAR_COLOR_KEY = "NavigationBarColor";
+ private static final String INPUT_VIEW_HEIGHT_WITHOUT_SYSTEM_WINDOW_INSET =
+ "InputViewHeightWithoutSystemWindowInset";
+ private static final String WINDOW_FLAGS = "WindowFlags";
+ private static final String WINDOW_FLAGS_MASK = "WindowFlagsMask";
+ private static final String FULLSCREEN_MODE_ALLOWED = "FullscreenModeAllowed";
+ private static final String INPUT_VIEW_SYSTEM_UI_VISIBILITY = "InputViewSystemUiVisibility";
+ private static final String HARD_KEYBOARD_CONFIGURATION_BEHAVIOR_ALLOWED =
+ "HardKeyboardConfigurationBehaviorAllowed";
+
+ @NonNull
+ private final PersistableBundle mBundle;
+
+ ImeSettings(@NonNull String clientPackageName, @NonNull Bundle bundle) {
+ mClientPackageName = clientPackageName;
+ mEventCallbackActionName = bundle.getString(EVENT_CALLBACK_INTENT_ACTION_KEY);
+ mBundle = bundle.getParcelable(DATA_KEY);
+ }
+
+ @Nullable
+ String getEventCallbackActionName() {
+ return mEventCallbackActionName;
+ }
+
+ @NonNull
+ String getClientPackageName() {
+ return mClientPackageName;
+ }
+
+ public boolean fullscreenModeAllowed(boolean defaultValue) {
+ return mBundle.getBoolean(FULLSCREEN_MODE_ALLOWED, defaultValue);
+ }
+
+ @ColorInt
+ public int getBackgroundColor(@ColorInt int defaultColor) {
+ return mBundle.getInt(BACKGROUND_COLOR_KEY, defaultColor);
+ }
+
+ public boolean hasNavigationBarColor() {
+ return mBundle.keySet().contains(NAVIGATION_BAR_COLOR_KEY);
+ }
+
+ @ColorInt
+ public int getNavigationBarColor() {
+ return mBundle.getInt(NAVIGATION_BAR_COLOR_KEY);
+ }
+
+ public int getInputViewHeightWithoutSystemWindowInset(int defaultHeight) {
+ return mBundle.getInt(INPUT_VIEW_HEIGHT_WITHOUT_SYSTEM_WINDOW_INSET, defaultHeight);
+ }
+
+ public int getWindowFlags(int defaultFlags) {
+ return mBundle.getInt(WINDOW_FLAGS, defaultFlags);
+ }
+
+ public int getWindowFlagsMask(int defaultFlags) {
+ return mBundle.getInt(WINDOW_FLAGS_MASK, defaultFlags);
+ }
+
+ public int getInputViewSystemUiVisibility(int defaultFlags) {
+ return mBundle.getInt(INPUT_VIEW_SYSTEM_UI_VISIBILITY, defaultFlags);
+ }
+
+ public boolean getHardKeyboardConfigurationBehaviorAllowed(boolean defaultValue) {
+ return mBundle.getBoolean(HARD_KEYBOARD_CONFIGURATION_BEHAVIOR_ALLOWED, defaultValue);
+ }
+
+ static Bundle serializeToBundle(@NonNull String eventCallbackActionName,
+ @Nullable Builder builder) {
+ final Bundle result = new Bundle();
+ result.putString(EVENT_CALLBACK_INTENT_ACTION_KEY, eventCallbackActionName);
+ result.putParcelable(DATA_KEY, builder != null ? builder.mBundle : PersistableBundle.EMPTY);
+ return result;
+ }
+
+ /**
+ * The builder class for {@link ImeSettings}.
+ */
+ public static final class Builder {
+ private final PersistableBundle mBundle = new PersistableBundle();
+
+ /**
+ * Controls whether fullscreen mode is allowed or not.
+ *
+ * <p>By default, fullscreen mode is not allowed in {@link MockIme}.</p>
+ *
+ * @param allowed {@code true} if fullscreen mode is allowed
+ * @see MockIme#onEvaluateFullscreenMode()
+ */
+ public Builder setFullscreenModeAllowed(boolean allowed) {
+ mBundle.putBoolean(FULLSCREEN_MODE_ALLOWED, allowed);
+ return this;
+ }
+
+ /**
+ * Sets the background color of the {@link MockIme}.
+ * @param color background color to be used
+ */
+ public Builder setBackgroundColor(@ColorInt int color) {
+ mBundle.putInt(BACKGROUND_COLOR_KEY, color);
+ return this;
+ }
+
+ /**
+ * Sets the color to be passed to {@link android.view.Window#setNavigationBarColor(int)}.
+ *
+ * @param color color to be passed to {@link android.view.Window#setNavigationBarColor(int)}
+ * @see android.view.View
+ */
+ public Builder setNavigationBarColor(@ColorInt int color) {
+ mBundle.putInt(NAVIGATION_BAR_COLOR_KEY, color);
+ return this;
+ }
+
+ /**
+ * Sets the input view height measured from the bottom system window inset.
+ * @param height height of the soft input view. This does not include the system window
+ * inset such as navigation bar
+ */
+ public Builder setInputViewHeightWithoutSystemWindowInset(int height) {
+ mBundle.putInt(INPUT_VIEW_HEIGHT_WITHOUT_SYSTEM_WINDOW_INSET, height);
+ return this;
+ }
+
+ /**
+ * Sets window flags to be specified to {@link android.view.Window#setFlags(int, int)} of
+ * the main {@link MockIme} window.
+ *
+ * <p>When {@link android.view.WindowManager.LayoutParams#FLAG_LAYOUT_IN_OVERSCAN} is set,
+ * {@link MockIme} tries to render the navigation bar by itself.</p>
+ *
+ * @param flags flags to be specified
+ * @param flagsMask mask bits that specify what bits need to be cleared before setting
+ * {@code flags}
+ * @see android.view.WindowManager
+ */
+ public Builder setWindowFlags(int flags, int flagsMask) {
+ mBundle.putInt(WINDOW_FLAGS, flags);
+ mBundle.putInt(WINDOW_FLAGS_MASK, flagsMask);
+ return this;
+ }
+
+ /**
+ * Sets flags to be specified to {@link android.view.View#setSystemUiVisibility(int)} of
+ * the main soft input view (the returned view from {@link MockIme#onCreateInputView()}).
+ *
+ * @param visibilityFlags flags to be specified
+ * @see android.view.View
+ */
+ public Builder setInputViewSystemUiVisibility(int visibilityFlags) {
+ mBundle.putInt(INPUT_VIEW_SYSTEM_UI_VISIBILITY, visibilityFlags);
+ return this;
+ }
+
+ /**
+ * Controls whether {@link MockIme} is allowed to change the behavior based on
+ * {@link android.content.res.Configuration#keyboard} and
+ * {@link android.content.res.Configuration#hardKeyboardHidden}.
+ *
+ * <p>Methods in {@link android.inputmethodservice.InputMethodService} such as
+ * {@link android.inputmethodservice.InputMethodService#onEvaluateInputViewShown()} and
+ * {@link android.inputmethodservice.InputMethodService#onShowInputRequested(int, boolean)}
+ * change their behaviors when a hardware keyboard is attached. This is confusing when
+ * writing tests so by default {@link MockIme} tries to cancel those behaviors. This
+ * settings re-enables such a behavior.</p>
+ *
+ * @param allowed {@code true} when {@link MockIme} is allowed to change the behavior when
+ * a hardware keyboard is attached
+ *
+ * @see android.inputmethodservice.InputMethodService#onEvaluateInputViewShown()
+ * @see android.inputmethodservice.InputMethodService#onShowInputRequested(int, boolean)
+ */
+ public Builder setHardKeyboardConfigurationBehaviorAllowed(boolean allowed) {
+ mBundle.putBoolean(HARD_KEYBOARD_CONFIGURATION_BEHAVIOR_ALLOWED, allowed);
+ return this;
+ }
+ }
+}
diff --git a/tests/autofillservice/src/com/android/cts/mockime/ImeState.java b/tests/autofillservice/src/com/android/cts/mockime/ImeState.java
new file mode 100644
index 0000000..0135b30
--- /dev/null
+++ b/tests/autofillservice/src/com/android/cts/mockime/ImeState.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.mockime;
+
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * An immutable object that stores several runtime state of {@link MockIme}.
+ */
+public final class ImeState {
+ private final boolean mHasInputBinding;
+ private final boolean mHasDummyInputConnection;
+
+ /**
+ * @return {@code true} if {@link MockIme#getCurrentInputBinding()} returned non-null
+ * {@link android.view.inputmethod.InputBinding} when this snapshot was taken.
+ */
+ public boolean hasInputBinding() {
+ return mHasInputBinding;
+ }
+
+ /**
+ * @return {@code true} if {@link MockIme#getCurrentInputConnection()} returned non-dummy
+ * {@link android.view.inputmethod.InputConnection} when this snapshot was taken.
+ */
+ public boolean hasDummyInputConnection() {
+ return mHasDummyInputConnection;
+ }
+
+ ImeState(boolean hasInputBinding, boolean hasDummyInputConnection) {
+ mHasInputBinding = hasInputBinding;
+ mHasDummyInputConnection = hasDummyInputConnection;
+ }
+
+ @NonNull
+ Bundle toBundle() {
+ final Bundle bundle = new Bundle();
+ bundle.putBoolean("mHasInputBinding", mHasInputBinding);
+ bundle.putBoolean("mHasDummyInputConnection", mHasDummyInputConnection);
+ return bundle;
+ }
+
+ @Nullable
+ static ImeState fromBundle(@Nullable Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ final boolean hasInputBinding = bundle.getBoolean("mHasInputBinding");
+ final boolean hasDummyInputConnection = bundle.getBoolean("mHasDummyInputConnection");
+ return new ImeState(hasInputBinding, hasDummyInputConnection);
+ }
+}
diff --git a/tests/autofillservice/src/com/android/cts/mockime/MockIme.java b/tests/autofillservice/src/com/android/cts/mockime/MockIme.java
new file mode 100644
index 0000000..52a1182
--- /dev/null
+++ b/tests/autofillservice/src/com/android/cts/mockime/MockIme.java
@@ -0,0 +1,686 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.mockime;
+
+import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.inputmethodservice.InputMethodService;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Process;
+import android.os.ResultReceiver;
+import android.os.SystemClock;
+import androidx.annotation.AnyThread;
+import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputBinding;
+import android.view.inputmethod.InputMethod;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * Mock IME for end-to-end tests.
+ */
+public final class MockIme extends InputMethodService {
+
+ private static final String TAG = "MockIme";
+
+ private static final String PACKAGE_NAME = "android.autofillservice.cts";
+
+ static ComponentName getComponentName() {
+ return new ComponentName(PACKAGE_NAME, MockIme.class.getName());
+ }
+
+ static String getImeId() {
+ return getComponentName().flattenToShortString();
+ }
+
+ static String getCommandActionName(@NonNull String eventActionName) {
+ return eventActionName + ".command";
+ }
+
+ private final HandlerThread mHandlerThread = new HandlerThread("CommandReceiver");
+
+ private final Handler mMainHandler = new Handler();
+
+ private static final class CommandReceiver extends BroadcastReceiver {
+ @NonNull
+ private final String mActionName;
+ @NonNull
+ private final Consumer<ImeCommand> mOnReceiveCommand;
+
+ CommandReceiver(@NonNull String actionName,
+ @NonNull Consumer<ImeCommand> onReceiveCommand) {
+ mActionName = actionName;
+ mOnReceiveCommand = onReceiveCommand;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (TextUtils.equals(mActionName, intent.getAction())) {
+ mOnReceiveCommand.accept(ImeCommand.fromBundle(intent.getExtras()));
+ }
+ }
+ }
+
+ @WorkerThread
+ private void onReceiveCommand(@NonNull ImeCommand command) {
+ getTracer().onReceiveCommand(command, () -> {
+ if (command.shouldDispatchToMainThread()) {
+ mMainHandler.post(() -> onHandleCommand(command));
+ } else {
+ onHandleCommand(command);
+ }
+ });
+ }
+
+ @AnyThread
+ private void onHandleCommand(@NonNull ImeCommand command) {
+ getTracer().onHandleCommand(command, () -> {
+ if (command.shouldDispatchToMainThread()) {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ throw new IllegalStateException("command " + command
+ + " should be handled on the main thread");
+ }
+ switch (command.getName()) {
+ case "commitText": {
+ final CharSequence text = command.getExtras().getString("text");
+ final int newCursorPosition =
+ command.getExtras().getInt("newCursorPosition");
+ getCurrentInputConnection().commitText(text, newCursorPosition);
+ break;
+ }
+ case "setBackDisposition": {
+ final int backDisposition =
+ command.getExtras().getInt("backDisposition");
+ setBackDisposition(backDisposition);
+ break;
+ }
+ case "requestHideSelf": {
+ final int flags = command.getExtras().getInt("flags");
+ requestHideSelf(flags);
+ break;
+ }
+ case "requestShowSelf": {
+ final int flags = command.getExtras().getInt("flags");
+ requestShowSelf(flags);
+ break;
+ }
+ }
+ }
+ });
+ }
+
+ @Nullable
+ private CommandReceiver mCommandReceiver;
+
+ @Nullable
+ private ImeSettings mSettings;
+
+ private final AtomicReference<String> mImeEventActionName = new AtomicReference<>();
+
+ @Nullable
+ String getImeEventActionName() {
+ return mImeEventActionName.get();
+ }
+
+ private final AtomicReference<String> mClientPackageName = new AtomicReference<>();
+
+ @Nullable
+ String getClientPackageName() {
+ return mClientPackageName.get();
+ }
+
+ private class MockInputMethodImpl extends InputMethodImpl {
+ @Override
+ public void showSoftInput(int flags, ResultReceiver resultReceiver) {
+ getTracer().showSoftInput(flags, resultReceiver,
+ () -> super.showSoftInput(flags, resultReceiver));
+ }
+
+ @Override
+ public void hideSoftInput(int flags, ResultReceiver resultReceiver) {
+ getTracer().hideSoftInput(flags, resultReceiver,
+ () -> super.hideSoftInput(flags, resultReceiver));
+ }
+
+ @Override
+ public void attachToken(IBinder token) {
+ getTracer().attachToken(token, () -> super.attachToken(token));
+ }
+
+ @Override
+ public void bindInput(InputBinding binding) {
+ getTracer().bindInput(binding, () -> super.bindInput(binding));
+ }
+
+ @Override
+ public void unbindInput() {
+ getTracer().unbindInput(() -> super.unbindInput());
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ // Initialize minimum settings to send events in Tracer#onCreate().
+ mSettings = SettingsProvider.getSettings();
+ if (mSettings == null) {
+ throw new IllegalStateException("Settings file is not found. "
+ + "Make sure MockImeSession.create() is used to launch Mock IME.");
+ }
+ mClientPackageName.set(mSettings.getClientPackageName());
+ mImeEventActionName.set(mSettings.getEventCallbackActionName());
+
+ getTracer().onCreate(() -> {
+ super.onCreate();
+ mHandlerThread.start();
+ final String actionName = getCommandActionName(mSettings.getEventCallbackActionName());
+ mCommandReceiver = new CommandReceiver(actionName, this::onReceiveCommand);
+ final IntentFilter filter = new IntentFilter(actionName);
+ final Handler handler = new Handler(mHandlerThread.getLooper());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ registerReceiver(mCommandReceiver, filter, null /* broadcastPermission */, handler,
+ Context.RECEIVER_VISIBLE_TO_INSTANT_APPS);
+ } else {
+ registerReceiver(mCommandReceiver, filter, null /* broadcastPermission */, handler);
+ }
+
+ final int windowFlags = mSettings.getWindowFlags(0);
+ final int windowFlagsMask = mSettings.getWindowFlagsMask(0);
+ if (windowFlags != 0 || windowFlagsMask != 0) {
+ final int prevFlags = getWindow().getWindow().getAttributes().flags;
+ getWindow().getWindow().setFlags(windowFlags, windowFlagsMask);
+ // For some reasons, seems that we need to post another requestLayout() when
+ // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS bit is changed.
+ // TODO: Investigate the reason.
+ if ((windowFlagsMask & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0) {
+ final boolean hadFlag = (prevFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
+ final boolean hasFlag = (windowFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
+ if (hadFlag != hasFlag) {
+ final View decorView = getWindow().getWindow().getDecorView();
+ decorView.post(() -> decorView.requestLayout());
+ }
+ }
+ }
+
+ if (mSettings.hasNavigationBarColor()) {
+ getWindow().getWindow().setNavigationBarColor(mSettings.getNavigationBarColor());
+ }
+ });
+ }
+
+ @Override
+ public void onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly) {
+ getTracer().onConfigureWindow(win, isFullscreen, isCandidatesOnly,
+ () -> super.onConfigureWindow(win, isFullscreen, isCandidatesOnly));
+ }
+
+ @Override
+ public boolean onEvaluateFullscreenMode() {
+ return getTracer().onEvaluateFullscreenMode(() ->
+ mSettings.fullscreenModeAllowed(false) && super.onEvaluateFullscreenMode());
+ }
+
+ private static final class KeyboardLayoutView extends LinearLayout {
+ @NonNull
+ private final ImeSettings mSettings;
+ @NonNull
+ private final View.OnLayoutChangeListener mLayoutListener;
+
+ KeyboardLayoutView(Context context, @NonNull ImeSettings imeSettings,
+ @Nullable Consumer<ImeLayoutInfo> onInputViewLayoutChangedCallback) {
+ super(context);
+
+ mSettings = imeSettings;
+
+ setOrientation(VERTICAL);
+
+ final int defaultBackgroundColor =
+ getResources().getColor(android.R.color.holo_orange_dark, null);
+ setBackgroundColor(mSettings.getBackgroundColor(defaultBackgroundColor));
+
+ final int mainSpacerHeight = mSettings.getInputViewHeightWithoutSystemWindowInset(
+ LayoutParams.WRAP_CONTENT);
+ {
+ final RelativeLayout layout = new RelativeLayout(getContext());
+ final TextView textView = new TextView(getContext());
+ final RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.MATCH_PARENT,
+ RelativeLayout.LayoutParams.WRAP_CONTENT);
+ params.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
+ textView.setLayoutParams(params);
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
+ textView.setGravity(Gravity.CENTER);
+ textView.setText(getImeId());
+ layout.addView(textView);
+ addView(layout, LayoutParams.MATCH_PARENT, mainSpacerHeight);
+ }
+
+ final int systemUiVisibility = mSettings.getInputViewSystemUiVisibility(0);
+ if (systemUiVisibility != 0) {
+ setSystemUiVisibility(systemUiVisibility);
+ }
+
+ mLayoutListener = (View v, int left, int top, int right, int bottom, int oldLeft,
+ int oldTop, int oldRight, int oldBottom) ->
+ onInputViewLayoutChangedCallback.accept(
+ ImeLayoutInfo.fromLayoutListenerCallback(
+ v, left, top, right, bottom, oldLeft, oldTop, oldRight,
+ oldBottom));
+ this.addOnLayoutChangeListener(mLayoutListener);
+ }
+
+ private void updateBottomPaddingIfNecessary(int newPaddingBottom) {
+ if (getPaddingBottom() != newPaddingBottom) {
+ setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), newPaddingBottom);
+ }
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ if (insets.isConsumed()
+ || (getSystemUiVisibility() & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0) {
+ // In this case we are not interested in consuming NavBar region.
+ // Make sure that the bottom padding is empty.
+ updateBottomPaddingIfNecessary(0);
+ return insets;
+ }
+
+ // In some cases the bottom system window inset is not a navigation bar. Wear devices
+ // that have bottom chin are examples. For now, assume that it's a navigation bar if it
+ // has the same height as the root window's stable bottom inset.
+ final WindowInsets rootWindowInsets = getRootWindowInsets();
+ if (rootWindowInsets != null && (rootWindowInsets.getStableInsetBottom()
+ != insets.getSystemWindowInsetBottom())) {
+ // This is probably not a NavBar.
+ updateBottomPaddingIfNecessary(0);
+ return insets;
+ }
+
+ final int possibleNavBarHeight = insets.getSystemWindowInsetBottom();
+ updateBottomPaddingIfNecessary(possibleNavBarHeight);
+ return possibleNavBarHeight <= 0
+ ? insets
+ : insets.replaceSystemWindowInsets(
+ insets.getSystemWindowInsetLeft(),
+ insets.getSystemWindowInsetTop(),
+ insets.getSystemWindowInsetRight(),
+ 0 /* bottom */);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ removeOnLayoutChangeListener(mLayoutListener);
+ }
+ }
+
+ private void onInputViewLayoutChanged(@NonNull ImeLayoutInfo layoutInfo) {
+ getTracer().onInputViewLayoutChanged(layoutInfo, () -> { });
+ }
+
+ @Override
+ public View onCreateInputView() {
+ return getTracer().onCreateInputView(() ->
+ new KeyboardLayoutView(this, mSettings, this::onInputViewLayoutChanged));
+ }
+
+ @Override
+ public void onStartInput(EditorInfo editorInfo, boolean restarting) {
+ getTracer().onStartInput(editorInfo, restarting,
+ () -> super.onStartInput(editorInfo, restarting));
+ }
+
+ @Override
+ public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
+ getTracer().onStartInputView(editorInfo, restarting,
+ () -> super.onStartInputView(editorInfo, restarting));
+ }
+
+ @Override
+ public void onFinishInputView(boolean finishingInput) {
+ getTracer().onFinishInputView(finishingInput,
+ () -> super.onFinishInputView(finishingInput));
+ }
+
+ @Override
+ public void onFinishInput() {
+ getTracer().onFinishInput(() -> super.onFinishInput());
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return getTracer().onKeyDown(keyCode, event, () -> super.onKeyDown(keyCode, event));
+ }
+
+ @CallSuper
+ public boolean onEvaluateInputViewShown() {
+ return getTracer().onEvaluateInputViewShown(() -> {
+ // onShowInputRequested() is indeed @CallSuper so we always call this, even when the
+ // result is ignored.
+ final boolean originalResult = super.onEvaluateInputViewShown();
+ if (!mSettings.getHardKeyboardConfigurationBehaviorAllowed(false)) {
+ final Configuration config = getResources().getConfiguration();
+ if (config.keyboard != Configuration.KEYBOARD_NOKEYS
+ && config.hardKeyboardHidden != Configuration.HARDKEYBOARDHIDDEN_YES) {
+ // Override the behavior of InputMethodService#onEvaluateInputViewShown()
+ return true;
+ }
+ }
+ return originalResult;
+ });
+ }
+
+ @Override
+ public boolean onShowInputRequested(int flags, boolean configChange) {
+ return getTracer().onShowInputRequested(flags, configChange, () -> {
+ // onShowInputRequested() is not marked with @CallSuper, but just in case.
+ final boolean originalResult = super.onShowInputRequested(flags, configChange);
+ if (!mSettings.getHardKeyboardConfigurationBehaviorAllowed(false)) {
+ if ((flags & InputMethod.SHOW_EXPLICIT) == 0
+ && getResources().getConfiguration().keyboard
+ != Configuration.KEYBOARD_NOKEYS) {
+ // Override the behavior of InputMethodService#onShowInputRequested()
+ return true;
+ }
+ }
+ return originalResult;
+ });
+ }
+
+ @Override
+ public void onDestroy() {
+ getTracer().onDestroy(() -> {
+ super.onDestroy();
+ unregisterReceiver(mCommandReceiver);
+ mHandlerThread.quitSafely();
+ });
+ }
+
+ @Override
+ public AbstractInputMethodImpl onCreateInputMethodInterface() {
+ return getTracer().onCreateInputMethodInterface(() -> new MockInputMethodImpl());
+ }
+
+ private final ThreadLocal<Tracer> mThreadLocalTracer = new ThreadLocal<>();
+
+ private Tracer getTracer() {
+ Tracer tracer = mThreadLocalTracer.get();
+ if (tracer == null) {
+ tracer = new Tracer(this);
+ mThreadLocalTracer.set(tracer);
+ }
+ return tracer;
+ }
+
+ @NonNull
+ private ImeState getState() {
+ final boolean hasInputBinding = getCurrentInputBinding() != null;
+ final boolean hasDummyInputConnectionConnection =
+ !hasInputBinding
+ || getCurrentInputConnection() == getCurrentInputBinding().getConnection();
+ return new ImeState(hasInputBinding, hasDummyInputConnectionConnection);
+ }
+
+ /**
+ * Event tracing helper class for {@link MockIme}.
+ */
+ private static final class Tracer {
+
+ @NonNull
+ private final MockIme mIme;
+
+ private final int mThreadId = Process.myTid();
+
+ @NonNull
+ private final String mThreadName =
+ Thread.currentThread().getName() != null ? Thread.currentThread().getName() : "";
+
+ private final boolean mIsMainThread =
+ Looper.getMainLooper().getThread() == Thread.currentThread();
+
+ private int mNestLevel = 0;
+
+ private String mImeEventActionName;
+
+ private String mClientPackageName;
+
+ Tracer(@NonNull MockIme mockIme) {
+ mIme = mockIme;
+ }
+
+ private void sendEventInternal(@NonNull ImeEvent event) {
+ if (mImeEventActionName == null) {
+ mImeEventActionName = mIme.getImeEventActionName();
+ }
+ if (mClientPackageName == null) {
+ mClientPackageName = mIme.getClientPackageName();
+ }
+ if (mImeEventActionName == null || mClientPackageName == null) {
+ Log.e(TAG, "Tracer cannot be used before onCreate()");
+ return;
+ }
+ final Intent intent = new Intent()
+ .setAction(mImeEventActionName)
+ .setPackage(mClientPackageName)
+ .putExtras(event.toBundle())
+ .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
+ | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
+ mIme.sendBroadcast(intent);
+ }
+
+ private void recordEventInternal(@NonNull String eventName, @NonNull Runnable runnable) {
+ recordEventInternal(eventName, runnable, new Bundle());
+ }
+
+ private void recordEventInternal(@NonNull String eventName, @NonNull Runnable runnable,
+ @NonNull Bundle arguments) {
+ recordEventInternal(eventName, () -> {
+ runnable.run(); return null;
+ }, arguments);
+ }
+
+ private <T> T recordEventInternal(@NonNull String eventName,
+ @NonNull Supplier<T> supplier) {
+ return recordEventInternal(eventName, supplier, new Bundle());
+ }
+
+ private <T> T recordEventInternal(@NonNull String eventName,
+ @NonNull Supplier<T> supplier, @NonNull Bundle arguments) {
+ final ImeState enterState = mIme.getState();
+ final long enterTimestamp = SystemClock.elapsedRealtimeNanos();
+ final long enterWallTime = System.currentTimeMillis();
+ final int nestLevel = mNestLevel;
+ // Send enter event
+ sendEventInternal(new ImeEvent(eventName, nestLevel, mThreadName,
+ mThreadId, mIsMainThread, enterTimestamp, 0, enterWallTime,
+ 0, enterState, null, arguments, null));
+ ++mNestLevel;
+ T result;
+ try {
+ result = supplier.get();
+ } finally {
+ --mNestLevel;
+ }
+ final long exitTimestamp = SystemClock.elapsedRealtimeNanos();
+ final long exitWallTime = System.currentTimeMillis();
+ final ImeState exitState = mIme.getState();
+ // Send exit event
+ sendEventInternal(new ImeEvent(eventName, nestLevel, mThreadName,
+ mThreadId, mIsMainThread, enterTimestamp, exitTimestamp, enterWallTime,
+ exitWallTime, enterState, exitState, arguments, result));
+ return result;
+ }
+
+ public void onCreate(@NonNull Runnable runnable) {
+ recordEventInternal("onCreate", runnable);
+ }
+
+ public void onConfigureWindow(Window win, boolean isFullscreen,
+ boolean isCandidatesOnly, @NonNull Runnable runnable) {
+ final Bundle arguments = new Bundle();
+ arguments.putBoolean("isFullscreen", isFullscreen);
+ arguments.putBoolean("isCandidatesOnly", isCandidatesOnly);
+ recordEventInternal("onConfigureWindow", runnable, arguments);
+ }
+
+ public boolean onEvaluateFullscreenMode(@NonNull BooleanSupplier supplier) {
+ return recordEventInternal("onEvaluateFullscreenMode", supplier::getAsBoolean);
+ }
+
+ public boolean onEvaluateInputViewShown(@NonNull BooleanSupplier supplier) {
+ return recordEventInternal("onEvaluateInputViewShown", supplier::getAsBoolean);
+ }
+
+ public View onCreateInputView(@NonNull Supplier<View> supplier) {
+ return recordEventInternal("onCreateInputView", supplier);
+ }
+
+ public void onStartInput(EditorInfo editorInfo, boolean restarting,
+ @NonNull Runnable runnable) {
+ final Bundle arguments = new Bundle();
+ arguments.putParcelable("editorInfo", editorInfo);
+ arguments.putBoolean("restarting", restarting);
+ recordEventInternal("onStartInput", runnable, arguments);
+ }
+
+ public void onStartInputView(EditorInfo editorInfo, boolean restarting,
+ @NonNull Runnable runnable) {
+ final Bundle arguments = new Bundle();
+ arguments.putParcelable("editorInfo", editorInfo);
+ arguments.putBoolean("restarting", restarting);
+ recordEventInternal("onStartInputView", runnable, arguments);
+ }
+
+ public void onFinishInputView(boolean finishingInput, @NonNull Runnable runnable) {
+ final Bundle arguments = new Bundle();
+ arguments.putBoolean("finishingInput", finishingInput);
+ recordEventInternal("onFinishInputView", runnable, arguments);
+ }
+
+ public void onFinishInput(@NonNull Runnable runnable) {
+ recordEventInternal("onFinishInput", runnable);
+ }
+
+ public boolean onKeyDown(int keyCode, KeyEvent event, @NonNull BooleanSupplier supplier) {
+ final Bundle arguments = new Bundle();
+ arguments.putInt("keyCode", keyCode);
+ arguments.putParcelable("event", event);
+ return recordEventInternal("onKeyDown", supplier::getAsBoolean, arguments);
+ }
+
+ public boolean onShowInputRequested(int flags, boolean configChange,
+ @NonNull BooleanSupplier supplier) {
+ final Bundle arguments = new Bundle();
+ arguments.putInt("flags", flags);
+ arguments.putBoolean("configChange", configChange);
+ return recordEventInternal("onShowInputRequested", supplier::getAsBoolean, arguments);
+ }
+
+ public void onDestroy(@NonNull Runnable runnable) {
+ recordEventInternal("onDestroy", runnable);
+ }
+
+ public void attachToken(IBinder token, @NonNull Runnable runnable) {
+ final Bundle arguments = new Bundle();
+ arguments.putBinder("token", token);
+ recordEventInternal("attachToken", runnable, arguments);
+ }
+
+ public void bindInput(InputBinding binding, @NonNull Runnable runnable) {
+ final Bundle arguments = new Bundle();
+ arguments.putParcelable("binding", binding);
+ recordEventInternal("bindInput", runnable, arguments);
+ }
+
+ public void unbindInput(@NonNull Runnable runnable) {
+ recordEventInternal("unbindInput", runnable);
+ }
+
+ public void showSoftInput(int flags, ResultReceiver resultReceiver,
+ @NonNull Runnable runnable) {
+ final Bundle arguments = new Bundle();
+ arguments.putInt("flags", flags);
+ arguments.putParcelable("resultReceiver", resultReceiver);
+ recordEventInternal("showSoftInput", runnable, arguments);
+ }
+
+ public void hideSoftInput(int flags, ResultReceiver resultReceiver,
+ @NonNull Runnable runnable) {
+ final Bundle arguments = new Bundle();
+ arguments.putInt("flags", flags);
+ arguments.putParcelable("resultReceiver", resultReceiver);
+ recordEventInternal("hideSoftInput", runnable, arguments);
+ }
+
+ public AbstractInputMethodImpl onCreateInputMethodInterface(
+ @NonNull Supplier<AbstractInputMethodImpl> supplier) {
+ return recordEventInternal("onCreateInputMethodInterface", supplier);
+ }
+
+ public void onReceiveCommand(
+ @NonNull ImeCommand command, @NonNull Runnable runnable) {
+ final Bundle arguments = new Bundle();
+ arguments.putBundle("command", command.toBundle());
+ recordEventInternal("onReceiveCommand", runnable, arguments);
+ }
+
+ public void onHandleCommand(
+ @NonNull ImeCommand command, @NonNull Runnable runnable) {
+ final Bundle arguments = new Bundle();
+ arguments.putBundle("command", command.toBundle());
+ recordEventInternal("onHandleCommand", runnable, arguments);
+ }
+
+ public void onInputViewLayoutChanged(@NonNull ImeLayoutInfo imeLayoutInfo,
+ @NonNull Runnable runnable) {
+ final Bundle arguments = new Bundle();
+ imeLayoutInfo.writeToBundle(arguments);
+ recordEventInternal("onInputViewLayoutChanged", runnable, arguments);
+ }
+ }
+}
diff --git a/tests/autofillservice/src/com/android/cts/mockime/MockImeSession.java b/tests/autofillservice/src/com/android/cts/mockime/MockImeSession.java
new file mode 100644
index 0000000..727da2cf
--- /dev/null
+++ b/tests/autofillservice/src/com/android/cts/mockime/MockImeSession.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.mockime;
+
+import android.app.UiAutomation;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.provider.Settings;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import android.text.TextUtils;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.compatibility.common.util.PollingCheck;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Represents an active Mock IME session, which provides basic primitives to write end-to-end tests
+ * for IME APIs.
+ *
+ * <p>To use {@link MockIme} via {@link MockImeSession}, you need to </p>
+ * <p>Public methods are not thread-safe.</p>
+ */
+public class MockImeSession implements AutoCloseable {
+ private final String mImeEventActionName =
+ "com.android.cts.mockime.action.IME_EVENT." + SystemClock.elapsedRealtimeNanos();
+
+ private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(10);
+
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final UiAutomation mUiAutomation;
+
+ private final HandlerThread mHandlerThread = new HandlerThread("EventReceiver");
+
+ private static final class EventStore {
+ private static final int INITIAL_ARRAY_SIZE = 32;
+
+ @NonNull
+ public final ImeEvent[] mArray;
+ public int mLength;
+
+ EventStore() {
+ mArray = new ImeEvent[INITIAL_ARRAY_SIZE];
+ mLength = 0;
+ }
+
+ EventStore(EventStore src, int newLength) {
+ mArray = new ImeEvent[newLength];
+ mLength = src.mLength;
+ System.arraycopy(src.mArray, 0, mArray, 0, src.mLength);
+ }
+
+ public EventStore add(ImeEvent event) {
+ if (mLength + 1 <= mArray.length) {
+ mArray[mLength] = event;
+ ++mLength;
+ return this;
+ } else {
+ return new EventStore(this, mLength * 2).add(event);
+ }
+ }
+
+ public ImeEventStream.ImeEventArray takeSnapshot() {
+ return new ImeEventStream.ImeEventArray(mArray, mLength);
+ }
+ }
+
+ private static final class MockImeEventReceiver extends BroadcastReceiver {
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ @NonNull
+ private EventStore mCurrentEventStore = new EventStore();
+
+ @NonNull
+ private final String mActionName;
+
+ MockImeEventReceiver(@NonNull String actionName) {
+ mActionName = actionName;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (TextUtils.equals(mActionName, intent.getAction())) {
+ synchronized (mLock) {
+ mCurrentEventStore =
+ mCurrentEventStore.add(ImeEvent.fromBundle(intent.getExtras()));
+ }
+ }
+ }
+
+ public ImeEventStream.ImeEventArray takeEventSnapshot() {
+ synchronized (mLock) {
+ return mCurrentEventStore.takeSnapshot();
+ }
+ }
+ }
+ private final MockImeEventReceiver mEventReceiver =
+ new MockImeEventReceiver(mImeEventActionName);
+
+ private final ImeEventStream mEventStream =
+ new ImeEventStream(mEventReceiver::takeEventSnapshot);
+
+ private static String executeShellCommand(
+ @NonNull UiAutomation uiAutomation, @NonNull String command) throws IOException {
+ try (ParcelFileDescriptor.AutoCloseInputStream in =
+ new ParcelFileDescriptor.AutoCloseInputStream(
+ uiAutomation.executeShellCommand(command))) {
+ final StringBuilder sb = new StringBuilder();
+ final byte[] buffer = new byte[4096];
+ while (true) {
+ final int numRead = in.read(buffer);
+ if (numRead <= 0) {
+ break;
+ }
+ sb.append(new String(buffer, 0, numRead));
+ }
+ return sb.toString();
+ }
+ }
+
+ @Nullable
+ private String getCurrentInputMethodId() {
+ // TODO: Replace this with IMM#getCurrentInputMethodIdForTesting()
+ return Settings.Secure.getString(mContext.getContentResolver(),
+ Settings.Secure.DEFAULT_INPUT_METHOD);
+ }
+
+ @Nullable
+ private static void writeMockImeSettings(@NonNull Context context,
+ @NonNull String imeEventActionName,
+ @Nullable ImeSettings.Builder imeSettings) throws Exception {
+ final Bundle bundle = ImeSettings.serializeToBundle(imeEventActionName, imeSettings);
+ context.getContentResolver().call(SettingsProvider.AUTHORITY, "write", null, bundle);
+ }
+
+ private ComponentName getMockImeComponentName() {
+ return MockIme.getComponentName();
+ }
+
+ private String getMockImeId() {
+ return MockIme.getImeId();
+ }
+
+ private MockImeSession(@NonNull Context context, @NonNull UiAutomation uiAutomation) {
+ mContext = context;
+ mUiAutomation = uiAutomation;
+ }
+
+ private void initialize(@Nullable ImeSettings.Builder imeSettings) throws Exception {
+ // Make sure that MockIME is not selected.
+ if (mContext.getSystemService(InputMethodManager.class)
+ .getInputMethodList()
+ .stream()
+ .anyMatch(info -> getMockImeComponentName().equals(info.getComponent()))) {
+ executeShellCommand(mUiAutomation, "ime reset");
+ }
+ if (mContext.getSystemService(InputMethodManager.class)
+ .getEnabledInputMethodList()
+ .stream()
+ .anyMatch(info -> getMockImeComponentName().equals(info.getComponent()))) {
+ throw new IllegalStateException();
+ }
+
+ writeMockImeSettings(mContext, mImeEventActionName, imeSettings);
+
+ mHandlerThread.start();
+ mContext.registerReceiver(mEventReceiver,
+ new IntentFilter(mImeEventActionName), null /* broadcastPermission */,
+ new Handler(mHandlerThread.getLooper()));
+
+ executeShellCommand(mUiAutomation, "ime enable " + getMockImeId());
+ executeShellCommand(mUiAutomation, "ime set " + getMockImeId());
+
+ PollingCheck.check("Make sure that MockIME becomes available", TIMEOUT,
+ () -> getMockImeId().equals(getCurrentInputMethodId()));
+ }
+
+ /**
+ * Creates a new Mock IME session. During this session, you can receive various events from
+ * {@link MockIme}.
+ *
+ * @param context {@link Context} to be used to receive inter-process events from the
+ * {@link MockIme} (e.g. via {@link BroadcastReceiver}
+ * @param uiAutomation {@link UiAutomation} object to change the device state that are typically
+ * guarded by permissions.
+ * @param imeSettings Key-value pairs to be passed to the {@link MockIme}.
+ * @return A session object, with which you can retrieve event logs from the {@link MockIme} and
+ * can clean up the session.
+ */
+ @NonNull
+ public static MockImeSession create(
+ @NonNull Context context,
+ @NonNull UiAutomation uiAutomation,
+ @Nullable ImeSettings.Builder imeSettings) throws Exception {
+ final MockImeSession client = new MockImeSession(context, uiAutomation);
+ client.initialize(imeSettings);
+ return client;
+ }
+
+ /**
+ * @return {@link ImeEventStream} object that stores events sent from {@link MockIme} since the
+ * session is created.
+ */
+ public ImeEventStream openEventStream() {
+ return mEventStream.copy();
+ }
+
+ /**
+ * Closes the active session and de-selects {@link MockIme}. Currently which IME will be
+ * selected next is up to the system.
+ */
+ public void close() throws Exception {
+ executeShellCommand(mUiAutomation, "ime reset");
+
+ PollingCheck.check("Make sure that MockIME becomes unavailable", TIMEOUT, () ->
+ mContext.getSystemService(InputMethodManager.class)
+ .getEnabledInputMethodList()
+ .stream()
+ .noneMatch(info -> getMockImeComponentName().equals(info.getComponent())));
+
+ mContext.unregisterReceiver(mEventReceiver);
+ mHandlerThread.quitSafely();
+ mContext.getContentResolver().call(SettingsProvider.AUTHORITY, "delete", null, null);
+ }
+
+ /**
+ * Lets {@link MockIme} to call
+ * {@link android.view.inputmethod.InputConnection#commitText(CharSequence, int)} with the given
+ * parameters.
+ *
+ * <p>This triggers {@code getCurrentInputConnection().commitText(text, newCursorPosition)}.</p>
+ *
+ * @param text to be passed as the {@code text} parameter
+ * @param newCursorPosition to be passed as the {@code newCursorPosition} parameter
+ * @return {@link ImeCommand} object that can be passed to
+ * {@link ImeEventStreamTestUtils#expectCommand(ImeEventStream, ImeCommand, long)} to
+ * wait until this event is handled by {@link MockIme}
+ */
+ @NonNull
+ public ImeCommand callCommitText(@NonNull CharSequence text, int newCursorPosition) {
+ final Bundle params = new Bundle();
+ params.putCharSequence("text", text);
+ params.putInt("newCursorPosition", newCursorPosition);
+ final ImeCommand command = new ImeCommand(
+ "commitText", SystemClock.elapsedRealtimeNanos(), true, params);
+ final Intent intent = new Intent();
+ intent.setPackage(MockIme.getComponentName().getPackageName());
+ intent.setAction(MockIme.getCommandActionName(mImeEventActionName));
+ intent.putExtras(command.toBundle());
+ mContext.sendBroadcast(intent);
+ return command;
+ }
+
+ @NonNull
+ public ImeCommand callSetBackDisposition(int backDisposition) {
+ final Bundle params = new Bundle();
+ params.putInt("backDisposition", backDisposition);
+ final ImeCommand command = new ImeCommand(
+ "setBackDisposition", SystemClock.elapsedRealtimeNanos(), true, params);
+ final Intent intent = new Intent();
+ intent.setPackage(MockIme.getComponentName().getPackageName());
+ intent.setAction(MockIme.getCommandActionName(mImeEventActionName));
+ intent.putExtras(command.toBundle());
+ mContext.sendBroadcast(intent);
+ return command;
+ }
+
+ @NonNull
+ public ImeCommand callRequestHideSelf(int flags) {
+ final Bundle params = new Bundle();
+ params.putInt("flags", flags);
+ final ImeCommand command = new ImeCommand(
+ "requestHideSelf", SystemClock.elapsedRealtimeNanos(), true, params);
+ final Intent intent = new Intent();
+ intent.setPackage(MockIme.getComponentName().getPackageName());
+ intent.setAction(MockIme.getCommandActionName(mImeEventActionName));
+ intent.putExtras(command.toBundle());
+ mContext.sendBroadcast(intent);
+ return command;
+ }
+
+ @NonNull
+ public ImeCommand callRequestShowSelf(int flags) {
+ final Bundle params = new Bundle();
+ params.putInt("flags", flags);
+ final ImeCommand command = new ImeCommand(
+ "requestShowSelf", SystemClock.elapsedRealtimeNanos(), true, params);
+ final Intent intent = new Intent();
+ intent.setPackage(MockIme.getComponentName().getPackageName());
+ intent.setAction(MockIme.getCommandActionName(mImeEventActionName));
+ intent.putExtras(command.toBundle());
+ mContext.sendBroadcast(intent);
+ return command;
+ }
+}
diff --git a/tests/autofillservice/src/com/android/cts/mockime/MockImeSessionRule.java b/tests/autofillservice/src/com/android/cts/mockime/MockImeSessionRule.java
new file mode 100644
index 0000000..8632c3b
--- /dev/null
+++ b/tests/autofillservice/src/com/android/cts/mockime/MockImeSessionRule.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.mockime;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.InstrumentationRegistry;
+
+import com.android.cts.mockime.ImeSettings.Builder;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import static org.junit.Assume.assumeTrue;
+
+/**
+ * Custom JUnit4 rule to automatically open and close a {@link MockImeSession}.
+ */
+public final class MockImeSessionRule implements TestRule {
+
+ private static final String TAG = MockImeSessionRule.class.getSimpleName();
+ private final Context mContext;
+ private final Builder mImeSettings;
+ private final UiAutomation mUiAutomation;
+ private MockImeSession mMockImeSession;
+ private boolean mIgnoreInitException;
+ private Throwable mInitException;
+
+ public MockImeSessionRule() {
+ this(InstrumentationRegistry.getTargetContext(),
+ InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+ new ImeSettings.Builder(), /* ignoreInitException= */ false);
+ }
+
+ public MockImeSessionRule(boolean ignoreInitException) {
+ this(InstrumentationRegistry.getTargetContext(),
+ InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+ new ImeSettings.Builder(), ignoreInitException);
+ }
+
+ public MockImeSessionRule(@NonNull Context context, @NonNull UiAutomation uiAutomation,
+ @NonNull ImeSettings.Builder imeSettings, boolean ignoreInitException) {
+ mContext = context;
+ mUiAutomation = uiAutomation;
+ mImeSettings = imeSettings;
+ mIgnoreInitException = ignoreInitException;
+ }
+
+ @Override
+ public Statement apply(@NonNull Statement base, @NonNull Description description) {
+ return new Statement() {
+
+ @Override
+ public void evaluate() throws Throwable {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Creating MockImeSession on " + description.getDisplayName());
+ }
+ try {
+ mMockImeSession = MockImeSession.create(mContext, mUiAutomation, mImeSettings);
+ } catch (Throwable t) {
+ Log.e(TAG, "Error creating MockImeSession", t);
+ if (mIgnoreInitException) {
+ mInitException = t;
+ } else {
+ throw t;
+ }
+ }
+ try {
+ base.evaluate();
+ } finally {
+ if (mMockImeSession != null) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Closing MockImeSession on " + description.getDisplayName());
+ }
+ mMockImeSession.close();
+ }
+ }
+ }
+ };
+ }
+
+ @Nullable
+ public MockImeSession getMockImeSession() {
+ return mMockImeSession;
+ }
+
+ @Nullable
+ public Throwable getInitException() {
+ return mInitException;
+ }
+
+ public void assumeAvailable() {
+ if (mInitException == null) return;
+ assumeTrue("Exception setting MockingIme: " + mInitException, mInitException == null);
+ }
+}
diff --git a/tests/autofillservice/src/com/android/cts/mockime/SettingsProvider.java b/tests/autofillservice/src/com/android/cts/mockime/SettingsProvider.java
new file mode 100644
index 0000000..d9a565b
--- /dev/null
+++ b/tests/autofillservice/src/com/android/cts/mockime/SettingsProvider.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.mockime;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+/**
+ * {@link ContentProvider} to receive {@link ImeSettings} via
+ * {@link ContentProvider#call(String, String, String, Bundle)}.
+ */
+public class SettingsProvider extends ContentProvider {
+
+ static final Uri AUTHORITY = Uri.parse("content://com.android.cts.mockime.provider");
+
+ @Nullable
+ private static ImeSettings sSettings = null;
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public Bundle call(String method, String arg, Bundle extras) {
+ if ("write".equals(method)) {
+ sSettings = null;
+ final String callingPackageName = getCallingPackage();
+ if (callingPackageName == null) {
+ throw new SecurityException("Failed to obtain the calling package name.");
+ }
+ sSettings = new ImeSettings(callingPackageName, extras);
+ } else if ("delete".equals(method)) {
+ sSettings = null;
+ }
+ return Bundle.EMPTY;
+ }
+
+ static ImeSettings getSettings() {
+ return sSettings;
+ }
+}
diff --git a/tests/core/runner-axt/src/com/android/cts/runner/CrashParserRunListener.java b/tests/core/runner-axt/src/com/android/cts/runner/CrashParserRunListener.java
new file mode 100644
index 0000000..d838fb4
--- /dev/null
+++ b/tests/core/runner-axt/src/com/android/cts/runner/CrashParserRunListener.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.runner;
+
+import androidx.test.internal.runner.listener.InstrumentationRunListener;
+import android.util.Log;
+import org.junit.runner.Description;
+
+/**
+ * A {@link RunListener} for CrashParser. Dumps the test name to logs when
+ * tests start.
+ */
+public class CrashParserRunListener extends InstrumentationRunListener {
+
+ private static final String TAG = "CrashParserRunListener";
+
+ // Constant must be kept in sync with CrashUtils.java
+ public static final String NEW_TEST_ALERT = "New test starting with name: ";
+
+ @Override
+ public void testStarted(Description description) throws Exception {
+ Log.i(TAG, NEW_TEST_ALERT + description.toString());
+ }
+
+}
diff --git a/tests/core/runner/src/com/android/cts/runner/CrashParserRunListener.java b/tests/core/runner/src/com/android/cts/runner/CrashParserRunListener.java
new file mode 100644
index 0000000..fbbb684
--- /dev/null
+++ b/tests/core/runner/src/com/android/cts/runner/CrashParserRunListener.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.runner;
+
+import android.support.test.internal.runner.listener.InstrumentationRunListener;
+import android.util.Log;
+import org.junit.runner.Description;
+
+/**
+ * A {@link RunListener} for CrashParser. Dumps the test name to logs when
+ * tests start.
+ */
+public class CrashParserRunListener extends InstrumentationRunListener {
+
+ private static final String TAG = "CrashParserRunListener";
+
+ // Constant must be kept in sync with CrashUtils.java
+ public static final String NEW_TEST_ALERT = "New test starting with name: ";
+
+ @Override
+ public void testStarted(Description description) throws Exception {
+ Log.i(TAG, NEW_TEST_ALERT + description.toString());
+ }
+
+}
diff --git a/tests/framework/base/activitymanager/src/android/server/am/lifecycle/ActivityLifecycleTests.java b/tests/framework/base/activitymanager/src/android/server/am/lifecycle/ActivityLifecycleTests.java
index 0e92146..712f729 100644
--- a/tests/framework/base/activitymanager/src/android/server/am/lifecycle/ActivityLifecycleTests.java
+++ b/tests/framework/base/activitymanager/src/android/server/am/lifecycle/ActivityLifecycleTests.java
@@ -346,6 +346,7 @@
getLaunchActivityBuilder().execute();
waitAndAssertActivityStates(state(secondActivity, ON_PAUSE));
+ waitAndAssertActivityStates(state(callbackTrackingActivity, ON_STOP));
// Finish top activity and verify that activity below became focused.
getLifecycleLog().clear();
diff --git a/tests/security/src/android/keystore/cts/AuthorizationList.java b/tests/security/src/android/keystore/cts/AuthorizationList.java
index e4c5eb6..e6849f3 100644
--- a/tests/security/src/android/keystore/cts/AuthorizationList.java
+++ b/tests/security/src/android/keystore/cts/AuthorizationList.java
@@ -185,6 +185,7 @@
private Date creationDateTime;
private Integer origin;
private boolean rollbackResistant;
+ private boolean rollbackResistance;
private RootOfTrust rootOfTrust;
private Integer osVersion;
private Integer osPatchLevel;
@@ -270,6 +271,9 @@
case KM_TAG_ROLLBACK_RESISTANT & KEYMASTER_TAG_TYPE_MASK:
rollbackResistant = true;
break;
+ case KM_TAG_ROLLBACK_RESISTANCE & KEYMASTER_TAG_TYPE_MASK:
+ rollbackResistance = true;
+ break;
case KM_TAG_AUTH_TIMEOUT & KEYMASTER_TAG_TYPE_MASK:
authTimeout = Asn1Utils.getIntegerFromAsn1(value);
break;
@@ -535,6 +539,10 @@
return rollbackResistant;
}
+ public boolean isRollbackResistance() {
+ return rollbackResistance;
+ }
+
public RootOfTrust getRootOfTrust() {
return rootOfTrust;
}
@@ -675,6 +683,10 @@
s.append("\nRollback resistant: true");
}
+ if (rollbackResistance) {
+ s.append("\nRollback resistance: true");
+ }
+
if (rootOfTrust != null) {
s.append("\nRoot of Trust:\n");
s.append(rootOfTrust);
diff --git a/tests/tests/content/src/android/content/cts/AvailableIntentsTest.java b/tests/tests/content/src/android/content/cts/AvailableIntentsTest.java
index 7ded622..064bf0b 100644
--- a/tests/tests/content/src/android/content/cts/AvailableIntentsTest.java
+++ b/tests/tests/content/src/android/content/cts/AvailableIntentsTest.java
@@ -33,6 +33,7 @@
import android.telecom.TelecomManager;
import android.test.AndroidTestCase;
+import com.android.compatibility.common.util.CddTest;
import com.android.compatibility.common.util.FeatureUtil;
import java.util.List;
@@ -74,6 +75,7 @@
* Test ACTION_VIEW when url is http://web_address,
* it will open a browser window to the URL specified.
*/
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testViewNormalUrl() {
Uri uri = Uri.parse(NORMAL_URL);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
@@ -84,6 +86,7 @@
* Test ACTION_VIEW when url is https://web_address,
* it will open a browser window to the URL specified.
*/
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testViewSecureUrl() {
Uri uri = Uri.parse(SECURE_URL);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
@@ -94,6 +97,7 @@
* Test ACTION_WEB_SEARCH when url is http://web_address,
* it will open a browser window to the URL specified.
*/
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testWebSearchNormalUrl() {
Uri uri = Uri.parse(NORMAL_URL);
Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
@@ -105,6 +109,7 @@
* Test ACTION_WEB_SEARCH when url is https://web_address,
* it will open a browser window to the URL specified.
*/
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testWebSearchSecureUrl() {
Uri uri = Uri.parse(SECURE_URL);
Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
@@ -184,6 +189,7 @@
/**
* Test ACTION_SHOW_CALL_SETTINGS, it will display the call preferences.
*/
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testShowCallSettings() {
PackageManager packageManager = mContext.getPackageManager();
if (packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
@@ -195,6 +201,7 @@
/**
* Test ACTION_SHOW_RESPOND_VIA_SMS_SETTINGS, it will display the respond by SMS preferences.
*/
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testShowRespondViaSmsSettings() {
PackageManager packageManager = mContext.getPackageManager();
if (packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
@@ -206,6 +213,7 @@
/**
* Test start camera by intent
*/
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testCamera() {
PackageManager packageManager = mContext.getPackageManager();
if (packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
@@ -224,6 +232,7 @@
}
}
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testSettings() {
assertCanBeHandled(new Intent(Settings.ACTION_SETTINGS));
}
@@ -231,6 +240,7 @@
/**
* Test add event in calendar
*/
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testCalendarAddAppointment() {
Intent addAppointmentIntent = new Intent(Intent.ACTION_EDIT);
addAppointmentIntent.setType("vnd.android.cursor.item/event");
@@ -240,6 +250,7 @@
/**
* Test view call logs
*/
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testContactsCallLogs() {
PackageManager packageManager = mContext.getPackageManager();
if (packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
@@ -252,6 +263,7 @@
/**
* Test view music playback
*/
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testMusicPlayback() {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(ContentUris.withAppendedId(
@@ -259,6 +271,7 @@
assertCanBeHandled(intent);
}
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testAlarmClockSetAlarm() {
Intent intent = new Intent(AlarmClock.ACTION_SET_ALARM);
intent.putExtra(AlarmClock.EXTRA_MESSAGE, "Custom message");
@@ -267,17 +280,20 @@
assertCanBeHandled(intent);
}
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testAlarmClockSetTimer() {
Intent intent = new Intent(AlarmClock.ACTION_SET_TIMER);
intent.putExtra(AlarmClock.EXTRA_LENGTH, 60000);
assertCanBeHandled(intent);
}
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testAlarmClockShowAlarms() {
Intent intent = new Intent(AlarmClock.ACTION_SHOW_ALARMS);
assertCanBeHandled(intent);
}
+ @CddTest(requirement="3.2.3.1/C-0-1")
public void testAlarmClockShowTimers() {
if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK_ONLY)) {
return;
diff --git a/tests/tests/content/src/android/content/cts/ContentProviderTest.java b/tests/tests/content/src/android/content/cts/ContentProviderTest.java
index dc4a031..95f4611 100644
--- a/tests/tests/content/src/android/content/cts/ContentProviderTest.java
+++ b/tests/tests/content/src/android/content/cts/ContentProviderTest.java
@@ -29,6 +29,8 @@
import android.content.cts.R;
+import com.android.compatibility.common.util.CddTest;
+
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -36,6 +38,7 @@
/**
* Test {@link ContentProvider}.
*/
+@CddTest(requirement="3.5/C-0-2")
public class ContentProviderTest extends AndroidTestCase {
private static final String TEST_PACKAGE_NAME = "android.content.cts";
private static final String TEST_FILE_NAME = "testFile.tmp";
diff --git a/tests/tests/content/src/android/content/cts/IntentTest.java b/tests/tests/content/src/android/content/cts/IntentTest.java
index af53e56..51684ae 100644
--- a/tests/tests/content/src/android/content/cts/IntentTest.java
+++ b/tests/tests/content/src/android/content/cts/IntentTest.java
@@ -39,6 +39,7 @@
import android.util.Xml;
import com.android.content.cts.DummyParcelable;
+import com.android.compatibility.common.util.CddTest;
import java.io.IOException;
import java.io.Serializable;
@@ -47,6 +48,7 @@
import java.util.Objects;
import java.util.Set;
+@CddTest(requirement="3.5/C-0-1")
public class IntentTest extends AndroidTestCase {
private Intent mIntent;
diff --git a/tests/tests/dpi/src/android/dpi/cts/ConfigurationTest.java b/tests/tests/dpi/src/android/dpi/cts/ConfigurationTest.java
index 7ac5246..8fdc701 100644
--- a/tests/tests/dpi/src/android/dpi/cts/ConfigurationTest.java
+++ b/tests/tests/dpi/src/android/dpi/cts/ConfigurationTest.java
@@ -83,8 +83,8 @@
allowedDensities.add(DisplayMetrics.DENSITY_XXHIGH);
allowedDensities.add(DisplayMetrics.DENSITY_560);
allowedDensities.add(DisplayMetrics.DENSITY_XXXHIGH);
- assertTrue("DisplayMetrics#densityDpi must be one of the DisplayMetrics.DENSITY_* values: "
- + allowedDensities, allowedDensities.contains(mMetrics.densityDpi));
+ assertTrue("DisplayMetrics.DENSITY_DEVICE_STABLE must be one of the DisplayMetrics.DENSITY_* values: "
+ + allowedDensities, allowedDensities.contains(DisplayMetrics.DENSITY_DEVICE_STABLE));
assertEquals(mMetrics.density,
(float) mMetrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT,
diff --git a/tests/tests/media/src/android/media/cts/MediaPlayerTest.java b/tests/tests/media/src/android/media/cts/MediaPlayerTest.java
index 669a65a..342cba7 100644
--- a/tests/tests/media/src/android/media/cts/MediaPlayerTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaPlayerTest.java
@@ -22,6 +22,7 @@
import android.graphics.Rect;
import android.hardware.Camera;
import android.media.AudioManager;
+import android.media.CamcorderProfile;
import android.media.MediaDataSource;
import android.media.MediaFormat;
import android.media.MediaMetadataRetriever;
@@ -58,6 +59,7 @@
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.UUID;
@@ -1024,7 +1026,14 @@
// getSupportedVideoSizes returns null when separate video/preview size
// is not supported.
if (videoSizes == null) {
- videoSizes = parameters.getSupportedPreviewSizes();
+ // If we have CamcorderProfile use it instead of Preview size.
+ if (CamcorderProfile.hasProfile(0, CamcorderProfile.QUALITY_LOW)) {
+ CamcorderProfile profile = CamcorderProfile.get(0, CamcorderProfile.QUALITY_LOW);
+ videoSizes = new ArrayList();
+ videoSizes.add(mCamera.new Size(profile.videoFrameWidth, profile.videoFrameHeight));
+ } else {
+ videoSizes = parameters.getSupportedPreviewSizes();
+ }
}
for (Camera.Size size : videoSizes)
{
diff --git a/tests/tests/media/src/android/media/cts/RoutingTest.java b/tests/tests/media/src/android/media/cts/RoutingTest.java
index e1765d8..f2e078a 100644
--- a/tests/tests/media/src/android/media/cts/RoutingTest.java
+++ b/tests/tests/media/src/android/media/cts/RoutingTest.java
@@ -666,6 +666,12 @@
return;
}
+ AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+ if (devices.length < 2) {
+ // In this case, we cannot switch output device, that may cause the test fail.
+ return;
+ }
+
mRoutingChanged = false;
mRoutingChangedLooper = null;
// Create MediaPlayer in another thread to make sure there is a looper active for events.
@@ -678,6 +684,27 @@
AudioRoutingListener listener = new AudioRoutingListener();
MediaPlayer mediaPlayer = allocMediaPlayer();
mediaPlayer.addOnRoutingChangedListener(listener, null);
+ // With setting preferred device, the output device may switch.
+ // Post the request delayed to ensure the message queue is running
+ // so that the routing changed event can be handled correctly.
+ Handler handler = new Handler();
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ AudioDeviceInfo routedDevice = mediaPlayer.getRoutedDevice();
+ if (routedDevice == null) {
+ return;
+ }
+ AudioDeviceInfo[] devices = mAudioManager.getDevices(
+ AudioManager.GET_DEVICES_OUTPUTS);
+ for (AudioDeviceInfo device : devices) {
+ if (routedDevice.getId() != device.getId()) {
+ mediaPlayer.setPreferredDevice(device);
+ break;
+ }
+ }
+ }
+ }, 1000);
Looper.loop();
mediaPlayer.removeOnRoutingChangedListener(listener);
mediaPlayer.stop();
diff --git a/tests/tests/ndef/Android.mk b/tests/tests/ndef/Android.mk
index df28c7b..0e5b1dd 100644
--- a/tests/tests/ndef/Android.mk
+++ b/tests/tests/ndef/Android.mk
@@ -24,7 +24,7 @@
# When built, explicitly put it in the data partition.
LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
-LOCAL_STATIC_JAVA_LIBRARIES := ctstestrunner-axt
+LOCAL_STATIC_JAVA_LIBRARIES := ctstestrunner-axt compatibility-device-util-axt
LOCAL_SRC_FILES := $(call all-java-files-under, src)
diff --git a/tests/tests/ndef/src/android/ndef/cts/NdefTest.java b/tests/tests/ndef/src/android/ndef/cts/NdefTest.java
index b0a838c..7b8cadb 100644
--- a/tests/tests/ndef/src/android/ndef/cts/NdefTest.java
+++ b/tests/tests/ndef/src/android/ndef/cts/NdefTest.java
@@ -24,6 +24,8 @@
import android.nfc.NdefRecord;
import android.nfc.FormatException;
+import com.android.compatibility.common.util.CddTest;
+
import junit.framework.TestCase;
/**
@@ -32,6 +34,7 @@
* hardware is required, so these API's are mandatory even on Android
* devices without NFC hardware.
*/
+@CddTest(requirement="7.4.4/C-0-1")
public class NdefTest extends TestCase {
static final Charset ASCII = Charset.forName("US-ASCII");
static final Charset UTF8 = Charset.forName("UTF-8");
@@ -86,6 +89,7 @@
new byte[] {1,2,3}, new byte[] {4,5,6}, new byte[] {7,8,9})).hashCode());
}
+ @CddTest(requirement="7.4.4/C-0-1")
public void testInvalidParsing() throws FormatException {
final byte[][] invalidNdefMessages = {
{}, // too short
@@ -115,6 +119,7 @@
}
}
+ @CddTest(requirement="7.4.4/C-0-1")
public void testValidParsing() throws FormatException {
// short record
assertEquals(new NdefMessage(new NdefRecord(NdefRecord.TNF_EMPTY, null, null, null)),
@@ -307,6 +312,7 @@
(byte) 0x6f, (byte) 0x6d}));
}
+ @CddTest(requirement="7.4.4/C-0-1")
public void testCreateUri() {
assertEquals(new byte[] {
(byte)0xD1, 1, 8, 'U', (byte)0x01, 'n', 'f', 'c', '.', 'c', 'o', 'm'},
@@ -327,6 +333,7 @@
new NdefMessage(NdefRecord.createUri("\u00A2")).toByteArray());
}
+ @CddTest(requirement="7.4.4/C-0-1")
public void testCreateMime() {
assertEquals(
new NdefRecord(NdefRecord.TNF_MIME_MEDIA, "text/plain".getBytes(ASCII), null,
@@ -385,6 +392,7 @@
NdefRecord.createExternal("A.b", "C!", null));
}
+ @CddTest(requirement="7.4.4/C-0-1")
public void testCreateApplicationRecord() throws FormatException {
NdefMessage m;
NdefRecord r;
@@ -431,6 +439,7 @@
assertEquals("com.foo.bar".getBytes(), r.getPayload());
}
+ @CddTest(requirement="7.4.4/C-0-1")
public void testToByteArray() throws FormatException {
NdefRecord r;
@@ -480,6 +489,7 @@
1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,})).toByteArray());
}
+ @CddTest(requirement="7.4.4/C-0-1")
public void testToUri() {
// absolute uri
assertEquals(Uri.parse("http://www.android.com"),
@@ -516,6 +526,7 @@
assertEquals(null, new NdefRecord(NdefRecord.TNF_EMPTY, null, null, null).toUri());
}
+ @CddTest(requirement="7.4.4/C-0-1")
public void testToMimeType() {
assertEquals(null, NdefRecord.createUri("http://www.android.com").toMimeType());
assertEquals(null, new NdefRecord(NdefRecord.TNF_EMPTY, null, null, null).toMimeType());
diff --git a/tests/tests/net/jni/NativeDnsJni.c b/tests/tests/net/jni/NativeDnsJni.c
index 352c0c5..6d3d1c3 100644
--- a/tests/tests/net/jni/NativeDnsJni.c
+++ b/tests/tests/net/jni/NativeDnsJni.c
@@ -120,8 +120,8 @@
gai_strerror(res));
return JNI_FALSE;
}
- if (strstr(buf, "google.com") == NULL) {
- ALOGD("getnameinfo(%s (GoogleDNS) ) didn't return google.com: %s",
+ if (strstr(buf, "google.com") == NULL && strstr(buf, "dns.google") == NULL) {
+ ALOGD("getnameinfo(%s (GoogleDNS) ) didn't return google.com or dns.google: %s",
GoogleDNSIpV4Address, buf);
return JNI_FALSE;
}
@@ -133,8 +133,9 @@
res, gai_strerror(res));
return JNI_FALSE;
}
- if (strstr(buf, "google.com") == NULL) {
- ALOGD("getnameinfo(%s) didn't return google.com: %s", GoogleDNSIpV6Address2, buf);
+ if (strstr(buf, "google.com") == NULL && strstr(buf, "dns.google") == NULL) {
+ ALOGD("getnameinfo(%s (GoogleDNS) ) didn't return google.com or dns.google: %s",
+ GoogleDNSIpV6Address2, buf);
return JNI_FALSE;
}
diff --git a/tests/tests/provider/src/android/provider/cts/BlockedNumberContractTest.java b/tests/tests/provider/src/android/provider/cts/BlockedNumberContractTest.java
index 756fa76..87e8b3e 100644
--- a/tests/tests/provider/src/android/provider/cts/BlockedNumberContractTest.java
+++ b/tests/tests/provider/src/android/provider/cts/BlockedNumberContractTest.java
@@ -30,6 +30,8 @@
import android.telephony.TelephonyManager;
import android.util.Log;
+import com.android.compatibility.common.util.CddTest;
+
import junit.framework.Assert;
import java.util.ArrayList;
@@ -43,6 +45,7 @@
// make cts
// cts-tradefed
// run cts -m CtsProviderTestCases --test android.provider.cts.BlockedNumberContractTest
+@CddTest(requirement="7.4.1.1/C-1-1,C-1-2")
public class BlockedNumberContractTest extends TestCaseThatRunsIfTelephonyIsEnabled {
private static final String TAG = "BlockedNumberContractTest";
private ContentResolver mContentResolver;
@@ -74,6 +77,7 @@
super.tearDown();
}
+ @CddTest(requirement="7.4.1.1/C-1-2")
public void testProviderInteractionsAsRegularApp_fails() {
if (!mIsSystemUser) {
Log.i(TAG, "skipping BlockedNumberContractTest");
@@ -131,6 +135,7 @@
assertNull(mContentResolver.getType(BlockedNumberContract.AUTHORITY_URI));
}
+ @CddTest(requirement="7.4.1.1/CC-1-2")
public void testInsertAndBlockCheck_succeeds() throws Exception {
if (!mIsSystemUser) {
Log.i(TAG, "skipping BlockedNumberContractTest");
@@ -162,6 +167,7 @@
assertFalse(BlockedNumberContract.isBlocked(mContext, "random string"));
}
+ @CddTest(requirement="7.4.1.1/C-1-2")
public void testUnblock_succeeds() throws Exception {
if (!mIsSystemUser) {
Log.i(TAG, "skipping BlockedNumberContractTest");
@@ -181,6 +187,7 @@
assertFalse(BlockedNumberContract.isBlocked(mContext, "1234@abcd.com"));
}
+ @CddTest(requirement="7.4.1.1/C-1-2")
public void testInsert_failsWithInvalidInputs() throws Exception {
if (!mIsSystemUser) {
Log.i(TAG, "skipping BlockedNumberContractTest");
@@ -226,6 +233,7 @@
}
}
+ @CddTest(requirement="7.4.1.1/C-1-2")
public void testUpdate_isUnsupported() throws Exception {
if (!mIsSystemUser) {
Log.i(TAG, "skipping BlockedNumberContractTest");
@@ -250,6 +258,7 @@
assertFalse(BlockedNumberContract.isBlocked(mContext, ""));
}
+ @CddTest(requirement="7.4.1.1/C-1-2")
public void testDelete() throws Exception {
if (!mIsSystemUser) {
Log.i(TAG, "skipping BlockedNumberContractTest");
@@ -321,6 +330,7 @@
}
}
+ @CddTest(requirement="7.4.1.1/C-1-2")
public void testProviderNotifiesChangesUsingContentObserver() throws Exception {
if (!mIsSystemUser) {
Log.i(TAG, "skipping BlockedNumberContractTest");
@@ -351,6 +361,7 @@
}
}
+ @CddTest(requirement="7.4.1.1/C-1-2")
public void testAccessingNonExistentMethod_fails() throws Exception {
if (!mIsSystemUser) {
Log.i(TAG, "skipping BlockedNumberContractTest");
diff --git a/tests/tests/security/AndroidManifest.xml b/tests/tests/security/AndroidManifest.xml
index 18159e3..7103969 100644
--- a/tests/tests/security/AndroidManifest.xml
+++ b/tests/tests/security/AndroidManifest.xml
@@ -57,6 +57,8 @@
android:label="CTS tests of android.security.cts">
<meta-data android:name="listener"
android:value="com.android.cts.runner.CtsTestRunListener" />
+ <meta-data android:name="listener"
+ android:value="com.android.cts.runner.CrashParserRunListener" />
</instrumentation>
</manifest>
diff --git a/tests/tests/security/AndroidTest.xml b/tests/tests/security/AndroidTest.xml
index 5482dd6..3a879d1 100644
--- a/tests/tests/security/AndroidTest.xml
+++ b/tests/tests/security/AndroidTest.xml
@@ -20,6 +20,7 @@
<option name="cleanup-apks" value="true" />
<option name="test-file-name" value="CtsSecurityTestCases.apk" />
</target_preparer>
+ <target_preparer class="com.android.compatibility.common.tradefed.targetprep.CrashReporter" />
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.security.cts" />
<option name="runtime-hint" value="1h8m15s" />
diff --git a/tests/tests/security/src/android/security/cts/SSLConscryptPlainTextExposureTest.java b/tests/tests/security/src/android/security/cts/SSLConscryptPlainTextExposureTest.java
index 8f6477e..5002a72 100644
--- a/tests/tests/security/src/android/security/cts/SSLConscryptPlainTextExposureTest.java
+++ b/tests/tests/security/src/android/security/cts/SSLConscryptPlainTextExposureTest.java
@@ -98,7 +98,7 @@
public SocketChannel socketChannel;
public SSLEngine clientEngine;
public String remoteAddress = "127.0.0.1";
- public int port = 9000;
+ public int port = 7000;
public ByteBuffer[] dataOutAppBuffers = new ByteBuffer[3];
public ByteBuffer dataOutNetBuffer;
public ByteBuffer hsInAppBuffer, hsInNetBuffer, hsOutAppBuffer, hsOutNetBuffer;
@@ -492,7 +492,7 @@
public ByteBuffer dataInAppBuffer, dataInNetBuffer;
final String hostAddress = "127.0.0.1";
- public int port = 9000;
+ public int port = 7000;
public boolean bActive = false;
public Selector selector;
diff --git a/tests/tests/security/src/android/security/cts/StagefrightTest.java b/tests/tests/security/src/android/security/cts/StagefrightTest.java
index 31769e1..adf8131 100644
--- a/tests/tests/security/src/android/security/cts/StagefrightTest.java
+++ b/tests/tests/security/src/android/security/cts/StagefrightTest.java
@@ -47,8 +47,13 @@
import android.view.Surface;
import android.webkit.cts.CtsTestServer;
+import com.android.compatibility.common.util.CrashUtils;
+
import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.File;
import java.io.FileInputStream;
+import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
@@ -58,6 +63,10 @@
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
import android.security.cts.R;
@@ -70,6 +79,7 @@
static final String TAG = "StagefrightTest";
private final long TIMEOUT_NS = 10000000000L; // 10 seconds.
+ private final static long CHECK_INTERVAL = 50;
public StagefrightTest() {
}
@@ -898,10 +908,41 @@
return new Surface(surfaceTex);
}
+ public JSONArray getCrashReport(String testname, long timeout)
+ throws InterruptedException {
+ Log.i(TAG, CrashUtils.UPLOAD_REQUEST);
+ File reportFile = new File(CrashUtils.DEVICE_PATH, testname);
+ File lockFile = new File(CrashUtils.DEVICE_PATH, CrashUtils.LOCK_FILENAME);
+ while ((!reportFile.exists() || !lockFile.exists()) && timeout > 0) {
+ Thread.sleep(CHECK_INTERVAL);
+ timeout -= CHECK_INTERVAL;
+ }
+ if (!reportFile.exists() || !reportFile.isFile() || !lockFile.exists()) {
+ return null;
+ }
+ try (BufferedReader reader = new BufferedReader(new FileReader(reportFile))) {
+ StringBuilder json = new StringBuilder();
+ String line = reader.readLine();
+ while (line != null) {
+ json.append(line);
+ line = reader.readLine();
+ }
+ return new JSONArray(json.toString());
+ } catch (IOException | JSONException e) {
+ Log.e(TAG, "Failed to deserialize crash list with error " + e.getMessage());
+ return null;
+ }
+ }
+
class MediaPlayerCrashListener
- implements MediaPlayer.OnErrorListener,
+ implements MediaPlayer.OnErrorListener,
MediaPlayer.OnPreparedListener,
MediaPlayer.OnCompletionListener {
+
+ private final String[] validProcessNames = {
+ "mediaserver", "mediadrmserver", "media.extractor", "media.codec", "media.metrics"
+ };
+
@Override
public boolean onError(MediaPlayer mp, int newWhat, int extra) {
Log.i(TAG, "error: " + newWhat + "/" + extra);
@@ -942,6 +983,21 @@
// and see if more errors show up.
SystemClock.sleep(1000);
}
+ if (what == MediaPlayer.MEDIA_ERROR_SERVER_DIED) {
+ JSONArray crashes = getCrashReport(getName(), 5000);
+ if (crashes == null) {
+ Log.e(TAG, "Crash results not found for test " + getName());
+ return what;
+ } else if (CrashUtils.detectCrash(validProcessNames, true, crashes)) {
+ return what;
+ } else {
+ Log.i(TAG, "Crash ignored due to no security crash found for test " +
+ getName());
+ // 0 is the code for no error.
+ return 0;
+ }
+
+ }
return what;
}
diff --git a/tests/tests/systemintents/Android.mk b/tests/tests/systemintents/Android.mk
index c68aaf1..16b8998 100644
--- a/tests/tests/systemintents/Android.mk
+++ b/tests/tests/systemintents/Android.mk
@@ -27,7 +27,7 @@
LOCAL_PACKAGE_NAME := CtsSystemIntentTestCases
-LOCAL_STATIC_JAVA_LIBRARIES := ctstestrunner-axt androidx.test.rules
+LOCAL_STATIC_JAVA_LIBRARIES := ctstestrunner-axt androidx.test.rules compatibility-device-util-axt
LOCAL_SDK_VERSION := test_current
diff --git a/tests/tests/systemintents/src/android/systemintents/cts/TestSystemIntents.java b/tests/tests/systemintents/src/android/systemintents/cts/TestSystemIntents.java
index f6911b3..941fef7 100644
--- a/tests/tests/systemintents/src/android/systemintents/cts/TestSystemIntents.java
+++ b/tests/tests/systemintents/src/android/systemintents/cts/TestSystemIntents.java
@@ -29,6 +29,8 @@
import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.compatibility.common.util.CddTest;
+
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -81,6 +83,7 @@
new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS))
};
+ @CddTest(requirement="3.8.3.3/C-1-1,3.2.3.5/C-1-1,7.4.7/C-1-2,C-2-3,6.2/C-0-1")
@Test
public void testSystemIntents() {
final PackageManager pm = InstrumentationRegistry.getContext().getPackageManager();
diff --git a/tests/tests/telecom/src/android/telecom/cts/ExtendedInCallServiceTest.java b/tests/tests/telecom/src/android/telecom/cts/ExtendedInCallServiceTest.java
index 954112b..2d60870 100644
--- a/tests/tests/telecom/src/android/telecom/cts/ExtendedInCallServiceTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/ExtendedInCallServiceTest.java
@@ -34,6 +34,8 @@
import android.telecom.VideoProfile;
import android.telephony.TelephonyManager;
+import com.android.compatibility.common.util.CddTest;
+
import java.util.List;
/**
@@ -374,6 +376,7 @@
}
}
+ @CddTest(requirement="7.4.1.1/C-1-3")
public void testIncomingCallFromBlockedNumber_IsRejected() throws Exception {
if (!mShouldTestTelecom) {
return;
diff --git a/tests/tests/telecom/src/android/telecom/cts/TelecomAvailabilityTest.java b/tests/tests/telecom/src/android/telecom/cts/TelecomAvailabilityTest.java
index 8163520..fba31a0 100644
--- a/tests/tests/telecom/src/android/telecom/cts/TelecomAvailabilityTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/TelecomAvailabilityTest.java
@@ -29,6 +29,8 @@
import android.test.InstrumentationTestCase;
import android.util.Log;
+import com.android.compatibility.common.util.CddTest;
+
import java.util.ArrayList;
import java.util.List;
@@ -99,6 +101,7 @@
telephonyMatches);
}
+ @CddTest(requirement="7.4.1.1/C-1-6")
public void testTelecomCanManageBlockedNumbers() {
if (!shouldTestTelecom(mContext)) {
return;
diff --git a/tests/tests/view/res/layout-land/drag_drop_layout.xml b/tests/tests/view/res/layout-land/drag_drop_layout.xml
new file mode 100644
index 0000000..79aa5df
--- /dev/null
+++ b/tests/tests/view/res/layout-land/drag_drop_layout.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Copyright (C) 2019 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.
+ -->
+
+<LinearLayout
+ android:id="@+id/drag_drop_activity_main"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <FrameLayout
+ android:id="@+id/container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="42dp"
+ android:background="#BBBBBB">
+ <FrameLayout
+ android:id="@+id/subcontainer"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="42dp"
+ android:background="#666666">
+ <View
+ android:id="@+id/inner"
+ android:layout_width="42dp"
+ android:layout_height="42dp"
+ android:layout_margin="42dp"
+ android:background="#00FF00" />
+ </FrameLayout>
+ </FrameLayout>
+ <View
+ android:id="@+id/draggable"
+ android:layout_width="42dp"
+ android:layout_height="42dp"
+ android:layout_margin="42dp"
+ android:background="#0000FF" />
+</LinearLayout>
diff --git a/tests/tests/view/res/layout/drag_drop_layout.xml b/tests/tests/view/res/layout/drag_drop_layout.xml
index cf882bd..9f4614c 100644
--- a/tests/tests/view/res/layout/drag_drop_layout.xml
+++ b/tests/tests/view/res/layout/drag_drop_layout.xml
@@ -25,26 +25,26 @@
android:id="@+id/container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_margin="42dp"
+ android:layout_margin="21dp"
android:background="#BBBBBB">
<FrameLayout
android:id="@+id/subcontainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_margin="42dp"
+ android:layout_margin="21dp"
android:background="#666666">
<View
android:id="@+id/inner"
- android:layout_width="42dp"
- android:layout_height="42dp"
- android:layout_margin="42dp"
+ android:layout_width="21dp"
+ android:layout_height="21dp"
+ android:layout_margin="21dp"
android:background="#00FF00" />
</FrameLayout>
</FrameLayout>
<View
android:id="@+id/draggable"
- android:layout_width="42dp"
- android:layout_height="42dp"
- android:layout_margin="42dp"
+ android:layout_width="21dp"
+ android:layout_height="21dp"
+ android:layout_margin="21dp"
android:background="#0000FF" />
</LinearLayout>
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewTest.java
index f5f3139..14dd431 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewTest.java
@@ -1781,10 +1781,30 @@
&& mOnUiThread.getHeight() != 0;
}
}.run();
- assertEquals(mOnUiThread.getHeight(),
- mOnUiThread.getContentHeight() * mOnUiThread.getScale(), 2f);
- final int pageHeight = 600;
+ final int tolerance = 2;
+ // getHeight() returns physical pixels and it is from web contents' size, getContentHeight()
+ // returns CSS pixels and it is from compositor. In order to compare these two values, we
+ // need to scale getContentHeight() by the device scale factor. This also amplifies any
+ // rounding errors. Internally, getHeight() could also have rounding error first and then
+ // times device scale factor, so we are comparing two rounded numbers below.
+ // We allow 2 * getScale() as the delta, because getHeight() and getContentHeight() may
+ // use different rounding algorithms and the results are from different computation
+ // sequences. The extreme case is that in CSS pixel we have 2 as differences (0.9999 rounded
+ // down and 1.0001 rounded up), therefore we ended with 2 * getScale().
+ assertEquals(
+ mOnUiThread.getHeight(),
+ mOnUiThread.getContentHeight() * mOnUiThread.getScale(),
+ tolerance * mOnUiThread.getScale());
+
+ // Make pageHeight bigger than the larger dimension of the device, so the page is taller
+ // than viewport. Because when layout_height set to match_parent, getContentHeight() will
+ // give maximum value between the actual web content height and the viewport height. When
+ // viewport height is bigger, |extraSpace| below is not the extra space on the web page.
+ // Note that we are passing physical pixels rather than CSS pixels here, since screen
+ // density scale is generally greater than 1, it only makes the page content taller.
+ DisplayMetrics metrics = mOnUiThread.getDisplayMetrics();
+ final int pageHeight = Math.max(metrics.widthPixels, metrics.heightPixels);
// set the margin to 0
final String p = "<p style=\"height:" + pageHeight
+ "px;margin:0px auto;\">Get the height of HTML content.</p>";
@@ -1803,7 +1823,13 @@
new PollingCheck() {
@Override
protected boolean check() {
- return pageHeight + pageHeight + extraSpace == mOnUiThread.getContentHeight();
+ // |pageHeight| is accurate, |extraSpace| = getContentheight() - |pageHeight|, so it
+ // might have rounding error +-1, also getContentHeight() might have rounding error
+ // +-1, so we allow error 2. Note that |pageHeight|, |extraSpace| and
+ // getContentHeight() are all CSS pixels.
+ final int expectedContentHeight = pageHeight + pageHeight + extraSpace;
+ return Math.abs(expectedContentHeight - mOnUiThread.getContentHeight())
+ <= tolerance;
}
}.run();
}
diff --git a/tests/tests/widget/src/android/widget/cts/ZoomButtonTest.java b/tests/tests/widget/src/android/widget/cts/ZoomButtonTest.java
index acd0c4c..54d7c22 100644
--- a/tests/tests/widget/src/android/widget/cts/ZoomButtonTest.java
+++ b/tests/tests/widget/src/android/widget/cts/ZoomButtonTest.java
@@ -44,10 +44,6 @@
import org.junit.runner.RunWith;
import org.xmlpull.v1.XmlPullParser;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ZoomButtonTest {
@@ -128,87 +124,62 @@
assertFalse(mZoomButton.dispatchUnhandledMove(null, View.FOCUS_DOWN));
}
- private void verifyZoomSpeed(ZoomClickListener zoomClickListener, long zoomSpeedMs) {
- mZoomButton.setZoomSpeed(zoomSpeedMs);
-
- final long startTime = System.nanoTime();
- // Emulate long click that "lasts" for ten seconds
- CtsTouchUtils.emulateLongPressOnViewCenter(mInstrumentation, mZoomButton, 10000);
-
- final List<Long> callbackInvocations = zoomClickListener.getClickTimes();
- assertFalse("Expecting at least one callback", callbackInvocations.isEmpty());
-
- // Verify that the first callback is fired after the system-level long press timeout.
- final long minTimeUntilFirstInvocationMs = ViewConfiguration.getLongPressTimeout();
- final long actualTimeUntilFirstInvocationNs = callbackInvocations.get(0) - startTime;
- assertTrue("First callback not during long press timeout was " +
- actualTimeUntilFirstInvocationNs / NANOS_IN_MILLI +
- " while long press timeout is " + minTimeUntilFirstInvocationMs,
- (callbackInvocations.get(0) - startTime) >
- minTimeUntilFirstInvocationMs * NANOS_IN_MILLI);
-
- // Verify that subsequent callbacks are at least zoom-speed milliseconds apart. Note that
- // we do not have any hard guarantee about the max limit on the time between successive
- // callbacks.
- final long minTimeBetweenInvocationsNs = zoomSpeedMs * NANOS_IN_MILLI;
- if (callbackInvocations.size() > 1) {
- for (int i = 0; i < callbackInvocations.size() - 1; i++) {
- final long actualTimeBetweenInvocationsNs =
- (callbackInvocations.get(i + 1) - callbackInvocations.get(i)) *
- NANOS_IN_MILLI;
- assertTrue("Callback " + (i + 1) + " happened " +
- actualTimeBetweenInvocationsNs / NANOS_IN_MILLI +
- " after the previous one, while zoom speed is " + zoomSpeedMs,
- actualTimeBetweenInvocationsNs > minTimeBetweenInvocationsNs);
- }
- }
- }
-
- @LargeTest
- @Test
- public void testOnLongClick() {
- // Since Mockito doesn't have utilities to track the timestamps of method invocations,
- // we're using our own custom click listener for that. We want to verify that the
- // first listener invocation was after long press timeout, and the rest were spaced
- // by at least our zoom speed milliseconds
-
- mZoomButton.setEnabled(true);
- ZoomClickListener zoomClickListener = new ZoomClickListener();
- mZoomButton.setOnClickListener(zoomClickListener);
-
- verifyZoomSpeed(zoomClickListener, 2000);
- }
-
@LargeTest
@Test
public void testSetZoomSpeed() {
- final long[] zoomSpeeds = { 100, -1, 5000, 1000, 2500 };
+ final long[] zoomSpeeds = { 0, 100 };
mZoomButton.setEnabled(true);
ZoomClickListener zoomClickListener = new ZoomClickListener();
mZoomButton.setOnClickListener(zoomClickListener);
for (long zoomSpeed : zoomSpeeds) {
- // Reset the tracker list of our listener, but continue using it for testing
+ // Reset the tracking state of our listener, but continue using it for testing
// various zoom speeds on the same ZoomButton
zoomClickListener.reset();
- verifyZoomSpeed(zoomClickListener, zoomSpeed);
+
+ mZoomButton.setZoomSpeed(zoomSpeed);
+
+ final long startTime = System.nanoTime();
+ // Emulate long click
+ long longPressWait = ViewConfiguration.getLongPressTimeout()
+ + zoomSpeed + 100;
+ CtsTouchUtils.emulateLongPressOnViewCenter(mInstrumentation, mZoomButton,
+ longPressWait);
+
+ final Long callbackFirstInvocationTime = zoomClickListener.getTimeOfFirstClick();
+ assertNotNull("Expecting at least one callback", callbackFirstInvocationTime);
+
+ // Verify that the first callback is fired after the system-level long press timeout.
+ final long minTimeUntilFirstInvocationMs = ViewConfiguration.getLongPressTimeout();
+ final long actualTimeUntilFirstInvocationNs = callbackFirstInvocationTime - startTime;
+ assertTrue("First callback not during long press timeout was "
+ + actualTimeUntilFirstInvocationNs / NANOS_IN_MILLI
+ + " while long press timeout is " + minTimeUntilFirstInvocationMs,
+ (callbackFirstInvocationTime - startTime)
+ > minTimeUntilFirstInvocationMs * NANOS_IN_MILLI);
+ assertTrue("First callback should have happened sooner than "
+ + actualTimeUntilFirstInvocationNs / NANOS_IN_MILLI,
+ (callbackFirstInvocationTime - startTime)
+ <= (minTimeUntilFirstInvocationMs + 100) * NANOS_IN_MILLI);
}
}
private static class ZoomClickListener implements View.OnClickListener {
- private List<Long> mClickTimes = new ArrayList<>();
+ private Long mTimeOfFirstClick = null;
public void reset() {
- mClickTimes.clear();
+ mTimeOfFirstClick = null;
}
- public List<Long> getClickTimes() {
- return Collections.unmodifiableList(mClickTimes);
+ public Long getTimeOfFirstClick() {
+ return mTimeOfFirstClick;
}
public void onClick(View v) {
- // Add the current system time to the tracker list
- mClickTimes.add(System.nanoTime());
+ if (mTimeOfFirstClick == null) {
+ // Mark the current system time as the time of first click
+ mTimeOfFirstClick = System.nanoTime();
+ }
}
}
}
diff --git a/tests/tvprovider/src/android/tvprovider/cts/TvProviderPerfTest.java b/tests/tvprovider/src/android/tvprovider/cts/TvProviderPerfTest.java
index e9c7ae0..e18df4c 100644
--- a/tests/tvprovider/src/android/tvprovider/cts/TvProviderPerfTest.java
+++ b/tests/tvprovider/src/android/tvprovider/cts/TvProviderPerfTest.java
@@ -86,6 +86,11 @@
final int TRANSACTION_SIZE = 1000;
double[] applyBatchTimes = MeasureTime.measure(TRANSACTION_RUNS, new MeasureRun() {
@Override
+ public void prepare(int i) {
+ mContentResolver.delete(Channels.CONTENT_URI, null, null);
+ }
+
+ @Override
public void run(int i) {
operations.clear();
for (int j = 0; j < TRANSACTION_SIZE; ++j) {