blob: 0e2f5cf266507e7bdba99190ec1c507fd573b091 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.widget.cts;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.app.Activity;
import android.app.Instrumentation;
import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Process;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.FrameLayout;
import android.widget.ListView;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
import android.widget.StackView;
import android.widget.cts.appwidget.MyAppWidgetProvider;
import android.widget.cts.appwidget.MyAppWidgetService;
import com.android.compatibility.common.util.PollingCheck;
import com.android.compatibility.common.util.SystemUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Test {@link RemoteViews} that expect to operate within a {@link AppWidgetHostView} root.
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class RemoteViewsWidgetTest {
public static final String[] COUNTRY_LIST = new String[] {
"Argentina", "Australia", "Belize", "Botswana", "Brazil", "Cameroon", "China", "Cyprus",
"Denmark", "Djibouti", "Ethiopia", "Fiji", "Finland", "France", "Gabon", "Germany",
"Ghana", "Haiti", "Honduras", "Iceland", "India", "Indonesia", "Ireland", "Italy",
"Japan", "Kiribati", "Laos", "Lesotho", "Liberia", "Malaysia", "Mongolia", "Myanmar",
"Nauru", "Norway", "Oman", "Pakistan", "Philippines", "Portugal", "Romania", "Russia",
"Rwanda", "Singapore", "Slovakia", "Slovenia", "Somalia", "Swaziland", "Togo", "Tuvalu",
"Uganda", "Ukraine", "United States", "Vanuatu", "Venezuela", "Zimbabwe"
};
private static final String GRANT_BIND_APP_WIDGET_PERMISSION_COMMAND =
"appwidget grantbind --package android.widget.cts --user 0";
private static final String REVOKE_BIND_APP_WIDGET_PERMISSION_COMMAND =
"appwidget revokebind --package android.widget.cts --user 0";
private static final long TEST_TIMEOUT_MS = 5000;
@Rule
public ActivityTestRule<RemoteViewsCtsActivity> mActivityRule =
new ActivityTestRule<>(RemoteViewsCtsActivity.class);
private Instrumentation mInstrumentation;
private Context mContext;
private boolean mHasAppWidgets;
private AppWidgetHostView mAppWidgetHostView;
private int mAppWidgetId;
private StackView mStackView;
private ListView mListView;
private AppWidgetHost mAppWidgetHost;
@Before
public void setup() throws Throwable {
mInstrumentation = InstrumentationRegistry.getInstrumentation();
mContext = mInstrumentation.getTargetContext();
mHasAppWidgets = hasAppWidgets();
if (!mHasAppWidgets) {
return;
}
// We want to bind widgets - run a shell command to grant bind permission to our
// package.
grantBindAppWidgetPermission();
mAppWidgetHost = new AppWidgetHost(mContext, 0);
mAppWidgetHost.deleteHost();
mAppWidgetHost.startListening();
// Configure the app widget provider behavior
final CountDownLatch providerCountDownLatch = new CountDownLatch(2);
MyAppWidgetProvider.configure(providerCountDownLatch, null, null);
// Grab the provider to be bound
final AppWidgetProviderInfo providerInfo = getAppWidgetProviderInfo();
// Allocate a widget id to bind
mAppWidgetId = mAppWidgetHost.allocateAppWidgetId();
// Bind the app widget
boolean isBinding = getAppWidgetManager().bindAppWidgetIdIfAllowed(mAppWidgetId,
providerInfo.getProfile(), providerInfo.provider, null);
assertTrue(isBinding);
// Wait for onEnabled and onUpdate calls on our provider
try {
assertTrue(providerCountDownLatch.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (InterruptedException ie) {
fail(ie.getMessage());
}
// Configure the app widget service behavior
final CountDownLatch factoryCountDownLatch = new CountDownLatch(2);
RemoteViewsService.RemoteViewsFactory factory =
mock(RemoteViewsService.RemoteViewsFactory.class);
when(factory.getCount()).thenReturn(COUNTRY_LIST.length);
doAnswer(new Answer<RemoteViews>() {
@Override
public RemoteViews answer(InvocationOnMock invocation) throws Throwable {
final int position = (Integer) invocation.getArguments()[0];
RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(),
R.layout.remoteviews_adapter_item);
remoteViews.setTextViewText(R.id.item, COUNTRY_LIST[position]);
// Set a fill-intent which will be used to fill-in the pending intent template
// which is set on the collection view in MyAppWidgetProvider.
Bundle extras = new Bundle();
extras.putString(MockURLSpanTestActivity.KEY_PARAM, COUNTRY_LIST[position]);
Intent fillInIntent = new Intent();
fillInIntent.putExtras(extras);
remoteViews.setOnClickFillInIntent(R.id.item, fillInIntent);
if (position == 0) {
factoryCountDownLatch.countDown();
}
return remoteViews;
}
}).when(factory).getViewAt(any(int.class));
when(factory.getViewTypeCount()).thenReturn(1);
MyAppWidgetService.setFactory(factory);
mActivityRule.runOnUiThread(
() -> mAppWidgetHostView = mAppWidgetHost.createView(
mContext, mAppWidgetId, providerInfo));
// Wait our factory to be called to create the first item
try {
assertTrue(factoryCountDownLatch.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (InterruptedException ie) {
fail(ie.getMessage());
}
// Add our host view to the activity behind this test. This is similar to how launchers
// add widgets to the on-screen UI.
ViewGroup root = (ViewGroup) mActivityRule.getActivity().findViewById(R.id.remoteView_host);
FrameLayout.MarginLayoutParams lp = new FrameLayout.MarginLayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
mAppWidgetHostView.setLayoutParams(lp);
mActivityRule.runOnUiThread(() -> root.addView(mAppWidgetHostView));
}
@After
public void teardown() {
if (!mHasAppWidgets) {
return;
}
mAppWidgetHost.deleteHost();
revokeBindAppWidgetPermission();
}
private void grantBindAppWidgetPermission() {
try {
SystemUtil.runShellCommand(mInstrumentation, GRANT_BIND_APP_WIDGET_PERMISSION_COMMAND);
} catch (IOException e) {
fail("Error granting app widget permission. Command: "
+ GRANT_BIND_APP_WIDGET_PERMISSION_COMMAND + ": ["
+ e.getMessage() + "]");
}
}
private void revokeBindAppWidgetPermission() {
try {
SystemUtil.runShellCommand(mInstrumentation, REVOKE_BIND_APP_WIDGET_PERMISSION_COMMAND);
} catch (IOException e) {
fail("Error revoking app widget permission. Command: "
+ REVOKE_BIND_APP_WIDGET_PERMISSION_COMMAND + ": ["
+ e.getMessage() + "]");
}
}
private boolean hasAppWidgets() {
return mInstrumentation.getTargetContext().getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS);
}
private AppWidgetManager getAppWidgetManager() {
return (AppWidgetManager) mContext.getSystemService(Context.APPWIDGET_SERVICE);
}
private AppWidgetProviderInfo getAppWidgetProviderInfo() {
ComponentName firstComponentName = new ComponentName(mContext.getPackageName(),
MyAppWidgetProvider.class.getName());
return getProviderInfo(firstComponentName);
}
private AppWidgetProviderInfo getProviderInfo(ComponentName componentName) {
List<AppWidgetProviderInfo> providers = getAppWidgetManager().getInstalledProviders();
final int providerCount = providers.size();
for (int i = 0; i < providerCount; i++) {
AppWidgetProviderInfo provider = providers.get(i);
if (componentName.equals(provider.provider)
&& Process.myUserHandle().equals(provider.getProfile())) {
return provider;
}
}
return null;
}
@Test
public void testInitialState() {
if (!mHasAppWidgets) {
return;
}
assertNotNull(mAppWidgetHostView);
mStackView = (StackView) mAppWidgetHostView.findViewById(R.id.remoteViews_stack);
assertNotNull(mStackView);
assertEquals(COUNTRY_LIST.length, mStackView.getCount());
assertEquals(0, mStackView.getDisplayedChild());
assertEquals(R.id.remoteViews_empty, mStackView.getEmptyView().getId());
}
private void verifySetDisplayedChild(int displayedChildIndex) {
final CountDownLatch updateLatch = new CountDownLatch(1);
MyAppWidgetProvider.configure(updateLatch, null, null);
// Create the intent to update the widget. Note that we're passing the value
// for displayed child index in the intent
Intent intent = new Intent(mContext, MyAppWidgetProvider.class);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { mAppWidgetId });
intent.putExtra(MyAppWidgetProvider.KEY_DISPLAYED_CHILD_INDEX, displayedChildIndex);
mContext.sendBroadcast(intent);
// Wait until the update request has been processed
try {
assertTrue(updateLatch.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (InterruptedException ie) {
fail(ie.getMessage());
}
// And wait until the underlying StackView has been updated to switch to the requested
// child
PollingCheck.waitFor(TEST_TIMEOUT_MS,
() -> mStackView.getDisplayedChild() == displayedChildIndex);
}
@Test
public void testSetDisplayedChild() {
if (!mHasAppWidgets) {
return;
}
mStackView = (StackView) mAppWidgetHostView.findViewById(R.id.remoteViews_stack);
verifySetDisplayedChild(4);
verifySetDisplayedChild(2);
verifySetDisplayedChild(6);
}
private void verifyShowCommand(String intentShowKey, int expectedDisplayedChild) {
final CountDownLatch updateLatch = new CountDownLatch(1);
MyAppWidgetProvider.configure(updateLatch, null, null);
// Create the intent to update the widget. Note that we're passing the "indication"
// which one of showNext / showPrevious APIs to execute in the intent that we're
// creating.
Intent intent = new Intent(mContext, MyAppWidgetProvider.class);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { mAppWidgetId });
intent.putExtra(intentShowKey, true);
mContext.sendBroadcast(intent);
// Wait until the update request has been processed
try {
assertTrue(updateLatch.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (InterruptedException ie) {
fail(ie.getMessage());
}
// And wait until the underlying StackView has been updated to switch to the expected
// child
PollingCheck.waitFor(TEST_TIMEOUT_MS,
() -> mStackView.getDisplayedChild() == expectedDisplayedChild);
}
@Test
public void testShowNextPrevious() {
if (!mHasAppWidgets) {
return;
}
mStackView = (StackView) mAppWidgetHostView.findViewById(R.id.remoteViews_stack);
// Two forward
verifyShowCommand(MyAppWidgetProvider.KEY_SHOW_NEXT, 1);
verifyShowCommand(MyAppWidgetProvider.KEY_SHOW_NEXT, 2);
// Four back (looping to the end of the adapter data)
verifyShowCommand(MyAppWidgetProvider.KEY_SHOW_PREVIOUS, 1);
verifyShowCommand(MyAppWidgetProvider.KEY_SHOW_PREVIOUS, 0);
verifyShowCommand(MyAppWidgetProvider.KEY_SHOW_PREVIOUS, COUNTRY_LIST.length - 1);
verifyShowCommand(MyAppWidgetProvider.KEY_SHOW_PREVIOUS, COUNTRY_LIST.length - 2);
// And three forward (looping to the start of the adapter data)
verifyShowCommand(MyAppWidgetProvider.KEY_SHOW_NEXT, COUNTRY_LIST.length - 1);
verifyShowCommand(MyAppWidgetProvider.KEY_SHOW_NEXT, 0);
verifyShowCommand(MyAppWidgetProvider.KEY_SHOW_NEXT, 1);
}
private void verifyItemClickIntents(int indexToClick) throws Throwable {
Instrumentation.ActivityMonitor am = mInstrumentation.addMonitor(
MockURLSpanTestActivity.class.getName(), null, false);
mStackView = (StackView) mAppWidgetHostView.findViewById(R.id.remoteViews_stack);
PollingCheck.waitFor(() -> mStackView.getCurrentView() != null);
final View initialView = mStackView.getCurrentView();
mActivityRule.runOnUiThread(
() -> mStackView.performItemClick(initialView, indexToClick, 0L));
Activity newActivity = am.waitForActivityWithTimeout(TEST_TIMEOUT_MS);
assertNotNull(newActivity);
assertTrue(newActivity instanceof MockURLSpanTestActivity);
assertEquals(COUNTRY_LIST[indexToClick], ((MockURLSpanTestActivity) newActivity).getParam());
newActivity.finish();
}
@Test
public void testSetOnClickPendingIntent() throws Throwable {
if (!mHasAppWidgets) {
return;
}
verifyItemClickIntents(0);
// Switch to another child
verifySetDisplayedChild(2);
verifyItemClickIntents(2);
// And one more
verifyShowCommand(MyAppWidgetProvider.KEY_SHOW_NEXT, 3);
verifyItemClickIntents(3);
}
private class ListScrollListener implements AbsListView.OnScrollListener {
private CountDownLatch mLatchToNotify;
private int mTargetPosition;
public ListScrollListener(CountDownLatch latchToNotify, int targetPosition) {
mLatchToNotify = latchToNotify;
mTargetPosition = targetPosition;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
if ((mTargetPosition >= firstVisibleItem) &&
(mTargetPosition <= (firstVisibleItem + visibleItemCount))) {
mLatchToNotify.countDown();
}
}
}
@Test
public void testSetScrollPosition() {
if (!mHasAppWidgets) {
return;
}
mListView = (ListView) mAppWidgetHostView.findViewById(R.id.remoteViews_list);
final CountDownLatch updateLatch = new CountDownLatch(1);
final AtomicBoolean scrollToPositionIsComplete = new AtomicBoolean(false);
// We're configuring our provider with three parameters:
// 1. The CountDownLatch to be notified when the provider has been enabled
// 2. The gating condition that waits until ListView has populated its content
// so that we can proceed to call setScrollPosition on it
// 3. The gating condition that waits until the setScrollPosition has completed
// its processing / scrolling so that we can proceed to call
// setRelativeScrollPosition on it
MyAppWidgetProvider.configure(updateLatch, () -> mListView.getChildCount() > 0,
scrollToPositionIsComplete::get);
final int positionToScrollTo = COUNTRY_LIST.length - 10;
final int scrollByAmount = COUNTRY_LIST.length / 2;
final int offsetScrollTarget = positionToScrollTo - scrollByAmount;
// Register the first scroll listener on our ListView. The listener will notify our latch
// when the "target" item comes into view. If that never happens, the latch will
// time out and fail the test.
final CountDownLatch scrollToPositionLatch = new CountDownLatch(1);
mListView.setOnScrollListener(
new ListScrollListener(scrollToPositionLatch, positionToScrollTo));
// Create the intent to update the widget. Note that we're passing the "indication"
// to switch to our ListView in the intent that we're creating.
Intent intent = new Intent(mContext, MyAppWidgetProvider.class);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { mAppWidgetId });
intent.putExtra(MyAppWidgetProvider.KEY_SWITCH_TO_LIST, true);
intent.putExtra(MyAppWidgetProvider.KEY_SCROLL_POSITION, positionToScrollTo);
intent.putExtra(MyAppWidgetProvider.KEY_SCROLL_OFFSET, -scrollByAmount);
mContext.sendBroadcast(intent);
// Wait until the update request has been processed
try {
assertTrue(updateLatch.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (InterruptedException ie) {
fail(ie.getMessage());
}
// And wait until the underlying ListView has been updated to be visible
PollingCheck.waitFor(TEST_TIMEOUT_MS, () -> mListView.getVisibility() == View.VISIBLE);
// Wait until our ListView has at least one visible child view. At that point we know
// that not only the host view is on screen, but also that the list view has completed
// its layout pass after having asked its adapter to populate the list content.
PollingCheck.waitFor(TEST_TIMEOUT_MS, () -> mListView.getChildCount() > 0);
// If we're on a really big display, we might be in a situation where the position
// we're going to scroll to is already visible. In that case the logic in the rest
// of this test will never fire off a listener callback and then fail the test.
final int lastVisiblePosition = mListView.getLastVisiblePosition();
if (positionToScrollTo <= lastVisiblePosition) {
return;
}
boolean result = false;
try {
result = scrollToPositionLatch.await(20, TimeUnit.SECONDS);
} catch (InterruptedException e) {
// ignore
}
assertTrue("Timed out while waiting for the target view to be scrolled into view", result);
if ((offsetScrollTarget < 0) ||
(offsetScrollTarget >= mListView.getFirstVisiblePosition())) {
// We can't scroll up because the target is either already visible or negative
return;
}
// Now register another scroll listener on our ListView. The listener will notify our latch
// when our new "target" item comes into view. If that never happens, the latch will
// time out and fail the test.
final CountDownLatch scrollByOffsetLatch = new CountDownLatch(1);
mListView.setOnScrollListener(
new ListScrollListener(scrollByOffsetLatch, offsetScrollTarget));
// Update our atomic boolean to "kick off" the widget provider request to call
// setRelativeScrollPosition on our RemoteViews
scrollToPositionIsComplete.set(true);
try {
result = scrollByOffsetLatch.await(20, TimeUnit.SECONDS);
} catch (InterruptedException e) {
// ignore
}
assertTrue("Timed out while waiting for the target view to be scrolled into view", result);
}
}