blob: 2f2b8f70fd7a2551671bcee8323c25bb36b90ea0 [file] [log] [blame]
/*
* Copyright (C) 2008 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.compatibility.common.util;
import static android.view.ViewTreeObserver.OnDrawListener;
import static android.view.ViewTreeObserver.OnGlobalLayoutListener;
import static org.mockito.hamcrest.MockitoHamcrest.argThat;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.test.rule.ActivityTestRule;
import android.text.Editable;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewTreeObserver;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import junit.framework.Assert;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* The useful methods for widget test.
*/
public class WidgetTestUtils {
/**
* Assert that two bitmaps have identical content (same dimensions, same configuration,
* same pixel content).
*
* @param b1 the first bitmap which needs to compare.
* @param b2 the second bitmap which needs to compare.
*/
public static void assertEquals(Bitmap b1, Bitmap b2) {
if (b1 == b2) {
return;
}
if (b1 == null || b2 == null) {
Assert.fail("the bitmaps are not equal");
}
// b1 and b2 are all not null.
if (b1.getWidth() != b2.getWidth() || b1.getHeight() != b2.getHeight()
|| b1.getConfig() != b2.getConfig()) {
Assert.fail("the bitmaps are not equal");
}
int w = b1.getWidth();
int h = b1.getHeight();
int s = w * h;
int[] pixels1 = new int[s];
int[] pixels2 = new int[s];
b1.getPixels(pixels1, 0, w, 0, 0, w, h);
b2.getPixels(pixels2, 0, w, 0, 0, w, h);
for (int i = 0; i < s; i++) {
if (pixels1[i] != pixels2[i]) {
Assert.fail("the bitmaps are not equal");
}
}
}
/**
* Find beginning of the special element.
* @param parser XmlPullParser will be parsed.
* @param firstElementName the target element name.
*
* @throws XmlPullParserException if XML Pull Parser related faults occur.
* @throws IOException if I/O-related error occur when parsing.
*/
public static final void beginDocument(XmlPullParser parser, String firstElementName)
throws XmlPullParserException, IOException {
Assert.assertNotNull(parser);
Assert.assertNotNull(firstElementName);
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
;
}
if (!parser.getName().equals(firstElementName)) {
throw new XmlPullParserException("Unexpected start tag: found " + parser.getName()
+ ", expected " + firstElementName);
}
}
/**
* Compare the expected pixels with actual, scaling for the target context density
*
* @throws AssertionFailedError
*/
public static void assertScaledPixels(int expected, int actual, Context context) {
Assert.assertEquals(expected * context.getResources().getDisplayMetrics().density,
actual, 3);
}
/** Converts dips into pixels using the {@link Context}'s density. */
public static int convertDipToPixels(Context context, int dip) {
float density = context.getResources().getDisplayMetrics().density;
return Math.round(density * dip);
}
/**
* Retrieve a bitmap that can be used for comparison on any density
* @param resources
* @return the {@link Bitmap} or <code>null</code>
*/
public static Bitmap getUnscaledBitmap(Resources resources, int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;
return BitmapFactory.decodeResource(resources, resId, options);
}
/**
* Retrieve a dithered bitmap that can be used for comparison on any density
* @param resources
* @param config the preferred config for the returning bitmap
* @return the {@link Bitmap} or <code>null</code>
*/
public static Bitmap getUnscaledAndDitheredBitmap(Resources resources,
int resId, Bitmap.Config config) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inDither = true;
options.inScaled = false;
options.inPreferredConfig = config;
return BitmapFactory.decodeResource(resources, resId, options);
}
/**
* Argument matcher for equality check of a CharSequence.
*
* @param expected expected CharSequence
*
* @return
*/
public static CharSequence sameCharSequence(final CharSequence expected) {
return argThat(new BaseMatcher<CharSequence>() {
@Override
public boolean matches(Object o) {
if (o instanceof CharSequence) {
return TextUtils.equals(expected, (CharSequence) o);
}
return false;
}
@Override
public void describeTo(Description description) {
description.appendText("doesn't match " + expected);
}
});
}
/**
* Argument matcher for equality check of an Editable.
*
* @param expected expected Editable
*
* @return
*/
public static Editable sameEditable(final Editable expected) {
return argThat(new BaseMatcher<Editable>() {
@Override
public boolean matches(Object o) {
if (o instanceof Editable) {
return TextUtils.equals(expected, (Editable) o);
}
return false;
}
@Override
public void describeTo(Description description) {
description.appendText("doesn't match " + expected);
}
});
}
/**
* Runs the specified {@link Runnable} on the main thread and ensures that the specified
* {@link View}'s tree is drawn before returning.
*
* @param activityTestRule the activity test rule used to run the test
* @param view the view whose tree should be drawn before returning
* @param runner the runnable to run on the main thread, or {@code null} to
* simply force invalidation and a draw pass
*/
public static void runOnMainAndDrawSync(@NonNull final ActivityTestRule activityTestRule,
@NonNull final View view, @Nullable final Runnable runner) {
final CountDownLatch latch = new CountDownLatch(1);
try {
activityTestRule.runOnUiThread(() -> {
final OnDrawListener listener = new OnDrawListener() {
@Override
public void onDraw() {
// posting so that the sync happens after the draw that's about to happen
view.post(() -> {
activityTestRule.getActivity().getWindow().getDecorView()
.getViewTreeObserver().removeOnDrawListener(this);
latch.countDown();
});
}
};
activityTestRule.getActivity().getWindow().getDecorView()
.getViewTreeObserver().addOnDrawListener(listener);
if (runner != null) {
runner.run();
}
view.invalidate();
});
Assert.assertTrue("Expected draw pass occurred within 5 seconds",
latch.await(5, TimeUnit.SECONDS));
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
/**
* Runs the specified Runnable on the main thread and ensures that the activity's view tree is
* laid out before returning.
*
* @param activityTestRule the activity test rule used to run the test
* @param runner the runnable to run on the main thread. {@code null} is
* allowed, and simply means that there no runnable is required.
* @param forceLayout true if there should be an explicit call to requestLayout(),
* false otherwise
*/
public static void runOnMainAndLayoutSync(@NonNull final ActivityTestRule activityTestRule,
@Nullable final Runnable runner, boolean forceLayout)
throws Throwable {
runOnMainAndLayoutSync(activityTestRule,
activityTestRule.getActivity().getWindow().getDecorView(), runner, forceLayout);
}
/**
* Runs the specified Runnable on the main thread and ensures that the specified view is
* laid out before returning.
*
* @param activityTestRule the activity test rule used to run the test
* @param view The view
* @param runner the runnable to run on the main thread. {@code null} is
* allowed, and simply means that there no runnable is required.
* @param forceLayout true if there should be an explicit call to requestLayout(),
* false otherwise
*/
public static void runOnMainAndLayoutSync(@NonNull final ActivityTestRule activityTestRule,
@NonNull final View view, @Nullable final Runnable runner, boolean forceLayout)
throws Throwable {
final View rootView = view.getRootView();
final CountDownLatch latch = new CountDownLatch(1);
activityTestRule.runOnUiThread(() -> {
final OnGlobalLayoutListener listener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
// countdown immediately since the layout we were waiting on has happened
latch.countDown();
}
};
rootView.getViewTreeObserver().addOnGlobalLayoutListener(listener);
if (runner != null) {
runner.run();
}
if (forceLayout) {
rootView.requestLayout();
}
});
try {
Assert.assertTrue("Expected layout pass within 5 seconds",
latch.await(5, TimeUnit.SECONDS));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}