Merge "Support nested prefetch in leanbacklib's gridviews" into nyc-support-25.2-dev
diff --git a/api/current.txt b/api/current.txt
index b9896a7..38fb0b6 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -2612,6 +2612,7 @@
public static abstract class PlaybackGlueHost.HostCallback {
ctor public PlaybackGlueHost.HostCallback();
+ method public void onHostDestroy();
method public void onHostPause();
method public void onHostResume();
method public void onHostStart();
@@ -4267,6 +4268,18 @@
method public void setStaticLabels(java.lang.CharSequence[]);
}
+ public class TimePicker extends android.support.v17.leanback.widget.picker.Picker {
+ ctor public TimePicker(android.content.Context, android.util.AttributeSet);
+ ctor public TimePicker(android.content.Context, android.util.AttributeSet, int);
+ method public int getHour();
+ method public int getMinute();
+ method public boolean is24Hour();
+ method public boolean isPm();
+ method public void setHour(int);
+ method public void setIs24Hour(boolean);
+ method public void setMinute(int);
+ }
+
}
package android.support.v17.preference {
@@ -5690,6 +5703,7 @@
method public android.content.ComponentName getServiceComponent();
method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
method public boolean isConnected();
+ method public void search(java.lang.String, android.os.Bundle, android.support.v4.media.MediaBrowserCompat.SearchCallback);
method public void subscribe(java.lang.String, android.support.v4.media.MediaBrowserCompat.SubscriptionCallback);
method public void subscribe(java.lang.String, android.os.Bundle, android.support.v4.media.MediaBrowserCompat.SubscriptionCallback);
method public void unsubscribe(java.lang.String);
@@ -5727,6 +5741,12 @@
field public static final int FLAG_PLAYABLE = 2; // 0x2
}
+ public static abstract class MediaBrowserCompat.SearchCallback {
+ ctor public MediaBrowserCompat.SearchCallback();
+ method public void onError(java.lang.String, android.os.Bundle);
+ method public void onSearchResult(java.lang.String, android.os.Bundle, java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>);
+ }
+
public static abstract class MediaBrowserCompat.SubscriptionCallback {
ctor public MediaBrowserCompat.SubscriptionCallback();
method public void onChildrenLoaded(java.lang.String, java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>);
@@ -5747,6 +5767,7 @@
method public abstract void onLoadChildren(java.lang.String, android.support.v4.media.MediaBrowserServiceCompat.Result<java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>>);
method public void onLoadChildren(java.lang.String, android.support.v4.media.MediaBrowserServiceCompat.Result<java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>>, android.os.Bundle);
method public void onLoadItem(java.lang.String, android.support.v4.media.MediaBrowserServiceCompat.Result<android.support.v4.media.MediaBrowserCompat.MediaItem>);
+ method public void onSearch(java.lang.String, android.os.Bundle, android.support.v4.media.MediaBrowserServiceCompat.Result<java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>>);
method public void setSessionToken(android.support.v4.media.session.MediaSessionCompat.Token);
field public static final java.lang.String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
}
@@ -5758,7 +5779,7 @@
field public static final java.lang.String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
field public static final java.lang.String EXTRA_RECENT = "android.service.media.extra.RECENT";
field public static final java.lang.String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
- field public static final java.lang.String EXTRA_SUGGESTION_KEYWORDS = "android.service.media.extra.SUGGESTION_KEYWORDS";
+ field public static final deprecated java.lang.String EXTRA_SUGGESTION_KEYWORDS = "android.service.media.extra.SUGGESTION_KEYWORDS";
}
public static class MediaBrowserServiceCompat.Result<T> {
diff --git a/build.gradle b/build.gradle
index f690dcb..6e593d7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -41,10 +41,11 @@
}
ext.supportVersion = '25.3.0-SNAPSHOT'
-ext.extraVersion = 43
+ext.extraVersion = 44
ext.supportRepoOut = ''
ext.buildNumber = Integer.toString(ext.extraVersion)
+ext.buildToolsVersion = '25.0.0'
ext.testRunnerVersion = '0.6-alpha'
ext.espressoVersion = '2.3-alpha'
@@ -52,19 +53,20 @@
// required for the doclava dependency.
ext.usePrebuilts = "true"
+// Prevent the Android Gradle plug-in from automatically downloading SDK dependencies.
+ext['android.builder.sdkDownload'] = false
+
final String platform = OperatingSystem.current().isMacOsX() ? 'darwin' : 'linux'
System.setProperty('android.dir', "${rootDir}/../../")
final String fullSdkPath = "${rootDir}/../../prebuilts/fullsdk-${platform}"
if (file(fullSdkPath).exists()) {
gradle.ext.currentSdk = 25
- ext.buildToolsVersion = '24.0.1'
project.ext.androidJar = files("${fullSdkPath}/platforms/android-${gradle.ext.currentSdk}/android.jar")
System.setProperty('android.home', "${rootDir}/../../prebuilts/fullsdk-${platform}")
File props = file("local.properties")
props.write "sdk.dir=${fullSdkPath}"
} else {
gradle.ext.currentSdk = 'current'
- ext.buildToolsVersion = '24.0.1'
project.ext.androidJar = files("${project.rootDir}/../../prebuilts/sdk/current/android.jar")
File props = file("local.properties")
props.write "android.dir=../../"
@@ -209,6 +211,9 @@
// Generates online docs.
task generateDocs(type: DoclavaTask, dependsOn: configurations.doclava) {
+ group = JavaBasePlugin.DOCUMENTATION_GROUP
+ description = 'Generates d.android.com style documentation.'
+
docletpath = configurations.doclava.resolve()
destinationDir = new File(project.docsDir, "online")
@@ -264,6 +269,9 @@
// Copies generated API files to current version.
task updateApi(type: UpdateApiTask, dependsOn: generateApi) {
+ group JavaBasePlugin.VERIFICATION_GROUP
+ description 'Invoke Doclava\'s ApiCheck tool to update current.txt based on current changes.'
+
newApiFile = new File(project.docsDir, 'release/current.txt')
oldApiFile = new File(project.rootDir, 'api/current.txt')
newRemovedApiFile = new File(project.docsDir, 'release/removed.txt')
@@ -434,7 +442,7 @@
}
project.afterEvaluate {
- // The archivesBaseName isn't available intially, so set it now
+ // The archivesBaseName isn't available initially, so set it now
def createZipTask = project.tasks.findByName("createSeparateZip")
if (createZipTask != null) {
createZipTask.appendix = archivesBaseName
diff --git a/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy b/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
index 3578958..c245f77 100644
--- a/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
+++ b/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
@@ -198,6 +198,7 @@
public CheckApiTask() {
group = 'Verification'
+ description = 'Invoke Doclava\'s ApiCheck tool to make sure current.txt is up to date.'
}
private Set<File> collectAndVerifyInputs() {
diff --git a/customtabs/tests/src/android/support/customtabs/PostMessageTest.java b/customtabs/tests/src/android/support/customtabs/PostMessageTest.java
index b212693..d20a06d 100644
--- a/customtabs/tests/src/android/support/customtabs/PostMessageTest.java
+++ b/customtabs/tests/src/android/support/customtabs/PostMessageTest.java
@@ -36,6 +36,7 @@
import org.junit.runner.RunWith;
import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -55,13 +56,14 @@
private Context mContext;
private CustomTabsServiceConnection mCustomTabsServiceConnection;
private PostMessageServiceConnection mPostMessageServiceConnection;
- private boolean mCustomTabsServiceConnected;
+ private AtomicBoolean mCustomTabsServiceConnected;
private boolean mPostMessageServiceConnected;
private CustomTabsSession mSession;
public PostMessageTest() {
mActivityTestRule = new ActivityTestRule<TestActivity>(TestActivity.class);
mServiceRule = new ServiceTestRule();
+ mCustomTabsServiceConnected = new AtomicBoolean(false);
}
@@ -89,13 +91,13 @@
mCustomTabsServiceConnection = new CustomTabsServiceConnection() {
@Override
public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
- mCustomTabsServiceConnected = true;
mSession = client.newSession(mCallback);
+ mCustomTabsServiceConnected.set(true);
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
- mCustomTabsServiceConnected = false;
+ mCustomTabsServiceConnected.set(false);
}
};
mPostMessageServiceConnection = new PostMessageServiceConnection(
@@ -126,10 +128,10 @@
PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
@Override
public boolean canProceed() {
- return mCustomTabsServiceConnected;
+ return mCustomTabsServiceConnected.get();
}
});
- assertTrue(mCustomTabsServiceConnected);
+ assertTrue(mCustomTabsServiceConnected.get());
assertTrue(mSession.requestPostMessageChannel(Uri.EMPTY));
assertEquals(CustomTabsService.RESULT_SUCCESS, mSession.postMessage("", null));
PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
diff --git a/design/src/android/support/design/internal/BottomNavigationMenuView.java b/design/src/android/support/design/internal/BottomNavigationMenuView.java
index d80cd64..5d7c19d 100644
--- a/design/src/android/support/design/internal/BottomNavigationMenuView.java
+++ b/design/src/android/support/design/internal/BottomNavigationMenuView.java
@@ -46,8 +46,7 @@
private final int mItemHeight;
private final OnClickListener mOnClickListener;
private final BottomNavigationAnimationHelperBase mAnimationHelper;
- private static final Pools.Pool<BottomNavigationItemView> sItemPool =
- new Pools.SynchronizedPool<>(5);
+ private final Pools.Pool<BottomNavigationItemView> mItemPool = new Pools.SynchronizedPool<>(5);
private boolean mShiftingMode = true;
@@ -250,12 +249,12 @@
}
public void buildMenuView() {
+ removeAllViews();
if (mButtons != null) {
for (BottomNavigationItemView item : mButtons) {
- sItemPool.release(item);
+ mItemPool.release(item);
}
}
- removeAllViews();
if (mMenu.size() == 0) {
mSelectedItemId = 0;
mSelectedItemPosition = 0;
@@ -307,7 +306,7 @@
}
private BottomNavigationItemView getNewItem() {
- BottomNavigationItemView item = sItemPool.acquire();
+ BottomNavigationItemView item = mItemPool.acquire();
if (item == null) {
item = new BottomNavigationItemView(getContext());
}
diff --git a/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarTest.java b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarTest.java
index 2db3471..aeca0be 100644
--- a/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarTest.java
+++ b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarTest.java
@@ -24,14 +24,18 @@
import android.os.Build;
import android.os.SystemClock;
import android.support.design.test.R;
+import android.support.test.filters.FlakyTest;
import android.support.test.filters.LargeTest;
import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.Suppress;
import android.widget.ImageView;
import org.junit.Test;
@LargeTest
public class AppBarWithCollapsingToolbarTest extends AppBarLayoutBaseTest {
+ @Suppress
+ @FlakyTest(bugId = 30701044)
@Test
public void testPinnedToolbar() throws Throwable {
configureContent(R.layout.design_appbar_toolbar_collapse_pin,
@@ -137,6 +141,8 @@
assertScrimAlpha(0);
}
+ @Suppress
+ @FlakyTest(bugId = 30701044)
@Test
public void testScrollingToolbar() throws Throwable {
configureContent(R.layout.design_appbar_toolbar_collapse_scroll,
@@ -247,6 +253,8 @@
assertScrimAlpha(0);
}
+ @Suppress
+ @FlakyTest(bugId = 30701044)
@Test
public void testScrollingToolbarEnterAlways() throws Throwable {
configureContent(R.layout.design_appbar_toolbar_collapse_scroll_enteralways,
@@ -352,6 +360,8 @@
assertScrimAlpha(0);
}
+ @Suppress
+ @FlakyTest(bugId = 30701044)
@Test
public void testPinnedToolbarAndAnchoredFab() throws Throwable {
configureContent(R.layout.design_appbar_toolbar_collapse_pin_with_fab,
@@ -418,6 +428,8 @@
}
}
+ @Suppress
+ @FlakyTest(bugId = 30701044)
@Test
public void testPinnedToolbarAndParallaxImage() throws Throwable {
configureContent(R.layout.design_appbar_toolbar_collapse_with_image,
@@ -521,6 +533,8 @@
* inherits from) has an issue with measuring children with margins when run on earlier
* versions of the platform.
*/
+ @Suppress
+ @FlakyTest(bugId = 30701044)
@Test
@SdkSuppress(minSdkVersion = 11)
public void testPinnedToolbarWithMargins() throws Throwable {
diff --git a/fragment/java/android/support/v4/app/FragmentActivity.java b/fragment/java/android/support/v4/app/FragmentActivity.java
index 0b38fd4..78e5370 100644
--- a/fragment/java/android/support/v4/app/FragmentActivity.java
+++ b/fragment/java/android/support/v4/app/FragmentActivity.java
@@ -113,8 +113,8 @@
boolean mCreated;
boolean mResumed;
- boolean mStopped;
- boolean mReallyStopped;
+ boolean mStopped = true;
+ boolean mReallyStopped = true;
boolean mRetaining;
boolean mOptionsMenuInvalidated;
diff --git a/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java b/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
index 4c0c0a4..3bac6e4 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
@@ -32,17 +32,21 @@
import android.content.Context;
import android.content.Intent;
+import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.fragment.test.R;
+import android.support.test.InstrumentationRegistry;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.app.FragmentManager.FragmentLifecycleCallbacks;
import android.support.v4.app.test.EmptyFragmentTestActivity;
+import android.support.v4.app.test.FragmentTestActivity;
import android.support.v4.view.ViewCompat;
import android.view.LayoutInflater;
import android.view.View;
@@ -57,6 +61,7 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
@MediumTest
@@ -586,6 +591,22 @@
assertFalse(fragment1.mCalledOnResume);
}
+ /**
+ * FragmentActivity should not raise the state of a Fragment while it is being destroyed.
+ */
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
+ @Test
+ public void fragmentActivityFinishEarly() throws Throwable {
+ Intent intent = new Intent(mActivityRule.getActivity(), FragmentTestActivity.class);
+ intent.putExtra("finishEarly", true);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ FragmentTestActivity activity = (FragmentTestActivity)
+ InstrumentationRegistry.getInstrumentation().startActivitySync(intent);
+
+ assertTrue(activity.onDestroyLatch.await(1000, TimeUnit.MILLISECONDS));
+ }
+
private void assertAnimationsMatch(FragmentManager fm, int enter, int exit, int popEnter,
int popExit) {
FragmentManagerImpl fmImpl = (FragmentManagerImpl) fm;
diff --git a/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java b/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
index ba5875a..8d948ae 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
@@ -35,14 +35,17 @@
}
};
- public static void waitForExecution(final ActivityTestRule<FragmentTestActivity> rule) {
+ public static void waitForExecution(final ActivityTestRule<? extends FragmentActivity> rule) {
// Wait for two cycles. When starting a postponed transition, it will post to
// the UI thread and then the execution will be added onto the queue after that.
// The two-cycle wait makes sure fragments have the opportunity to complete both
// before returning.
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- instrumentation.runOnMainSync(DO_NOTHING);
- instrumentation.runOnMainSync(DO_NOTHING);
+ try {
+ rule.runOnUiThread(DO_NOTHING);
+ rule.runOnUiThread(DO_NOTHING);
+ } catch (Throwable throwable) {
+ throw new RuntimeException(throwable);
+ }
}
public static boolean executePendingTransactions(
diff --git a/fragment/tests/java/android/support/v4/app/test/FragmentTestActivity.java b/fragment/tests/java/android/support/v4/app/test/FragmentTestActivity.java
index 5d13fe7..ef8cd0a 100644
--- a/fragment/tests/java/android/support/v4/app/test/FragmentTestActivity.java
+++ b/fragment/tests/java/android/support/v4/app/test/FragmentTestActivity.java
@@ -15,12 +15,15 @@
*/
package android.support.v4.app.test;
+import static org.junit.Assert.assertFalse;
+
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
+import android.support.annotation.Nullable;
import android.support.fragment.test.R;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
@@ -38,10 +41,25 @@
* A simple activity used for Fragment Transitions and lifecycle event ordering
*/
public class FragmentTestActivity extends FragmentActivity {
+ public final CountDownLatch onDestroyLatch = new CountDownLatch(1);
+
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.activity_content);
+ Intent intent = getIntent();
+ if (intent != null && intent.getBooleanExtra("finishEarly", false)) {
+ finish();
+ getSupportFragmentManager().beginTransaction()
+ .add(new AssertNotDestroyed(), "not destroyed")
+ .commit();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ onDestroyLatch.countDown();
}
public static class TestFragment extends Fragment {
@@ -258,4 +276,14 @@
onActivityResultResultCode = resultCode;
}
}
+
+ public static class AssertNotDestroyed extends Fragment {
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
+ assertFalse(getActivity().isDestroyed());
+ }
+ }
+ }
}
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
index 243f016..214c815 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
@@ -22,6 +22,7 @@
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
+import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH;
import static android.support.v4.media.MediaBrowserProtocol
.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_VERSION_CURRENT;
@@ -33,6 +34,8 @@
import static android.support.v4.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
import static android.support.v4.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
import static android.support.v4.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
+import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS;
+import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_QUERY;
import static android.support.v4.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT;
@@ -325,7 +328,30 @@
}
/**
- * A class with information on a single media item for use in browsing media.
+ * Searches {@link MediaItem media items} from the connected service. Not all services may
+ * support this, and {@link SearchCallback#onError} will be called if not implemented.
+ *
+ * @param query The search query that contains keywords separated by space. Should not be an
+ * empty string.
+ * @param extras The bundle of service-specific arguments to send to the media browser service.
+ * The contents of this bundle may affect the search result.
+ * @param callback The callback to receive the search result. Must be non-null.
+ */
+ public void search(@NonNull final String query, final Bundle extras,
+ @NonNull SearchCallback callback) {
+ if (TextUtils.isEmpty(query)) {
+ throw new IllegalArgumentException("query cannot be empty");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback cannot be null");
+ }
+ mImpl.search(query, extras, callback);
+ }
+
+ /**
+ * A class with information on a single media item for use in browsing/searching media.
+ * MediaItems are application dependent so we cannot guarantee that they contain the
+ * right values.
*/
public static class MediaItem implements Parcelable {
private final int mFlags;
@@ -486,7 +512,7 @@
* Returns the media id in the {@link MediaDescriptionCompat} for this item.
* @see MediaMetadataCompat#METADATA_KEY_MEDIA_ID
*/
- public @NonNull String getMediaId() {
+ public @Nullable String getMediaId() {
return mDescription.getMediaId();
}
}
@@ -765,6 +791,32 @@
}
}
+ /**
+ * Callback for receiving the result of {@link #search}.
+ */
+ public abstract static class SearchCallback {
+ /**
+ * Called when the {@link #search} finished successfully.
+ *
+ * @param query The search query sent for the search request to the connected service.
+ * @param extras The bundle of service-specific arguments sent to the connected service.
+ * @param items The list of media items which contains the search result.
+ */
+ public void onSearchResult(@NonNull String query, Bundle extras,
+ @NonNull List<MediaItem> items) {
+ }
+
+ /**
+ * Called when an error happens while {@link #search} or the connected service doesn't
+ * support {@link #search}.
+ *
+ * @param query The search query sent for the search request to the connected service.
+ * @param extras The bundle of service-specific arguments sent to the connected service.
+ */
+ public void onError(@NonNull String query, Bundle extras) {
+ }
+ }
+
interface MediaBrowserImpl {
void connect();
void disconnect();
@@ -777,6 +829,7 @@
@NonNull SubscriptionCallback callback);
void unsubscribe(@NonNull String parentId, SubscriptionCallback callback);
void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb);
+ void search(@NonNull String query, Bundle extras, @NonNull SearchCallback callback);
}
interface MediaBrowserServiceCallbackImpl {
@@ -1070,6 +1123,34 @@
}
@Override
+ public void search(@NonNull final String query, final Bundle extras,
+ @NonNull final SearchCallback callback) {
+ if (!isConnected()) {
+ Log.i(TAG, "Not connected, unable to search.");
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onError(query, extras);
+ }
+ });
+ return;
+ }
+
+ ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler);
+ try {
+ mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger);
+ } catch (RemoteException e) {
+ Log.i(TAG, "Remote error searching items with query: " + query, e);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onError(query, extras);
+ }
+ });
+ }
+ }
+
+ @Override
public void onServiceConnected(final Messenger callback, final String root,
final MediaSessionCompat.Token session, final Bundle extra) {
// Check to make sure there hasn't been a disconnect or a different ServiceConnection.
@@ -1537,6 +1618,45 @@
}
@Override
+ public void search(@NonNull final String query, final Bundle extras,
+ @NonNull final SearchCallback callback) {
+ if (!isConnected()) {
+ Log.i(TAG, "Not connected, unable to search.");
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onError(query, extras);
+ }
+ });
+ return;
+ }
+ if (mServiceBinderWrapper == null) {
+ Log.i(TAG, "The connected service doesn't support search.");
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Default framework implementation.
+ callback.onError(query, extras);
+ }
+ });
+ return;
+ }
+
+ ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler);
+ try {
+ mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger);
+ } catch (RemoteException e) {
+ Log.i(TAG, "Remote error searching items with query: " + query, e);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onError(query, extras);
+ }
+ });
+ }
+ }
+
+ @Override
public void onConnected() {
Bundle extras = MediaBrowserCompatApi21.getExtras(mBrowserObj);
if (extras == null) {
@@ -1804,6 +1924,15 @@
sendRequest(CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER, null, callbackMessenger);
}
+ void search(String query, Bundle extras, ResultReceiver receiver,
+ Messenger callbacksMessenger) throws RemoteException {
+ Bundle data = new Bundle();
+ data.putString(DATA_SEARCH_QUERY, query);
+ data.putBundle(DATA_SEARCH_EXTRAS, extras);
+ data.putParcelable(DATA_RESULT_RECEIVER, receiver);
+ sendRequest(CLIENT_MSG_SEARCH, data, callbacksMessenger);
+ }
+
private void sendRequest(int what, Bundle data, Messenger cbMessenger)
throws RemoteException {
Message msg = Message.obtain();
@@ -1830,7 +1959,7 @@
if (resultData != null) {
resultData.setClassLoader(MediaBrowserCompat.class.getClassLoader());
}
- if (resultCode != 0 || resultData == null
+ if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null
|| !resultData.containsKey(MediaBrowserServiceCompat.KEY_MEDIA_ITEM)) {
mCallback.onError(mMediaId);
return;
@@ -1843,4 +1972,37 @@
}
}
}
+
+ private static class SearchResultReceiver extends ResultReceiver {
+ private final String mQuery;
+ private final Bundle mExtras;
+ private final SearchCallback mCallback;
+
+ SearchResultReceiver(String query, Bundle extras, SearchCallback callback,
+ Handler handler) {
+ super(handler);
+ mQuery = query;
+ mExtras = extras;
+ mCallback = callback;
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null
+ || !resultData.containsKey(MediaBrowserServiceCompat.KEY_SEARCH_RESULTS)) {
+ mCallback.onError(mQuery, mExtras);
+ return;
+ }
+ Parcelable[] items = resultData.getParcelableArray(
+ MediaBrowserServiceCompat.KEY_SEARCH_RESULTS);
+ List<MediaItem> results = null;
+ if (items != null) {
+ results = new ArrayList<>();
+ for (Parcelable item : items) {
+ results.add((MediaItem) item);
+ }
+ }
+ mCallback.onSearchResult(mQuery, mExtras, results);
+ }
+ }
}
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java b/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java
index d2d12a7..2401d09 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java
@@ -29,6 +29,8 @@
public static final String DATA_PACKAGE_NAME = "data_package_name";
public static final String DATA_RESULT_RECEIVER = "data_result_receiver";
public static final String DATA_ROOT_HINTS = "data_root_hints";
+ public static final String DATA_SEARCH_EXTRAS = "data_search_extras";
+ public static final String DATA_SEARCH_QUERY = "data_search_query";
public static final String EXTRA_CLIENT_VERSION = "extra_client_version";
public static final String EXTRA_SERVICE_VERSION = "extra_service_version";
@@ -156,4 +158,16 @@
* - replyTo : Callback messenger
*/
public static final int CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER = 7;
+
+ /** (client v1)
+ * Sent to retrieve a specific media item from the connected service.
+ * - arg1 : The client version
+ * - data
+ * DATA_SEARCH_QUERY : A string for search query that contains keywords separated by space.
+ * DATA_SEARCH_EXTRAS : A bundle of service-specific arguments to send to the media browser
+ * service.
+ * DATA_RESULT_RECEIVER : Result receiver to get the result
+ * - replyTo : Callback messenger
+ */
+ public static final int CLIENT_MSG_SEARCH = 8;
}
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
index 22c3d55..9c65ce6 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
@@ -23,6 +23,7 @@
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
+import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN;
import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLING_UID;
@@ -33,6 +34,8 @@
import static android.support.v4.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
import static android.support.v4.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
import static android.support.v4.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
+import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS;
+import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_QUERY;
import static android.support.v4.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION;
@@ -116,14 +119,26 @@
@RestrictTo(LIBRARY_GROUP)
public static final String KEY_MEDIA_ITEM = "media_item";
- static final int RESULT_FLAG_OPTION_NOT_HANDLED = 0x00000001;
- static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 0x00000002;
+ /**
+ * A key for passing the list of MediaItems to the ResultReceiver in search.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final String KEY_SEARCH_RESULTS = "search_results";
+
+ static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0;
+ static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1;
+ static final int RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED = 1 << 2;
+
+ static final int RESULT_ERROR = -1;
+ static final int RESULT_OK = 0;
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED,
- RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED })
+ RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED, RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED })
private @interface ResultFlags { }
final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>();
@@ -454,6 +469,12 @@
case CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER:
mServiceBinderImpl.unregisterCallbacks(new ServiceCallbacksCompat(msg.replyTo));
break;
+ case CLIENT_MSG_SEARCH:
+ mServiceBinderImpl.search(data.getString(DATA_SEARCH_QUERY),
+ data.getBundle(DATA_SEARCH_EXTRAS),
+ (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER),
+ new ServiceCallbacksCompat(msg.replyTo));
+ break;
default:
Log.w(TAG, "Unhandled message: " + msg
+ "\n Service version: " + SERVICE_VERSION_CURRENT
@@ -720,6 +741,27 @@
}
});
}
+
+ public void search(final String query, final Bundle extras, final ResultReceiver receiver,
+ final ServiceCallbacks callbacks) {
+ if (TextUtils.isEmpty(query) || receiver == null) {
+ return;
+ }
+
+ mHandler.postOrRun(new Runnable() {
+ @Override
+ public void run() {
+ final IBinder b = callbacks.asBinder();
+
+ ConnectionRecord connection = mConnections.get(b);
+ if (connection == null) {
+ Log.w(TAG, "search for callback that isn't registered query=" + query);
+ return;
+ }
+ performSearch(query, extras, connection, receiver);
+ }
+ });
+ }
}
private interface ServiceCallbacks {
@@ -829,7 +871,6 @@
* @see BrowserRoot#EXTRA_RECENT
* @see BrowserRoot#EXTRA_OFFLINE
* @see BrowserRoot#EXTRA_SUGGESTED
- * @see BrowserRoot#EXTRA_SUGGESTION_KEYWORDS
*/
public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
int clientUid, @Nullable Bundle rootHints);
@@ -907,12 +948,39 @@
* @param result The Result to send the item to, or null if the id is
* invalid.
*/
- public void onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> result) {
+ public void onLoadItem(String itemId, @NonNull Result<MediaBrowserCompat.MediaItem> result) {
result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED);
result.sendResult(null);
}
/**
+ * Called to get the search result.
+ * <p>
+ * Implementations must call {@link Result#sendResult result.sendResult}. If the search will be
+ * an expensive operation {@link Result#detach result.detach} may be called before returning
+ * from this function, and then {@link Result#sendResult result.sendResult} called when the
+ * search has been completed.
+ * </p><p>
+ * In case there are no search results, call {@link Result#sendResult result.sendResult} with an
+ * empty list. In case there are some errors happened, call {@link Result#sendResult
+ * result.sendResult} with {@code null}, which will invoke {@link
+ * MediaBrowserCompat.SearchCallback#onError}.
+ * </p><p>
+ * The default implementation will invoke {@link MediaBrowserCompat.SearchCallback#onError}.
+ * </p>
+ *
+ * @param query The search query sent from the media browser. It contains keywords separated
+ * by space.
+ * @param extras The bundle of service-specific arguments sent from the media browser.
+ * @param result The {@link Result} to send the search result.
+ */
+ public void onSearch(@NonNull String query, Bundle extras,
+ @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
+ result.setFlags(RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED);
+ result.sendResult(null);
+ }
+
+ /**
* Call to set the media session.
* <p>
* This should be called as soon as possible during the service's startup.
@@ -953,7 +1021,6 @@
* @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT
* @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE
* @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED
- * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTION_KEYWORDS
*/
public final Bundle getBrowserRootHints() {
return mImpl.getBrowserRootHints();
@@ -1133,12 +1200,12 @@
@Override
void onResultSent(MediaBrowserCompat.MediaItem item, @ResultFlags int flags) {
if ((flags & RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED) != 0) {
- receiver.send(-1, null);
+ receiver.send(RESULT_ERROR, null);
return;
}
Bundle bundle = new Bundle();
bundle.putParcelable(KEY_MEDIA_ITEM, item);
- receiver.send(0, bundle);
+ receiver.send(RESULT_OK, bundle);
}
};
@@ -1152,6 +1219,34 @@
}
}
+ void performSearch(final String query, Bundle extras, ConnectionRecord connection,
+ final ResultReceiver receiver) {
+ final Result<List<MediaBrowserCompat.MediaItem>> result =
+ new Result<List<MediaBrowserCompat.MediaItem>>(query) {
+ @Override
+ void onResultSent(List<MediaBrowserCompat.MediaItem> items, @ResultFlags int flag) {
+ if ((flag & RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED) != 0
+ || items == null) {
+ receiver.send(RESULT_ERROR, null);
+ return;
+ }
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArray(KEY_SEARCH_RESULTS,
+ items.toArray(new MediaBrowserCompat.MediaItem[0]));
+ receiver.send(RESULT_OK, bundle);
+ }
+ };
+
+ mCurConnection = connection;
+ onSearch(query, extras, result);
+ mCurConnection = null;
+
+ if (!result.isDone()) {
+ throw new IllegalStateException("onSearch must call detach() or sendResult()"
+ + " before returning for query=" + query);
+ }
+ }
+
/**
* Contains information that the browser service needs to send to the client
* when first connected.
@@ -1170,7 +1265,6 @@
*
* @see #EXTRA_OFFLINE
* @see #EXTRA_SUGGESTED
- * @see #EXTRA_SUGGESTION_KEYWORDS
*/
public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
@@ -1188,7 +1282,6 @@
*
* @see #EXTRA_RECENT
* @see #EXTRA_SUGGESTED
- * @see #EXTRA_SUGGESTION_KEYWORDS
*/
public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
@@ -1207,7 +1300,6 @@
*
* @see #EXTRA_RECENT
* @see #EXTRA_OFFLINE
- * @see #EXTRA_SUGGESTION_KEYWORDS
*/
public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
@@ -1228,7 +1320,10 @@
* @see #EXTRA_RECENT
* @see #EXTRA_OFFLINE
* @see #EXTRA_SUGGESTED
+ * @deprecated Use {@link MediaBrowserCompat#search(String, Bundle,
+ * MediaBrowserCompat.SearchCallback)} instead.
*/
+ @Deprecated
public static final String EXTRA_SUGGESTION_KEYWORDS
= "android.service.media.extra.SUGGESTION_KEYWORDS";
diff --git a/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java b/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
index 85c64ea..7e436ec 100644
--- a/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
@@ -20,12 +20,15 @@
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import android.content.ComponentName;
import android.os.Bundle;
+import android.support.test.filters.LargeTest;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
import org.junit.Before;
import org.junit.Test;
@@ -49,6 +52,7 @@
private final ConnectionCallback mConnectionCallback = new ConnectionCallback();
private final SubscriptionCallback mSubscriptionCallback = new SubscriptionCallback();
private final ItemCallback mItemCallback = new ItemCallback();
+ private final SearchCallback mSearchCallback = new SearchCallback();
private MediaBrowserCompat mMediaBrowser;
private StubMediaBrowserServiceCompat mMediaBrowserService;
@@ -122,7 +126,7 @@
}
@Test
- @SmallTest
+ @LargeTest
public void testDelayedNotifyChildrenChanged() throws Exception {
synchronized (mWaitLock) {
mSubscriptionCallback.reset();
@@ -167,6 +171,47 @@
@Test
@SmallTest
+ public void testSearch() throws Exception {
+ final String key = "test-key";
+ final String val = "test-val";
+
+ synchronized (mWaitLock) {
+ mSearchCallback.reset();
+ mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY_FOR_NO_RESULT, null,
+ mSearchCallback);
+ mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertTrue(mSearchCallback.mOnSearchResult);
+ assertTrue(mSearchCallback.mSearchResults != null
+ && mSearchCallback.mSearchResults.size() == 0);
+ assertEquals(null, mSearchCallback.mSearchExtras);
+
+ mSearchCallback.reset();
+ mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY_FOR_ERROR, null,
+ mSearchCallback);
+ mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertTrue(mSearchCallback.mOnSearchResult);
+ assertNull(mSearchCallback.mSearchResults);
+ assertEquals(null, mSearchCallback.mSearchExtras);
+
+ mSearchCallback.reset();
+ Bundle extras = new Bundle();
+ extras.putString(key, val);
+ mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY, extras,
+ mSearchCallback);
+ mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertTrue(mSearchCallback.mOnSearchResult);
+ assertNotNull(mSearchCallback.mSearchResults);
+ for (MediaItem item : mSearchCallback.mSearchResults) {
+ assertNotNull(item.getMediaId());
+ assertTrue(item.getMediaId().contains(StubMediaBrowserServiceCompat.SEARCH_QUERY));
+ }
+ assertNotNull(mSearchCallback.mSearchExtras);
+ assertEquals(val, mSearchCallback.mSearchExtras.getString(key));
+ }
+ }
+
+ @Test
+ @SmallTest
public void testBrowserRoot() {
final String id = "test-id";
final String key = "test-key";
@@ -180,7 +225,7 @@
assertEquals(val, browserRoot.getExtras().getString(key));
}
- private void assertRootHints(MediaBrowserCompat.MediaItem item) {
+ private void assertRootHints(MediaItem item) {
Bundle rootHints = item.getDescription().getExtras();
assertNotNull(rootHints);
assertEquals(mRootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT),
@@ -206,11 +251,11 @@
boolean mOnChildrenLoadedWithOptions;
@Override
- public void onChildrenLoaded(String parentId, List<MediaBrowserCompat.MediaItem> children) {
+ public void onChildrenLoaded(String parentId, List<MediaItem> children) {
synchronized (mWaitLock) {
mOnChildrenLoaded = true;
if (children != null) {
- for (MediaBrowserCompat.MediaItem item : children) {
+ for (MediaItem item : children) {
assertRootHints(item);
}
}
@@ -219,12 +264,11 @@
}
@Override
- public void onChildrenLoaded(String parentId, List<MediaBrowserCompat.MediaItem> children,
- Bundle options) {
+ public void onChildrenLoaded(String parentId, List<MediaItem> children, Bundle options) {
synchronized (mWaitLock) {
mOnChildrenLoadedWithOptions = true;
if (children != null) {
- for (MediaBrowserCompat.MediaItem item : children) {
+ for (MediaItem item : children) {
assertRootHints(item);
}
}
@@ -242,7 +286,7 @@
boolean mOnItemLoaded;
@Override
- public void onItemLoaded(MediaBrowserCompat.MediaItem item) {
+ public void onItemLoaded(MediaItem item) {
synchronized (mWaitLock) {
mOnItemLoaded = true;
assertRootHints(item);
@@ -254,4 +298,36 @@
mOnItemLoaded = false;
}
}
+
+ private class SearchCallback extends MediaBrowserCompat.SearchCallback {
+ boolean mOnSearchResult;
+ Bundle mSearchExtras;
+ List<MediaItem> mSearchResults;
+
+ @Override
+ public void onSearchResult(String query, Bundle extras, List<MediaItem> items) {
+ synchronized (mWaitLock) {
+ mOnSearchResult = true;
+ mSearchResults = items;
+ mSearchExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onError(String query, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnSearchResult = true;
+ mSearchResults = null;
+ mSearchExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ public void reset() {
+ mOnSearchResult = false;
+ mSearchExtras = null;
+ mSearchResults = null;
+ }
+ }
}
diff --git a/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java b/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java
index 95b4a2b..3995200 100644
--- a/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java
+++ b/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java
@@ -43,6 +43,10 @@
MEDIA_ID_CHILDREN_DELAYED
};
+ static final String SEARCH_QUERY = "test_media_children";
+ static final String SEARCH_QUERY_FOR_NO_RESULT = "query no result";
+ static final String SEARCH_QUERY_FOR_ERROR = "query for error";
+
static StubMediaBrowserServiceCompat sInstance;
/* package private */ static MediaSessionCompat sSession;
@@ -72,8 +76,7 @@
if (MEDIA_ID_ROOT.equals(parentMediaId)) {
Bundle rootHints = getBrowserRootHints();
for (String id : MEDIA_ID_CHILDREN) {
- mediaItems.add(new MediaItem(new MediaDescriptionCompat.Builder()
- .setMediaId(id).setExtras(rootHints).build(), MediaItem.FLAG_BROWSABLE));
+ mediaItems.add(createMediaItem(id));
}
result.sendResult(mediaItems);
} else if (MEDIA_ID_CHILDREN_DELAYED.equals(parentMediaId)) {
@@ -97,9 +100,7 @@
for (String id : MEDIA_ID_CHILDREN) {
if (id.equals(itemId)) {
- result.sendResult(new MediaItem(new MediaDescriptionCompat.Builder()
- .setMediaId(id).setExtras(getBrowserRootHints()).build(),
- MediaItem.FLAG_BROWSABLE));
+ result.sendResult(createMediaItem(id));
return;
}
}
@@ -107,6 +108,23 @@
super.onLoadItem(itemId, result);
}
+ @Override
+ public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) {
+ if (SEARCH_QUERY_FOR_NO_RESULT.equals(query)) {
+ result.sendResult(Collections.<MediaItem>emptyList());
+ } else if (SEARCH_QUERY_FOR_ERROR.equals(query)) {
+ result.sendResult(null);
+ } else if (SEARCH_QUERY.equals(query)) {
+ List<MediaItem> items = new ArrayList<>();
+ for (String id : MEDIA_ID_CHILDREN) {
+ if (id.contains(query)) {
+ items.add(createMediaItem(id));
+ }
+ }
+ result.sendResult(items);
+ }
+ }
+
public void sendDelayedNotifyChildrenChanged() {
if (mPendingLoadChildrenResult != null) {
mPendingLoadChildrenResult.sendResult(Collections.<MediaItem>emptyList());
@@ -124,4 +142,10 @@
mPendingLoadItemResult = null;
}
}
+
+ private MediaItem createMediaItem(String id) {
+ return new MediaItem(new MediaDescriptionCompat.Builder()
+ .setMediaId(id).setExtras(getBrowserRootHints()).build(),
+ MediaItem.FLAG_BROWSABLE);
+ }
}
diff --git a/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java b/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java
index 81ff253..bbfc461 100644
--- a/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java
+++ b/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java
@@ -19,8 +19,6 @@
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import static org.junit.Assume.assumeTrue;
-
import android.os.Build;
import android.support.percent.test.R;
import android.support.test.filters.SmallTest;
@@ -108,8 +106,6 @@
@Before
public void setUp() throws Exception {
- assumeTrue(Build.VERSION.SDK_INT != 17);
-
final TestRelativeRtlActivity activity = mActivityTestRule.getActivity();
mPercentRelativeLayout = (PercentRelativeLayout) activity.findViewById(R.id.container);
mContainerWidth = mPercentRelativeLayout.getWidth();
@@ -132,6 +128,9 @@
@Test
public void testTopChild() {
+ if (Build.VERSION.SDK_INT == 17) {
+ return;
+ }
final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_top);
if (Build.VERSION.SDK_INT >= 17) {
@@ -161,6 +160,9 @@
@Test
public void testStartChild() {
+ if (Build.VERSION.SDK_INT == 17) {
+ return;
+ }
final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_start);
if (Build.VERSION.SDK_INT >= 17) {
@@ -191,6 +193,9 @@
@Test
public void testBottomChild() {
+ if (Build.VERSION.SDK_INT == 17) {
+ return;
+ }
final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_bottom);
if (Build.VERSION.SDK_INT >= 17) {
@@ -222,6 +227,9 @@
@Test
public void testEndChild() {
+ if (Build.VERSION.SDK_INT == 17) {
+ return;
+ }
final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_end);
if (Build.VERSION.SDK_INT >= 17) {
@@ -252,6 +260,9 @@
@Test
public void testCenterChild() {
+ if (Build.VERSION.SDK_INT == 17) {
+ return;
+ }
final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_center);
boolean supportsRtl = Build.VERSION.SDK_INT >= 17;
diff --git a/samples/SupportLeanbackDemos/res/raw/browse.mp4 b/samples/SupportLeanbackDemos/res/raw/browse.mp4
index b841a48..3f709fb 100644
--- a/samples/SupportLeanbackDemos/res/raw/browse.mp4
+++ b/samples/SupportLeanbackDemos/res/raw/browse.mp4
Binary files differ
diff --git a/v17/leanback/res/layout/lb_row_header.xml b/v17/leanback/res/layout/lb_row_header.xml
index 8962e9a..2729ae9 100644
--- a/v17/leanback/res/layout/lb_row_header.xml
+++ b/v17/leanback/res/layout/lb_row_header.xml
@@ -15,14 +15,13 @@
limitations under the License.
-->
-<android:LinearLayout
+<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android">
<android.support.v17.leanback.widget.RowHeaderView
- xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/row_header"
android:importantForAccessibility="no"
android:layout_width="wrap_content"
@@ -30,11 +29,10 @@
style="?rowHeaderStyle"/>
<TextView
- xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/row_header_description"
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?rowHeaderDescriptionStyle" />
-</android:LinearLayout>
+</LinearLayout>
diff --git a/v17/leanback/res/values/attrs.xml b/v17/leanback/res/values/attrs.xml
index 870d958..3e2d7f2 100644
--- a/v17/leanback/res/values/attrs.xml
+++ b/v17/leanback/res/values/attrs.xml
@@ -522,6 +522,14 @@
<attr name="guidedActionContentWidthNoIcon" format="reference" />
</declare-styleable>
+ <declare-styleable name="lbTimePicker">
+ <!-- attr indicating whether time is in 24 hour format (true) or AM/PM format (false). -->
+ <attr name="is24HourFormat" format="boolean" />
+ <!-- attr indicating whether time fields should be initially set to the current time.
+ By default, it's true i.e. TimePicker initializes fields with the current time. -->
+ <attr name="useCurrentTime" format="boolean" />
+ </declare-styleable>
+
<declare-styleable name="lbDatePicker">
<attr name="android:minDate" />
<attr name="android:maxDate" />
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
index 72072a2..37a6bfc 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
@@ -37,7 +37,9 @@
*/
final class DetailsBackgroundVideoHelper {
private static final long BACKGROUND_CROSS_FADE_DURATION = 500;
- private static final long CROSSFADE_DELAY = 0;
+ // Temporarily add CROSSFADE_DELAY waiting for video surface ready.
+ // We will remove this delay once PlaybackGlue have a callback for videoRenderingReady event.
+ private static final long CROSSFADE_DELAY = 1000;
/**
* Different states {@link DetailsFragment} can be in.
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
index ac11fde..8cced05 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
@@ -521,7 +521,7 @@
@CallSuper
void onSafeStart() {
if (mDetailsBackgroundController != null) {
- mDetailsBackgroundController.enablePlaybackHost();
+ mDetailsBackgroundController.onStart();
}
}
@@ -541,6 +541,14 @@
}
}
+ @Override
+ public void onStop() {
+ if (mDetailsBackgroundController != null) {
+ mDetailsBackgroundController.onStop();
+ }
+ super.onStop();
+ }
+
/**
* Called on every visible row to change view status when current selected row position
* or selected sub position changed. Subclass may override. The default
@@ -703,7 +711,6 @@
if (direction == View.FOCUS_DOWN) {
if (mRowsFragment.getVerticalGridView() != null) {
showTitle(true);
- slideInGridView();
return mRowsFragment.getVerticalGridView();
}
}
@@ -730,7 +737,6 @@
&& mVideoFragment.getView().hasFocus()) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
showTitle(true);
- slideInGridView();
getVerticalGridView().requestFocus();
return true;
}
@@ -748,10 +754,4 @@
getVerticalGridView().animateOut();
}
- /**
- * Slides in vertical grid view (displaying media item details) from below.
- */
- void slideInGridView() {
- getVerticalGridView().animateIn();
- }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
index a0bf00c..ef09ae6 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
@@ -228,16 +228,25 @@
}
/**
- * Enable Host for PlaybackGlue. This is delayed until: onStart() is called,
- * activity transitions are finished.
+ * When fragment is started and no running transition. First set host if not yet set, second
+ * start playing if it was paused before.
*/
- void enablePlaybackHost() {
+ void onStart() {
if (!mCanUseHost) {
mCanUseHost = true;
if (mPlaybackGlue != null) {
mPlaybackGlue.setHost(onCreateGlueHost());
}
}
+ if (mPlaybackGlue != null && mPlaybackGlue.isReadyForPlayback()) {
+ mPlaybackGlue.play();
+ }
+ }
+
+ void onStop() {
+ if (mPlaybackGlue != null) {
+ mPlaybackGlue.pause();
+ }
}
/**
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
index 53300ac..9c0d9a5 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
@@ -524,7 +524,7 @@
@CallSuper
void onSafeStart() {
if (mDetailsBackgroundController != null) {
- mDetailsBackgroundController.enablePlaybackHost();
+ mDetailsBackgroundController.onStart();
}
}
@@ -544,6 +544,14 @@
}
}
+ @Override
+ public void onStop() {
+ if (mDetailsBackgroundController != null) {
+ mDetailsBackgroundController.onStop();
+ }
+ super.onStop();
+ }
+
/**
* Called on every visible row to change view status when current selected row position
* or selected sub position changed. Subclass may override. The default
@@ -706,7 +714,6 @@
if (direction == View.FOCUS_DOWN) {
if (mRowsSupportFragment.getVerticalGridView() != null) {
showTitle(true);
- slideInGridView();
return mRowsSupportFragment.getVerticalGridView();
}
}
@@ -733,7 +740,6 @@
&& mVideoSupportFragment.getView().hasFocus()) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
showTitle(true);
- slideInGridView();
getVerticalGridView().requestFocus();
return true;
}
@@ -751,10 +757,4 @@
getVerticalGridView().animateOut();
}
- /**
- * Slides in vertical grid view (displaying media item details) from below.
- */
- void slideInGridView() {
- getVerticalGridView().animateIn();
- }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
index 5ab42c3..071a04a 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
@@ -231,16 +231,25 @@
}
/**
- * Enable Host for PlaybackGlue. This is delayed until: onStart() is called,
- * activity transitions are finished.
+ * When fragment is started and no running transition. First set host if not yet set, second
+ * start playing if it was paused before.
*/
- void enablePlaybackHost() {
+ void onStart() {
if (!mCanUseHost) {
mCanUseHost = true;
if (mPlaybackGlue != null) {
mPlaybackGlue.setHost(onCreateGlueHost());
}
}
+ if (mPlaybackGlue != null && mPlaybackGlue.isReadyForPlayback()) {
+ mPlaybackGlue.play();
+ }
+ }
+
+ void onStop() {
+ if (mPlaybackGlue != null) {
+ mPlaybackGlue.pause();
+ }
}
/**
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
index 1058ab1..8d6367a 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
@@ -835,6 +835,14 @@
super.onDestroyView();
}
+ @Override
+ public void onDestroy() {
+ if (mHostCallback != null) {
+ mHostCallback.onHostDestroy();
+ }
+ super.onDestroy();
+ }
+
/**
* Sets the playback row for the playback controls.
*/
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlayFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlayFragment.java
index 3b8cfd3..c601b2e 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlayFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlayFragment.java
@@ -787,6 +787,9 @@
@Override
public void onDestroyView() {
mRootView = null;
+ if (mHostCallback != null) {
+ mHostCallback.onHostDestroy();
+ }
super.onDestroyView();
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java
index b58a2ad..b4df936 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java
@@ -790,6 +790,9 @@
@Override
public void onDestroyView() {
mRootView = null;
+ if (mHostCallback != null) {
+ mHostCallback.onHostDestroy();
+ }
super.onDestroyView();
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
index b812004..3133264 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
@@ -838,6 +838,14 @@
super.onDestroyView();
}
+ @Override
+ public void onDestroy() {
+ if (mHostCallback != null) {
+ mHostCallback.onHostDestroy();
+ }
+ super.onDestroy();
+ }
+
/**
* Sets the playback row for the playback controls.
*/
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
index 2cf42aa..150e461 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
@@ -89,4 +89,11 @@
public SurfaceView getSurfaceView() {
return mVideoSurface;
}
+
+ @Override
+ public void onDestroyView() {
+ mVideoSurface = null;
+ mState = SURFACE_NOT_CREATED;
+ super.onDestroyView();
+ }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
index 7a811e4..c12b06f 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
@@ -92,4 +92,11 @@
public SurfaceView getSurfaceView() {
return mVideoSurface;
}
+
+ @Override
+ public void onDestroyView() {
+ mVideoSurface = null;
+ mState = SURFACE_NOT_CREATED;
+ super.onDestroyView();
+ }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java b/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
index abee2dc..b0f0b8a 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
@@ -29,7 +29,6 @@
import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.Row;
import android.support.v17.leanback.widget.RowPresenter;
-import android.util.Log;
import android.view.KeyEvent;
import android.view.SurfaceHolder;
import android.view.View;
@@ -159,14 +158,18 @@
* Release internal MediaPlayer. Should not use the object after call release().
*/
public void release() {
+ mInitialized = false;
mPlayer.release();
}
@Override
protected void onDetachedFromHost() {
- super.onDetachedFromHost();
+ if (getHost() instanceof SurfaceHolderGlueHost) {
+ ((SurfaceHolderGlueHost) getHost()).setSurfaceHolderCallback(null);
+ }
reset();
release();
+ super.onDetachedFromHost();
}
@Override
@@ -185,18 +188,19 @@
@Override
public void enableProgressUpdating(final boolean enabled) {
+ if (mRunnable != null) mHandler.removeCallbacks(mRunnable);
if (!enabled) {
- if (mRunnable != null) mHandler.removeCallbacks(mRunnable);
return;
}
- mRunnable = new Runnable() {
- @Override
- public void run() {
- updateProgress();
- Log.d(TAG, "enableProgressUpdating(boolean)");
- mHandler.postDelayed(this, getUpdatePeriod());
- }
- };
+ if (mRunnable == null) {
+ mRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateProgress();
+ mHandler.postDelayed(this, getUpdatePeriod());
+ }
+ };
+ }
mHandler.postDelayed(mRunnable, getUpdatePeriod());
}
@@ -262,7 +266,7 @@
@Override
public boolean isMediaPlaying() {
- return mPlayer.isPlaying();
+ return mInitialized && mPlayer.isPlaying();
}
@Override
@@ -295,7 +299,7 @@
@Override
public int getCurrentSpeedId() {
// 0 = Pause, 1 = Normal Playback Speed
- return mPlayer.isPlaying() ? 1 : 0;
+ return isMediaPlaying() ? 1 : 0;
}
@Override
@@ -305,6 +309,9 @@
@Override
public void play(int speed) {
+ if (!mInitialized || mPlayer.isPlaying()) {
+ return;
+ }
mPlayer.start();
onMetadataChanged();
onStateChanged();
@@ -313,8 +320,9 @@
@Override
public void pause() {
- if (mPlayer.isPlaying()) {
+ if (isMediaPlaying()) {
mPlayer.pause();
+ onStateChanged();
}
}
@@ -359,6 +367,9 @@
* @param newPosition The new position of the media track in milliseconds.
*/
protected void seekTo(int newPosition) {
+ if (!mInitialized) {
+ return;
+ }
mPlayer.seekTo(newPosition);
}
@@ -379,6 +390,7 @@
prepareMediaForPlaying();
} else {
mMediaSourceUri = uri;
+ prepareMediaForPlaying();
}
return true;
}
@@ -400,6 +412,7 @@
prepareMediaForPlaying();
} else {
mMediaSourcePath = path;
+ prepareMediaForPlaying();
}
return true;
}
@@ -436,6 +449,9 @@
mPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {
+ if (getControlsRow() == null) {
+ return;
+ }
getControlsRow().setBufferedProgress((int) (mp.getDuration() * (percent / 100f)));
}
});
@@ -475,15 +491,9 @@
* {@link PlaybackGlueHost}.
*/
class VideoPlayerSurfaceHolderCallback implements SurfaceHolder.Callback {
- private boolean mMediaPlayerReset = true;
-
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
- if (mMediaPlayerReset) {
- mMediaPlayerReset = false;
- setDisplay(surfaceHolder);
- prepareMediaForPlaying();
- }
+ setDisplay(surfaceHolder);
}
@Override
@@ -492,9 +502,7 @@
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
- reset();
setDisplay(null);
- mMediaPlayerReset = true;
}
}
}
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java b/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
index bdd67fb..db209c0 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
@@ -254,6 +254,12 @@
enableProgressUpdating(false);
}
+ @Override
+ protected void onDetachedFromHost() {
+ enableProgressUpdating(false);
+ super.onDetachedFromHost();
+ }
+
/**
* Instantiating a {@link PlaybackControlsRow} and corresponding
* {@link PlaybackControlsRowPresenter}. Subclass may override.
@@ -393,7 +399,9 @@
public void updateProgress() {
int position = getCurrentPosition();
if (DEBUG) Log.v(TAG, "updateProgress " + position);
- mControlsRow.setCurrentTime(position);
+ if (mControlsRow != null) {
+ mControlsRow.setCurrentTime(position);
+ }
}
/**
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java b/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
index 8ed4b93..3f55da3 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
@@ -21,10 +21,19 @@
/**
* Base class for abstraction of media play/pause feature. A subclass of PlaybackGlue will contain
- * implementation of Media Player. App initializes PlaybackGlue subclass, associated it with a
- * {@link PlaybackGlueHost}. {@link PlaybackGlueHost} is typically implemented by a Fragment or
- * an Activity, it provides the environment to render UI for PlaybackGlue object, it optionally
- * provides SurfaceHolder via {@link SurfaceHolderGlueHost} to render video.
+ * implementation of Media Player or a connection to playback Service. App initializes
+ * PlaybackGlue subclass, associated it with a {@link PlaybackGlueHost}. {@link PlaybackGlueHost}
+ * is typically implemented by a Fragment or an Activity, it provides the environment to render UI
+ * for PlaybackGlue object, it optionally provides SurfaceHolder via {@link SurfaceHolderGlueHost}
+ * to render video. A typical PlaybackGlue should release resources (e.g. MediaPlayer or connection
+ * to playback Service) in {@link #onDetachedFromHost()}.
+ * {@link #onDetachedFromHost()} is called in two cases:
+ * <ul>
+ * <li> app manually change it using {@link #setHost(PlaybackGlueHost)} call</li>
+ * <li> When host (fragment or activity) is destroyed </li>
+ * </ul>
+ * In rare case if an PlaybackGlue wants to live outside fragment / activity life cycle, it may
+ * manages resource release by itself.
*
* @see PlaybackGlueHost
*/
@@ -58,7 +67,10 @@
/**
* Returns true when the media player is ready to start media playback. Subclasses must
- * implement this method correctly.
+ * implement this method correctly. When returning false, app may listen to
+ * {@link PlayerCallback#onReadyForPlayback()} event.
+ *
+ * @see PlayerCallback#onReadyForPlayback()
*/
public boolean isReadyForPlayback() {
return true;
@@ -95,7 +107,10 @@
}
/**
- * This method is used to configure the {@link PlaybackGlueHost} with required listeners.
+ * This method is used to associate a PlaybackGlue with the {@link PlaybackGlueHost} which
+ * provides UI and optional {@link SurfaceHolderGlueHost}.
+ *
+ * @param host The host for the PlaybackGlue. Set to null to detach from the host.
*/
public final void setHost(PlaybackGlueHost host) {
if (mPlaybackGlueHost == host) {
@@ -161,13 +176,21 @@
public void onHostPause() {
PlaybackGlue.this.onHostPause();
}
+
+ @Override
+ public void onHostDestroy() {
+ if (mPlaybackGlueHost != null) {
+ mPlaybackGlueHost.attachToGlue(null);
+ }
+ }
});
}
/**
* This method is called when current associated {@link PlaybackGlueHost} is attached to a
- * different {@link PlaybackGlue}. Subclass may override and call super.onDetachedFromHost()
- * at last.
+ * different {@link PlaybackGlue} or {@link PlaybackGlueHost} is destroyed . Subclass may
+ * override and call super.onDetachedFromHost() at last. A typical PlaybackGlue will release
+ * resources (e.g. MediaPlayer or connection to playback service) in this method.
*/
@CallSuper
protected void onDetachedFromHost() {
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java b/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
index 3cf086b..799074c 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
@@ -72,6 +72,12 @@
*/
public void onHostResume() {
}
+
+ /**
+ * Callback triggered once the host(fragment) has been destroyed.
+ */
+ public void onHostDestroy() {
+ }
}
/**
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java b/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
index 86265fb..005a441 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
@@ -27,7 +27,6 @@
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
-import android.view.animation.AccelerateDecelerateInterpolator;
/**
* An abstract base class for vertically and horizontally scrolling lists. The items come
@@ -1003,22 +1002,21 @@
return mLayoutManager.getExtraLayoutSpace();
}
-
+ /**
+ * Temporarily slide out child views to bottom (for VerticalGridView) or end
+ * (for HorizontalGridView). The views will be automatically slide-in in next
+ * {@link #smoothScrollToPosition(int)} or {@link #scrollToPosition(int)}.
+ */
public void animateOut() {
- ((GridLayoutManager) getLayoutManager()).setIsSlidingChildViews(true);
- smoothScrollBy(0, -600, new AccelerateDecelerateInterpolator());
+ mLayoutManager.slideOut();
}
+ /**
+ * @deprecated No longer needed. Children being slide out by {@link #animateOut()} will be
+ * slide in next focus or (smooth)scrollToPosition action.
+ */
+ @Deprecated
public void animateIn() {
- addOnScrollListener(new OnScrollListener() {
- @Override
- public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
- if (newState == RecyclerView.SCROLL_STATE_IDLE) {
- ((GridLayoutManager) getLayoutManager()).setIsSlidingChildViews(false);
- }
- }
- });
- smoothScrollBy(0, 600, new AccelerateDecelerateInterpolator());
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
index a3ac029..5569836 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -42,6 +42,7 @@
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
+import android.view.animation.AccelerateDecelerateInterpolator;
import java.io.PrintWriter;
import java.io.StringWriter;
@@ -376,8 +377,8 @@
// effect smooth scrolling too over to bind an item view then drag the item view back.
final static int MIN_MS_SMOOTH_SCROLL_MAIN_SCREEN = 30;
- // Represents whether child views are sliding in or out.
- private boolean mIsSlidingChildViews;
+ // Represents whether child views are temporarily sliding out
+ boolean mIsSlidingChildViews;
String getTag() {
return TAG + ":" + mBaseGridView.getId();
@@ -1737,8 +1738,41 @@
return mGrid.appendOneColumnVisibleItems();
}
- public void setIsSlidingChildViews(boolean animatingChildViews) {
- this.mIsSlidingChildViews = animatingChildViews;
+ /**
+ * Temporarily slide out child and will be auto slide-in in next scrollToView().
+ */
+ void slideOut() {
+ if (mIsSlidingChildViews) {
+ return;
+ }
+ mIsSlidingChildViews = true;
+ if (mOrientation == VERTICAL) {
+ int distance = -getHeight();
+ int top = getChildAt(0).getTop();
+ if (top < 0) {
+ // scroll more if first child is above top edge
+ distance = distance + top;
+ }
+ mBaseGridView.smoothScrollBy(0, distance, new AccelerateDecelerateInterpolator());
+ } else {
+ int distance;
+ if (mReverseFlowPrimary) {
+ distance = getWidth();
+ int start = getChildAt(0).getRight();
+ if (start > distance) {
+ // scroll more if first child is outside right edge
+ distance = start;
+ }
+ } else {
+ distance = -getWidth();
+ int start = getChildAt(0).getLeft();
+ if (start < 0) {
+ // scroll more if first child is out side left edge
+ distance = distance + start;
+ }
+ }
+ mBaseGridView.smoothScrollBy(distance, 0, new AccelerateDecelerateInterpolator());
+ }
}
private boolean prependOneColumnVisibleItems() {
@@ -2317,6 +2351,12 @@
setSelection(position, 0, false, 0);
}
+ @Override
+ public void smoothScrollToPosition(RecyclerView recyclerView, State state,
+ int position) {
+ setSelection(position, 0, true, 0);
+ }
+
public void setSelection(int position,
int primaryScrollExtra) {
setSelection(position, 0, false, primaryScrollExtra);
@@ -2345,7 +2385,7 @@
public void setSelection(int position, int subposition, boolean smooth,
int primaryScrollExtra) {
- if (mFocusPosition != position && position != NO_POSITION
+ if (mIsSlidingChildViews || mFocusPosition != position && position != NO_POSITION
|| subposition != mSubFocusPosition || primaryScrollExtra != mPrimaryScrollExtra) {
scrollToSelection(position, subposition, smooth, primaryScrollExtra);
}
@@ -2621,6 +2661,7 @@
* Scroll to a given child view and change mFocusPosition.
*/
private void scrollToView(View view, View childView, boolean smooth) {
+ mIsSlidingChildViews = false;
int newFocusPosition = getPositionByView(view);
int newSubFocusPosition = getSubPositionByView(view, childView);
if (newFocusPosition != mFocusPosition || newSubFocusPosition != mSubFocusPosition) {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
index 31863a9..7725bf3 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
@@ -59,7 +59,7 @@
final static String DATE_FORMAT = "MM/dd/yyyy";
final DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
- PickerConstant mConstant;
+ PickerUtility.DateConstant mConstant;
Calendar mMinDate;
Calendar mMaxDate;
@@ -179,23 +179,13 @@
return mDatePickerFormat;
}
- private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
- if (oldCalendar == null) {
- return Calendar.getInstance(locale);
- } else {
- final long currentTimeMillis = oldCalendar.getTimeInMillis();
- Calendar newCalendar = Calendar.getInstance(locale);
- newCalendar.setTimeInMillis(currentTimeMillis);
- return newCalendar;
- }
- }
-
private void updateCurrentLocale() {
- mConstant = new PickerConstant(Locale.getDefault(), getContext().getResources());
- mTempDate = getCalendarForLocale(mTempDate, mConstant.locale);
- mMinDate = getCalendarForLocale(mMinDate, mConstant.locale);
- mMaxDate = getCalendarForLocale(mMaxDate, mConstant.locale);
- mCurrentDate = getCalendarForLocale(mCurrentDate, mConstant.locale);
+ mConstant = PickerUtility.getDateConstantInstance(Locale.getDefault(),
+ getContext().getResources());
+ mTempDate = PickerUtility.getCalendarForLocale(mTempDate, mConstant.locale);
+ mMinDate = PickerUtility.getCalendarForLocale(mMinDate, mConstant.locale);
+ mMaxDate = PickerUtility.getCalendarForLocale(mMaxDate, mConstant.locale);
+ mCurrentDate = PickerUtility.getCalendarForLocale(mCurrentDate, mConstant.locale);
if (mMonthColumn != null) {
mMonthColumn.setStaticLabels(mConstant.months);
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerConstant.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerConstant.java
deleted file mode 100644
index cfb704f..0000000
--- a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerConstant.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2015 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.support.v17.leanback.widget.picker;
-
-import android.content.res.Resources;
-import android.support.v17.leanback.R;
-
-import java.text.DateFormatSymbols;
-import java.util.Calendar;
-import java.util.Locale;
-
-/**
- * Date/Time Picker related constants
- */
-class PickerConstant {
-
- public final String[] months;
- public final String[] days;
- public final String[] hours12;
- public final String[] hours24;
- public final String[] minutes;
- public final String[] ampm;
- public final String dateSeparator;
- public final String timeSeparator;
- public final Locale locale;
-
- public PickerConstant(Locale locale, Resources resources) {
- this.locale = locale;
- DateFormatSymbols symbols = DateFormatSymbols.getInstance(locale);
- months = symbols.getShortMonths();
- Calendar calendar = Calendar.getInstance(locale);
- days = createStringIntArrays(calendar.getMinimum(Calendar.DAY_OF_MONTH),
- calendar.getMaximum(Calendar.DAY_OF_MONTH), "%02d");
- hours12 = createStringIntArrays(1, 12, "%02d");
- hours24 = createStringIntArrays(0, 23, "%02d");
- minutes = createStringIntArrays(0, 59, "%02d");
- ampm = symbols.getAmPmStrings();
- dateSeparator = resources.getString(R.string.lb_date_separator);
- timeSeparator = resources.getString(R.string.lb_time_separator);
- }
-
-
- public static String[] createStringIntArrays(int firstNumber, int lastNumber, String format) {
- String[] array = new String[lastNumber - firstNumber + 1];
- for (int i = firstNumber; i <= lastNumber; i++) {
- if (format != null) {
- array[i - firstNumber] = String.format(format, i);
- } else {
- array[i - firstNumber] = String.valueOf(i);
- }
- }
- return array;
- }
-
-
-}
\ No newline at end of file
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java
new file mode 100644
index 0000000..1e3a28f
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2017 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.support.v17.leanback.widget.picker;
+
+import android.content.res.Resources;
+import android.support.v17.leanback.R;
+
+import java.text.DateFormatSymbols;
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * Utility class that provides Date/Time related constants as well as locale-specific calendar for
+ * both {@link DatePicker} and {@link TimePicker}.
+ */
+class PickerUtility {
+
+ public static class DateConstant {
+ public final Locale locale;
+ public final String[] months;
+ public final String[] days;
+ public final String dateSeparator;
+
+ private DateConstant(Locale locale, Resources resources) {
+ this.locale = locale;
+ DateFormatSymbols symbols = DateFormatSymbols.getInstance(locale);
+ months = symbols.getShortMonths();
+ Calendar calendar = Calendar.getInstance(locale);
+ days = createStringIntArrays(calendar.getMinimum(Calendar.DAY_OF_MONTH),
+ calendar.getMaximum(Calendar.DAY_OF_MONTH), "%02d");
+ dateSeparator = resources.getString(R.string.lb_date_separator);
+ }
+ }
+
+ public static class TimeConstant {
+ public final Locale locale;
+ public final String[] hours12;
+ public final String[] hours24;
+ public final String[] minutes;
+ public final String[] ampm;
+ public final String timeSeparator;
+
+ private TimeConstant(Locale locale, Resources resources) {
+ this.locale = locale;
+ DateFormatSymbols symbols = DateFormatSymbols.getInstance(locale);
+ hours12 = createStringIntArrays(1, 12, "%02d");
+ hours24 = createStringIntArrays(0, 23, "%02d");
+ minutes = createStringIntArrays(0, 59, "%02d");
+ ampm = symbols.getAmPmStrings();
+ timeSeparator = resources.getString(R.string.lb_time_separator);
+ }
+ }
+
+ public static DateConstant getDateConstantInstance(Locale locale, Resources resources) {
+ return new DateConstant(locale, resources);
+ }
+
+ public static TimeConstant getTimeConstantInstance(Locale locale, Resources resources) {
+ return new TimeConstant(locale, resources);
+ }
+
+
+ public static String[] createStringIntArrays(int firstNumber, int lastNumber, String format) {
+ String[] array = new String[lastNumber - firstNumber + 1];
+ for (int i = firstNumber; i <= lastNumber; i++) {
+ if (format != null) {
+ array[i - firstNumber] = String.format(format, i);
+ } else {
+ array[i - firstNumber] = String.valueOf(i);
+ }
+ }
+ return array;
+ }
+
+ public static Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
+ if (oldCalendar == null) {
+ return Calendar.getInstance(locale);
+ } else {
+ final long currentTimeMillis = oldCalendar.getTimeInMillis();
+ Calendar newCalendar = Calendar.getInstance(locale);
+ newCalendar.setTimeInMillis(currentTimeMillis);
+ return newCalendar;
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java
new file mode 100644
index 0000000..a0452f5
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2017 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.support.v17.leanback.widget.picker;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.v17.leanback.R;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * {@link TimePicker} is a direct subclass of {@link Picker}.
+ * <p>
+ * This class is a widget for selecting time and displays it according to the formatting for the
+ * current system locale. The time can be selected by hour, minute, and AM/PM picker columns.
+ * The AM/PM mode is activated only when the 12 hour format is desired by setting
+ * {@code is24HourFormat} attribute to false. Otherwise, TimePicker displays only two columns for a
+ * 24 hour time format.
+ * <p>
+ * This widget can show the current time as the initial value if {@code useCurrentTime} is set to
+ * true. Each individual time picker field can be set set at any time by calling
+ * {@link #setHour(int)}, {@link #setMinute(int)} using 24-hour time format. The time format can
+ * also be changed at any time by calling {@link #setIs24Hour(boolean)}, and the AM/PM picker column
+ * will be activated or deactivated according to the given format.
+ *
+ * @attr ref R.styleable#lbTimePicker_is24HourFormat
+ * @attr ref R.styleable#lbTimePicker_useCurrentTime
+ */
+public class TimePicker extends Picker {
+
+ static final String TAG = "TimePicker";
+
+ private static final int AM_INDEX = 0;
+ private static final int PM_INDEX = 1;
+
+ private static final int HOURS_IN_HALF_DAY = 12;
+ PickerColumn mHourColumn;
+ PickerColumn mMinuteColumn;
+ PickerColumn mAmPmColumn;
+ private ViewGroup mPickerView;
+ private View mAmPmSeparatorView;
+ int mColHourIndex;
+ int mColMinuteIndex;
+ int mColAmPmIndex;
+
+ private final PickerUtility.TimeConstant mConstant;
+
+ private boolean mIs24hFormat;
+
+ private int mCurrentHour;
+ private int mCurrentMinute;
+ private int mCurrentAmPmIndex;
+
+ /**
+ * Constructor called when inflating a TimePicker widget. This version uses a default style of
+ * 0, so the only attribute values applied are those in the Context's Theme and the given
+ * AttributeSet.
+ *
+ * @param context the context this TimePicker widget is associated with through which we can
+ * access the current theme attributes and resources
+ * @param attrs the attributes of the XML tag that is inflating the TimePicker widget
+ */
+ public TimePicker(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Constructor called when inflating a TimePicker widget.
+ *
+ * @param context the context this TimePicker widget is associated with through which we can
+ * access the current theme attributes and resources
+ * @param attrs the attributes of the XML tag that is inflating the TimePicker widget
+ * @param defStyleAttr An attribute in the current theme that contains a reference to a style
+ * resource that supplies default values for the widget. Can be 0 to not
+ * look for defaults.
+ */
+ public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ mConstant = PickerUtility.getTimeConstantInstance(Locale.getDefault(),
+ context.getResources());
+
+ setSeparator(mConstant.timeSeparator);
+ mPickerView = (ViewGroup) findViewById(R.id.picker);
+ final TypedArray attributesArray = context.obtainStyledAttributes(attrs,
+ R.styleable.lbTimePicker);
+ mIs24hFormat = attributesArray.getBoolean(R.styleable.lbTimePicker_is24HourFormat, false);
+ boolean useCurrentTime = attributesArray.getBoolean(R.styleable.lbTimePicker_useCurrentTime,
+ true);
+
+ updateColumns(getTimePickerFormat());
+
+ // The column range for the minute and AM/PM column is static and does not change, whereas
+ // the hour column range can change depending on whether 12 or 24 hour format is set at
+ // any given time.
+ updateHourColumn(false);
+ updateMin(mMinuteColumn, 0);
+ updateMax(mMinuteColumn, 59);
+
+ updateMin(mAmPmColumn, 0);
+ updateMax(mAmPmColumn, 1);
+
+ updateAmPmColumn();
+
+ if (useCurrentTime) {
+ Calendar currentDate = PickerUtility.getCalendarForLocale(null,
+ mConstant.locale);
+ setHour(currentDate.get(Calendar.HOUR_OF_DAY));
+ setMinute(currentDate.get(Calendar.MINUTE));
+ }
+ }
+
+ private static boolean updateMin(PickerColumn column, int value) {
+ if (value != column.getMinValue()) {
+ column.setMinValue(value);
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean updateMax(PickerColumn column, int value) {
+ if (value != column.getMaxValue()) {
+ column.setMaxValue(value);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ *
+ * @return the time picker format string based on the current system locale and the layout
+ * direction
+ */
+ private String getTimePickerFormat() {
+ // Obtain the time format string per the current locale (e.g. h:mm a)
+ String hmaPattern = ((SimpleDateFormat) DateFormat
+ .getTimeInstance(DateFormat.SHORT, mConstant.locale)).toPattern();
+ boolean isRTL = TextUtils.getLayoutDirectionFromLocale(mConstant.locale) == View
+ .LAYOUT_DIRECTION_RTL;
+ boolean isAmPmAtEnd = hmaPattern.indexOf("a") > hmaPattern.indexOf("m");
+ // Hour will always appear to the left of minutes regardless of layout direction.
+ String timePickerFormat = isRTL ? "mh" : "hm";
+
+ return isAmPmAtEnd ? (timePickerFormat + "a") : ("a" + timePickerFormat);
+ }
+
+ private void updateColumns(String timePickerFormat) {
+ if (TextUtils.isEmpty(timePickerFormat)) {
+ timePickerFormat = "hma";
+ }
+ timePickerFormat = timePickerFormat.toUpperCase();
+
+ mHourColumn = mMinuteColumn = mAmPmColumn = null;
+ mColHourIndex = mColMinuteIndex = mColAmPmIndex = -1;
+
+ ArrayList<PickerColumn> columns = new ArrayList<>(3);
+ for (int i = 0; i < timePickerFormat.length(); i++) {
+ switch (timePickerFormat.charAt(i)) {
+ case 'H':
+ columns.add(mHourColumn = new PickerColumn());
+ mHourColumn.setStaticLabels(mConstant.hours24);
+ mColHourIndex = i;
+ break;
+ case 'M':
+ columns.add(mMinuteColumn = new PickerColumn());
+ mMinuteColumn.setStaticLabels(mConstant.minutes);
+ mColMinuteIndex = i;
+ break;
+ case 'A':
+ columns.add(mAmPmColumn = new PickerColumn());
+ mAmPmColumn.setStaticLabels(mConstant.ampm);
+ mColAmPmIndex = i;
+ updateMin(mAmPmColumn, 0);
+ updateMax(mAmPmColumn, 1);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid time picker format.");
+ }
+ }
+ setColumns(columns);
+ mAmPmSeparatorView = mPickerView.getChildAt(mColAmPmIndex == 0 ? 1 :
+ (2 * mColAmPmIndex - 1));
+ }
+
+ /**
+ * Updates the range in the hour column and notifies column changed if notifyChanged is true.
+ * Hour column can have either [0-23] or [1-12] depending on whether the 24 hour format is set
+ * or not.
+ *
+ * @param notifyChanged {code true} if we should notify data set changed on the hour column,
+ * {@code false} otherwise.
+ */
+ private void updateHourColumn(boolean notifyChanged) {
+ updateMin(mHourColumn, mIs24hFormat ? 0 : 1);
+ updateMax(mHourColumn, mIs24hFormat ? 23 : 12);
+ if (notifyChanged) {
+ setColumnAt(mColHourIndex, mHourColumn);
+ }
+ }
+
+ /**
+ * Updates AM/PM column depending on whether the 24 hour format is set or not. The visibility of
+ * this column is set to {@code GONE} for a 24 hour format, and {@code VISIBLE} in 12 hour
+ * format. This method also updates the value of this column for a 12 hour format.
+ */
+ private void updateAmPmColumn() {
+ if (mIs24hFormat) {
+ mColumnViews.get(mColAmPmIndex).setVisibility(GONE);
+ mAmPmSeparatorView.setVisibility(GONE);
+ } else {
+ mColumnViews.get(mColAmPmIndex).setVisibility(VISIBLE);
+ mAmPmSeparatorView.setVisibility(VISIBLE);
+ setColumnValue(mColAmPmIndex, mCurrentAmPmIndex, false);
+ }
+ }
+
+ /**
+ * Sets the currently selected hour using a 24-hour time.
+ *
+ * @param hour the hour to set, in the range (0-23)
+ * @see #getHour()
+ */
+ public void setHour(int hour) {
+ if (hour < 0 || hour > 23) {
+ throw new IllegalArgumentException("hour: " + hour + " is not in [0-23] range in");
+ }
+ mCurrentHour = hour;
+ if (!mIs24hFormat) {
+ if (mCurrentHour >= HOURS_IN_HALF_DAY) {
+ mCurrentAmPmIndex = PM_INDEX;
+ if (mCurrentHour > HOURS_IN_HALF_DAY) {
+ mCurrentHour -= HOURS_IN_HALF_DAY;
+ }
+ } else {
+ mCurrentAmPmIndex = AM_INDEX;
+ if (mCurrentHour == 0) {
+ mCurrentHour = HOURS_IN_HALF_DAY;
+ }
+ }
+ updateAmPmColumn();
+ }
+ setColumnValue(mColHourIndex, mCurrentHour, false);
+ }
+
+ /**
+ * Returns the currently selected hour using 24-hour time.
+ *
+ * @return the currently selected hour in the range (0-23)
+ * @see #setHour(int)
+ */
+ public int getHour() {
+ if (mIs24hFormat) {
+ return mCurrentHour;
+ }
+ if (mCurrentAmPmIndex == AM_INDEX) {
+ return mCurrentHour % HOURS_IN_HALF_DAY;
+ }
+ return (mCurrentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
+ }
+
+ /**
+ * Sets the currently selected minute.
+ *
+ * @param minute the minute to set, in the range (0-59)
+ * @see #getMinute()
+ */
+ public void setMinute(int minute) {
+ if (mCurrentMinute == minute) {
+ return;
+ }
+ if (minute < 0 || minute > 59) {
+ throw new IllegalArgumentException("minute: " + minute + " is not in [0-59] range.");
+ }
+ mCurrentMinute = minute;
+ setColumnValue(mColMinuteIndex, mCurrentMinute, false);
+ }
+
+ /**
+ * Returns the currently selected minute.
+ *
+ * @return the currently selected minute, in the range (0-59)
+ * @see #setMinute(int)
+ */
+ public int getMinute() {
+ return mCurrentMinute;
+ }
+
+ /**
+ * Sets whether this widget displays a 24-hour mode or a 12-hour mode with an AM/PM picker.
+ *
+ * @param is24Hour {@code true} to display in 24-hour mode,
+ * {@code false} ti display in 12-hour mode with AM/PM.
+ * @see #is24Hour()
+ */
+ public void setIs24Hour(boolean is24Hour) {
+ if (mIs24hFormat == is24Hour) {
+ return;
+ }
+ // the ordering of these statements is important
+ int currentHour = getHour();
+ mIs24hFormat = is24Hour;
+ updateHourColumn(true);
+ setHour(currentHour);
+ updateAmPmColumn();
+ }
+
+ /**
+ * @return {@code true} if this widget displays time in 24-hour mode,
+ * {@code false} otherwise.
+ *
+ * @see #setIs24Hour(boolean)
+ */
+ public boolean is24Hour() {
+ return mIs24hFormat;
+ }
+
+ /**
+ * Only meaningful for a 12-hour time.
+ *
+ * @return {@code true} if the currently selected time is in PM,
+ * {@code false} if the currently selected time in in AM.
+ */
+ public boolean isPm() {
+ return (mCurrentAmPmIndex == PM_INDEX);
+ }
+
+ @Override
+ public void onColumnValueChanged(int columnIndex, int newValue) {
+ if (columnIndex == mColHourIndex) {
+ mCurrentHour = newValue;
+ } else if (columnIndex == mColMinuteIndex) {
+ mCurrentMinute = newValue;
+ } else if (columnIndex == mColAmPmIndex) {
+ mCurrentAmPmIndex = newValue;
+ } else {
+ throw new IllegalArgumentException("Invalid column index.");
+ }
+ }
+}
diff --git a/v17/leanback/tests/AndroidManifest.xml b/v17/leanback/tests/AndroidManifest.xml
index f566245..ae52dd7 100644
--- a/v17/leanback/tests/AndroidManifest.xml
+++ b/v17/leanback/tests/AndroidManifest.xml
@@ -33,6 +33,10 @@
<activity android:name="android.support.v17.leanback.widget.GridActivity"
android:exported="true" />
+ <activity android:name="android.support.v17.leanback.widget.TimePickerActivity"
+ android:theme="@style/Theme.Leanback"
+ android:exported="true" />
+
<activity android:name="android.support.v17.leanback.widget.DatePickerActivity"
android:theme="@style/Theme.Leanback"
android:exported="true" />
@@ -70,31 +74,6 @@
android:theme="@style/Theme.Leanback.GuidedStep"
android:exported="true" />
- <activity android:name="android.support.v17.leanback.app.PlaybackTestActivity"
- android:theme="@style/Theme.Leanback"
- android:exported="true" />
-
- <activity android:name="android.support.v17.leanback.app.PlaybackSupportTestActivity"
- android:theme="@style/Theme.Leanback"
- android:exported="true" />
-
- <activity android:name="android.support.v17.leanback.app.PlaybackOverlayTestActivity"
- android:theme="@style/Theme.Leanback"
- android:exported="true" />
- <activity
- android:name="android.support.v17.leanback.app.VerticalGridFragmentTest$ImmediateRemoveFragmentActivity"
- android:exported="true"
- android:theme="@style/Theme.Leanback.VerticalGrid" />
-
- <activity
- android:name="android.support.v17.leanback.app.VerticalGridSupportFragmentTest$ImmediateRemoveFragmentActivity"
- android:exported="true"
- android:theme="@style/Theme.Leanback.VerticalGrid" />
-
- <activity android:name="android.support.v17.leanback.app.VideoFragmentTestActivity"
- android:theme="@style/Theme.Leanback"
- android:exported="true" />
-
<activity android:name="android.support.v17.leanback.app.TestActivity"
android:theme="@style/Theme.Leanback"
android:exported="true" />
diff --git a/v17/leanback/tests/generatev4.py b/v17/leanback/tests/generatev4.py
index b7d2a4c..ab6fafc 100755
--- a/v17/leanback/tests/generatev4.py
+++ b/v17/leanback/tests/generatev4.py
@@ -19,11 +19,11 @@
print "Generate v4 fragment related code for leanback"
-files = ['BrowseTest', 'GuidedStepTest']
+files = ['BrowseTest', 'GuidedStepTest', 'PlaybackTest']
cls = ['BrowseTest', 'Background', 'Base', 'BaseRow', 'Browse', 'Details', 'Error', 'Headers',
'PlaybackOverlay', 'Rows', 'Search', 'VerticalGrid', 'Branded',
- 'GuidedStepTest', 'GuidedStep', 'RowsTest']
+ 'GuidedStepTest', 'GuidedStep', 'RowsTest', 'PlaybackTest', 'Playback', 'Video']
for w in files:
print "copy {}Fragment to {}SupportFragment".format(w, w)
@@ -68,7 +68,7 @@
file.close()
outfile.close()
-testcls = ['Browse', 'GuidedStep', 'VerticalGrid']
+testcls = ['Browse', 'GuidedStep', 'VerticalGrid', 'Playback', 'Video']
for w in testcls:
print "copy {}FrgamentTest to {}SupportFragmentTest".format(w, w)
@@ -83,6 +83,7 @@
for w in cls:
line = line.replace('{}Fragment'.format(w), '{}SupportFragment'.format(w))
for w in testcls:
+ line = line.replace('SingleFragmentTestBase', 'SingleSupportFragmentTestBase')
line = line.replace('{}FragmentTestBase'.format(w), '{}SupportFragmentTestBase'.format(w))
line = line.replace('{}FragmentTest'.format(w), '{}SupportFragmentTest'.format(w))
line = line.replace('{}FragmentTestActivity'.format(w), '{}SupportFragmentTestActivity'.format(w))
@@ -91,6 +92,7 @@
line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
line = line.replace('extends Activity', 'extends FragmentActivity')
line = line.replace('Activity.this.getFragmentManager', 'Activity.this.getSupportFragmentManager')
+ line = line.replace('mActivity.getFragmentManager', 'mActivity.getSupportFragmentManager')
outfile.write(line)
file.close()
outfile.close()
@@ -182,6 +184,8 @@
outfile.write("/* This file is auto-generated from PlaybackControlGlueTest.java. DO NOT MODIFY. */\n\n")
for line in file:
line = line.replace('PlaybackControlGlue', 'PlaybackControlSupportGlue')
+ line = line.replace('PlaybackOverlayFragment', 'PlaybackOverlaySupportFragment')
+ line = line.replace('PlaybackGlueHostOld', 'PlaybackSupportGlueHostOld')
outfile.write(line)
file.close()
outfile.close()
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlSupportGlueTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlSupportGlueTest.java
index 748a39f..944c1f7 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlSupportGlueTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlSupportGlueTest.java
@@ -51,7 +51,7 @@
public class PlaybackControlSupportGlueTest {
- public static class PlayControlGlueImpl extends PlaybackControlSupportGlue {
+ static class PlayControlGlueImpl extends PlaybackControlSupportGlue {
int mSpeedId = PLAYBACK_SPEED_PAUSED;
// number of times onRowChanged callback is called
int mOnRowChangedCallCount = 0;
@@ -65,7 +65,7 @@
}
PlayControlGlueImpl(Context context, PlaybackOverlaySupportFragment fragment,
- int[] seekSpeeds) {
+ int[] seekSpeeds) {
super(context, fragment, seekSpeeds);
}
@@ -534,15 +534,15 @@
@Test
public void testOnItemClickedListener() {
PlaybackControlsRow row = new PlaybackControlsRow();
- final PlaybackOverlayFragment[] fragmentResult = new PlaybackOverlayFragment[1];
+ final PlaybackOverlaySupportFragment[] fragmentResult = new PlaybackOverlaySupportFragment[1];
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
- fragmentResult[0] = new PlaybackOverlayFragment();
+ fragmentResult[0] = new PlaybackOverlaySupportFragment();
}
});
- PlaybackOverlayFragment fragment = fragmentResult[0];
- glue.setHost(new PlaybackControlSupportGlue.PlaybackGlueHostOld(fragment));
+ PlaybackOverlaySupportFragment fragment = fragmentResult[0];
+ glue.setHost(new PlaybackControlSupportGlue.PlaybackSupportGlueHostOld(fragment));
glue.setControlsRow(row);
SparseArrayObjectAdapter adapter = (SparseArrayObjectAdapter)
row.getPrimaryActionsAdapter();
@@ -560,7 +560,7 @@
// Initially media is paused
assertEquals(PlaybackControlSupportGlue.PLAYBACK_SPEED_PAUSED, glue.getCurrentSpeedId());
- // simulate a click inside PlaybackOverlayFragment's PlaybackRow.
+ // simulate a click inside PlaybackOverlaySupportFragment's PlaybackRow.
fragment.getOnItemViewClickedListener().onItemClicked(vh, playPause, rowVh, row);
verify(listener, times(0)).onItemClicked(vh, playPause, rowVh, row);
assertEquals(PlaybackControlSupportGlue.PLAYBACK_SPEED_NORMAL, glue.getCurrentSpeedId());
@@ -605,5 +605,4 @@
// notifyPlaybackRowChanged on the old host and finally onRowChanged on the glue.
assertEquals(playbackGlue.getOnRowChangedCallCount(), 2);
}
-
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
index bf7077b..2fcf3ed 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
@@ -15,18 +15,18 @@
*/
package android.support.v17.leanback.app;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import android.content.Intent;
-import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.testutils.PollingCheck;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.OnItemViewClickedListener;
import android.support.v17.leanback.widget.OnItemViewSelectedListener;
@@ -37,7 +37,6 @@
import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
import android.view.KeyEvent;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -45,21 +44,35 @@
@MediumTest
@RunWith(AndroidJUnit4.class)
-public class PlaybackFragmentTest {
+public class PlaybackFragmentTest extends SingleFragmentTestBase {
private static final String TAG = "PlaybackFragmentTest";
private static final long TRANSITION_LENGTH = 1000;
- @Rule
- public ActivityTestRule<PlaybackTestActivity> activityTestRule =
- new ActivityTestRule<>(PlaybackTestActivity.class, false, false);
- private PlaybackTestActivity mActivity;
+ @Test
+ public void testDetachCalledWhenDestroyFragment() throws Throwable {
+ launchAndWaitActivity(PlaybackTestFragment.class, 1000);
+ PlaybackTestFragment fragment = (PlaybackTestFragment) mActivity.getTestFragment();
+ PlaybackGlue glue = fragment.getGlue();
+ activityTestRule.runOnUiThread(new Runnable() {
+ public void run() {
+ mActivity.finish();
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mActivity.isDestroyed();
+ }
+ });
+ assertNull(glue.getHost());
+ }
@Test
public void testSelectedListener() throws Throwable {
- Intent intent = new Intent();
- mActivity = activityTestRule.launchActivity(intent);
- PlaybackTestFragment fragment = mActivity.getPlaybackFragment();
+ launchAndWaitActivity(PlaybackTestFragment.class, 1000);
+ PlaybackTestFragment fragment = (PlaybackTestFragment) mActivity.getTestFragment();
+
assertTrue(fragment.getView().hasFocus());
OnItemViewSelectedListener selectedListener = Mockito.mock(
@@ -126,9 +139,9 @@
@Test
public void testClickedListener() throws Throwable {
- Intent intent = new Intent();
- mActivity = activityTestRule.launchActivity(intent);
- PlaybackTestFragment fragment = mActivity.getPlaybackFragment();
+ launchAndWaitActivity(PlaybackTestFragment.class, 1000);
+ PlaybackTestFragment fragment = (PlaybackTestFragment) mActivity.getTestFragment();
+
assertTrue(fragment.getView().hasFocus());
OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class);
@@ -210,10 +223,4 @@
listRowItemPassed);
}
- private void sendKeys(int ...keys) {
- for (int i = 0; i < keys.length; i++) {
- InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
- }
- }
-
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayFragmentTest.java
index f27ace0..f05da9c 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayFragmentTest.java
@@ -18,32 +18,26 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import android.content.Intent;
import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v17.leanback.test.R;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@MediumTest
@RunWith(AndroidJUnit4.class)
-public class PlaybackOverlayFragmentTest {
-
- @Rule
- public ActivityTestRule<PlaybackOverlayTestActivity> activityTestRule =
- new ActivityTestRule<>(PlaybackOverlayTestActivity.class, false, false);
- private PlaybackOverlayTestActivity mActivity;
+public class PlaybackOverlayFragmentTest extends SingleFragmentTestBase {
@Test
public void workaroundVideoViewStealFocus() {
- Intent intent = new Intent();
- mActivity = activityTestRule.launchActivity(intent);
+ launchAndWaitActivity(PlaybackOverlayTestFragment.class,
+ R.layout.playback_controls_with_video, 0);
+ PlaybackOverlayTestFragment fragment = (PlaybackOverlayTestFragment)
+ mActivity.getTestFragment();
assertFalse(mActivity.findViewById(R.id.videoView).hasFocus());
- assertTrue(mActivity.getPlaybackFragment().getView().hasFocus());
+ assertTrue(fragment.getView().hasFocus());
}
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestActivity.java
deleted file mode 100644
index ea2aa38..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestActivity.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class PlaybackOverlayTestActivity extends Activity {
- private List<PictureInPictureListener> mListeners = new ArrayList<>();
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.playback_controls_with_video);
- }
-
- @Override
- public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
- for (PictureInPictureListener listener : mListeners) {
- listener.onPictureInPictureModeChanged(isInPictureInPictureMode);
- }
- }
-
- public void registerPictureInPictureListener(PictureInPictureListener listener) {
- mListeners.add(listener);
- }
-
- public void unregisterPictureInPictureListener(PictureInPictureListener listener) {
- mListeners.remove(listener);
- }
-
- public interface PictureInPictureListener {
- void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
- }
-
- public PlaybackOverlayTestFragment getPlaybackFragment() {
- return (PlaybackOverlayTestFragment) getFragmentManager().findFragmentById(
- R.id.playback_controls_fragment);
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestFragment.java
index b44dd09..e5dbe08 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestFragment.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestFragment.java
@@ -40,9 +40,7 @@
import android.view.View;
import android.widget.Toast;
-public class PlaybackOverlayTestFragment
- extends PlaybackOverlayFragment
- implements PlaybackOverlayTestActivity.PictureInPictureListener {
+public class PlaybackOverlayTestFragment extends PlaybackOverlayFragment {
private static final String TAG = "leanback.PlaybackControlsFragment";
/**
@@ -169,26 +167,6 @@
public void onStart() {
super.onStart();
mGlue.setFadingEnabled(true);
- mGlue.enableProgressUpdating(mGlue.hasValidMedia() && mGlue.isMediaPlaying());
- ((PlaybackOverlayTestActivity) getActivity()).registerPictureInPictureListener(this);
- }
-
- @Override
- public void onStop() {
- mGlue.enableProgressUpdating(false);
- ((PlaybackOverlayTestActivity) getActivity()).unregisterPictureInPictureListener(this);
- super.onStop();
- }
-
- @Override
- public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
- if (isInPictureInPictureMode) {
- // Hide the controls in picture-in-picture mode.
- setFadingEnabled(true);
- fadeOut();
- } else {
- setFadingEnabled(mGlue.isMediaPlaying());
- }
}
abstract static class PlaybackControlHelper extends PlaybackControlGlue {
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
index fdba125..d33e3ef 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from PlaybackFragmentTest.java. DO NOT MODIFY. */
+
/*
* Copyright (C) 2016 The Android Open Source Project
*
@@ -15,18 +18,18 @@
*/
package android.support.v17.leanback.app;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import android.content.Intent;
-import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.testutils.PollingCheck;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.OnItemViewClickedListener;
import android.support.v17.leanback.widget.OnItemViewSelectedListener;
@@ -37,7 +40,6 @@
import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
import android.view.KeyEvent;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -45,21 +47,35 @@
@MediumTest
@RunWith(AndroidJUnit4.class)
-public class PlaybackSupportFragmentTest {
+public class PlaybackSupportFragmentTest extends SingleSupportFragmentTestBase {
private static final String TAG = "PlaybackSupportFragmentTest";
private static final long TRANSITION_LENGTH = 1000;
- @Rule
- public ActivityTestRule<PlaybackSupportTestActivity> activityTestRule =
- new ActivityTestRule<>(PlaybackSupportTestActivity.class, false, false);
- private PlaybackSupportTestActivity mActivity;
+ @Test
+ public void testDetachCalledWhenDestroyFragment() throws Throwable {
+ launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
+ PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) mActivity.getTestFragment();
+ PlaybackGlue glue = fragment.getGlue();
+ activityTestRule.runOnUiThread(new Runnable() {
+ public void run() {
+ mActivity.finish();
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mActivity.isDestroyed();
+ }
+ });
+ assertNull(glue.getHost());
+ }
@Test
public void testSelectedListener() throws Throwable {
- Intent intent = new Intent();
- mActivity = activityTestRule.launchActivity(intent);
- PlaybackSupportTestFragment fragment = mActivity.getPlaybackFragment();
+ launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
+ PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) mActivity.getTestFragment();
+
assertTrue(fragment.getView().hasFocus());
OnItemViewSelectedListener selectedListener = Mockito.mock(
@@ -82,8 +98,7 @@
ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
ArgumentCaptor.forClass(Presenter.ViewHolder.class);
- ArgumentCaptor<Object> itemCaptor =
- ArgumentCaptor.forClass(Object.class);
+ ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
@@ -127,9 +142,9 @@
@Test
public void testClickedListener() throws Throwable {
- Intent intent = new Intent();
- mActivity = activityTestRule.launchActivity(intent);
- PlaybackSupportTestFragment fragment = mActivity.getPlaybackFragment();
+ launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
+ PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) mActivity.getTestFragment();
+
assertTrue(fragment.getView().hasFocus());
OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class);
@@ -211,10 +226,4 @@
listRowItemPassed);
}
- private void sendKeys(int ...keys) {
- for (int i = 0; i < keys.length; i++) {
- InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
- }
- }
-
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestActivity.java
deleted file mode 100644
index c85fe83..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestActivity.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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.support.v17.leanback.app;
-
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-import android.support.v4.app.FragmentActivity;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class PlaybackSupportTestActivity extends FragmentActivity {
- private List<PictureInPictureListener> mListeners = new ArrayList<>();
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.playback_support_controls);
- }
-
- @Override
- public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
- for (PictureInPictureListener listener : mListeners) {
- listener.onPictureInPictureModeChanged(isInPictureInPictureMode);
- }
- }
-
- public void registerPictureInPictureListener(PictureInPictureListener listener) {
- mListeners.add(listener);
- }
-
- public void unregisterPictureInPictureListener(PictureInPictureListener listener) {
- mListeners.remove(listener);
- }
-
- public interface PictureInPictureListener {
- void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
- }
-
- public PlaybackSupportTestFragment getPlaybackFragment() {
- return (PlaybackSupportTestFragment) getSupportFragmentManager().findFragmentById(
- R.id.playback_controls_fragment);
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestActivity.java
deleted file mode 100644
index ff840ec..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestActivity.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class PlaybackTestActivity extends Activity {
- private List<PictureInPictureListener> mListeners = new ArrayList<>();
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.playback_controls);
- }
-
- @Override
- public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
- for (PictureInPictureListener listener : mListeners) {
- listener.onPictureInPictureModeChanged(isInPictureInPictureMode);
- }
- }
-
- public void registerPictureInPictureListener(PictureInPictureListener listener) {
- mListeners.add(listener);
- }
-
- public void unregisterPictureInPictureListener(PictureInPictureListener listener) {
- mListeners.remove(listener);
- }
-
- public interface PictureInPictureListener {
- void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
- }
-
- public PlaybackTestFragment getPlaybackFragment() {
- return (PlaybackTestFragment) getFragmentManager().findFragmentById(
- R.id.playback_controls_fragment);
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
index 043d73e..f9fd33f 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
@@ -19,7 +19,6 @@
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
-
import android.support.v17.leanback.media.PlaybackControlGlue;
import android.support.v17.leanback.test.R;
import android.support.v17.leanback.widget.Action;
@@ -28,7 +27,6 @@
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.OnItemViewSelectedListener;
import android.support.v17.leanback.widget.PlaybackControlsRow;
import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.PresenterSelector;
@@ -75,22 +73,12 @@
}
};
- private OnItemViewSelectedListener mOnItemViewSelectedListener =
- new OnItemViewSelectedListener() {
- @Override
- public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- Log.d(TAG, "onItemSelected: " + item + " row " + row);
- }
- };
-
@Override
public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate");
super.onCreate(savedInstanceState);
setBackgroundType(BACKGROUND_TYPE);
- // setOnItemViewSelectedListener(mOnItemViewSelectedListener);
createComponents(getActivity());
setOnItemViewClickedListener(mOnItemViewClickedListener);
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
similarity index 94%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java
rename to v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
index 4a07a60..0973fe1 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from PlaybackTestFragment.java. DO NOT MODIFY. */
+
/*
* Copyright (C) 2016 The Android Open Source Project
*
@@ -19,7 +22,6 @@
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
-
import android.support.v17.leanback.media.PlaybackControlGlue;
import android.support.v17.leanback.test.R;
import android.support.v17.leanback.widget.Action;
@@ -28,7 +30,6 @@
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.OnItemViewSelectedListener;
import android.support.v17.leanback.widget.PlaybackControlsRow;
import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.PresenterSelector;
@@ -40,13 +41,13 @@
import android.view.View;
import android.widget.Toast;
-public class PlaybackSupportTestFragment extends PlaybackSupportFragment {
- private static final String TAG = "PlaybackTestFragment";
+public class PlaybackTestSupportFragment extends PlaybackSupportFragment {
+ private static final String TAG = "PlaybackTestSupportFragment";
/**
* Change this to choose a different overlay background.
*/
- private static final int BACKGROUND_TYPE = PlaybackFragment.BG_LIGHT;
+ private static final int BACKGROUND_TYPE = PlaybackSupportFragment.BG_LIGHT;
private static final int ROW_CONTROLS = 0;
@@ -75,22 +76,12 @@
}
};
- private OnItemViewSelectedListener mOnItemViewSelectedListener =
- new OnItemViewSelectedListener() {
- @Override
- public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- Log.d(TAG, "onItemSelected: " + item + " row " + row);
- }
- };
-
@Override
public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate");
super.onCreate(savedInstanceState);
setBackgroundType(BACKGROUND_TYPE);
- // setOnItemViewSelectedListener(mOnItemViewSelectedListener);
createComponents(getActivity());
setOnItemViewClickedListener(mOnItemViewClickedListener);
@@ -124,7 +115,7 @@
};
mGlue.setHost(new PlaybackSupportFragmentGlueHost(this));
- // mGlue.setOnI
+ // mGlue.setOnI
mListRowPresenter = new ListRowPresenter();
setAdapter(new SparseArrayObjectAdapter(new PresenterSelector() {
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
index 8be38bb..3f6cc1b 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
@@ -29,13 +29,15 @@
*/
public static final String EXTRA_FRAGMENT_NAME = "fragmentName";
+ public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
- setContentView(R.layout.rows);
- if (savedInstanceState == null) {
+ setContentView(intent.getIntExtra(EXTRA_ACTIVITY_LAYOUT, R.layout.single_fragment));
+ if (savedInstanceState == null && findViewById(R.id.main_frame) != null) {
try {
Fragment fragment = (Fragment) Class.forName(
intent.getStringExtra(EXTRA_FRAGMENT_NAME)).newInstance();
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
index dbc8872..e34f4e6 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
@@ -62,10 +62,23 @@
launchAndWaitActivity(fragmentClass.getName(), waitTimeMs);
}
+ public void launchAndWaitActivity(Class fragmentClass, int activityLayoutId, long waitTimeMs) {
+ launchAndWaitActivity(fragmentClass.getName(), activityLayoutId, waitTimeMs);
+ }
+
public void launchAndWaitActivity(String firstFragmentName, long waitTimeMs) {
Intent intent = new Intent();
intent.putExtra(SingleFragmentTestActivity.EXTRA_FRAGMENT_NAME, firstFragmentName);
mActivity = activityTestRule.launchActivity(intent);
SystemClock.sleep(waitTimeMs);
}
+
+ public void launchAndWaitActivity(String firstFragmentName, int activityLayoutId,
+ long waitTimeMs) {
+ Intent intent = new Intent();
+ intent.putExtra(SingleFragmentTestActivity.EXTRA_FRAGMENT_NAME, firstFragmentName);
+ intent.putExtra(SingleFragmentTestActivity.EXTRA_ACTIVITY_LAYOUT, activityLayoutId);
+ mActivity = activityTestRule.launchActivity(intent);
+ SystemClock.sleep(waitTimeMs);
+ }
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
index 85d3f43..fb0d349 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
@@ -28,13 +28,15 @@
*/
public static final String EXTRA_FRAGMENT_NAME = "fragmentName";
+ public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
- setContentView(R.layout.rows);
- if (savedInstanceState == null) {
+ setContentView(intent.getIntExtra(EXTRA_ACTIVITY_LAYOUT, R.layout.single_fragment));
+ if (savedInstanceState == null && findViewById(R.id.main_frame) != null) {
try {
Fragment fragment = (Fragment) Class.forName(
intent.getStringExtra(EXTRA_FRAGMENT_NAME)).newInstance();
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
index af56715..5c4f4fd 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
@@ -16,13 +16,10 @@
package android.support.v17.leanback.app;
-import android.app.Activity;
import android.app.Fragment;
-import android.content.Intent;
import android.os.Bundle;
-import android.os.Handler;
+import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.VerticalGridPresenter;
@@ -32,7 +29,7 @@
@MediumTest
@RunWith(AndroidJUnit4.class)
-public class VerticalGridFragmentTest {
+public class VerticalGridFragmentTest extends SingleFragmentTestBase {
public static class GridFragment extends VerticalGridFragment {
@Override
@@ -48,31 +45,23 @@
}
}
- public static class ImmediateRemoveFragmentActivity extends Activity {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- new Handler().postDelayed(new Runnable(){
- public void run() {
- GridFragment f = new GridFragment();
- ImmediateRemoveFragmentActivity.this.getFragmentManager().beginTransaction()
- .replace(android.R.id.content, f, null).commit();
- f.startEntranceTransition();
- ImmediateRemoveFragmentActivity.this.getFragmentManager().beginTransaction()
- .replace(android.R.id.content, new Fragment(), null).commit();
- }
- }, 500);
- }
- }
-
@Test
public void immediateRemoveFragment() throws Throwable {
- Intent intent = new Intent();
- ActivityTestRule<ImmediateRemoveFragmentActivity> activityTestRule =
- new ActivityTestRule<>(ImmediateRemoveFragmentActivity.class, false, false);
- ImmediateRemoveFragmentActivity activity = activityTestRule.launchActivity(intent);
+ launchAndWaitActivity(GridFragment.class, 500);
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ public void run() {
+ GridFragment f = new GridFragment();
+ mActivity.getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, f, null).commit();
+ f.startEntranceTransition();
+ mActivity.getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, new Fragment(), null).commit();
+ }
+ });
Thread.sleep(1000);
+ mActivity.finish();
}
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
index 7dd402d..8b58c33 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
@@ -19,13 +19,10 @@
package android.support.v17.leanback.app;
-import android.support.v4.app.FragmentActivity;
import android.support.v4.app.Fragment;
-import android.content.Intent;
import android.os.Bundle;
-import android.os.Handler;
+import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.VerticalGridPresenter;
@@ -35,7 +32,7 @@
@MediumTest
@RunWith(AndroidJUnit4.class)
-public class VerticalGridSupportFragmentTest {
+public class VerticalGridSupportFragmentTest extends SingleSupportFragmentTestBase {
public static class GridFragment extends VerticalGridSupportFragment {
@Override
@@ -51,31 +48,23 @@
}
}
- public static class ImmediateRemoveFragmentActivity extends FragmentActivity {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- new Handler().postDelayed(new Runnable(){
- public void run() {
- GridFragment f = new GridFragment();
- ImmediateRemoveFragmentActivity.this.getSupportFragmentManager().beginTransaction()
- .replace(android.R.id.content, f, null).commit();
- f.startEntranceTransition();
- ImmediateRemoveFragmentActivity.this.getSupportFragmentManager().beginTransaction()
- .replace(android.R.id.content, new Fragment(), null).commit();
- }
- }, 500);
- }
- }
-
@Test
public void immediateRemoveFragment() throws Throwable {
- Intent intent = new Intent();
- ActivityTestRule<ImmediateRemoveFragmentActivity> activityTestRule =
- new ActivityTestRule<>(ImmediateRemoveFragmentActivity.class, false, false);
- ImmediateRemoveFragmentActivity activity = activityTestRule.launchActivity(intent);
+ launchAndWaitActivity(GridFragment.class, 500);
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ public void run() {
+ GridFragment f = new GridFragment();
+ mActivity.getSupportFragmentManager().beginTransaction()
+ .replace(android.R.id.content, f, null).commit();
+ f.startEntranceTransition();
+ mActivity.getSupportFragmentManager().beginTransaction()
+ .replace(android.R.id.content, new Fragment(), null).commit();
+ }
+ });
Thread.sleep(1000);
+ mActivity.finish();
}
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
index b8bb68e..5c8c89e 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
@@ -15,58 +15,103 @@
*/
package android.support.v17.leanback.app;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
-import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.media.MediaPlayerGlue;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.media.PlaybackGlueHost;
import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.view.LayoutInflater;
import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
-import org.junit.Rule;
+import junit.framework.Assert;
+
import org.junit.Test;
import org.junit.runner.RunWith;
@MediumTest
@RunWith(AndroidJUnit4.class)
-public class VideoFragmentTest {
+public class VideoFragmentTest extends SingleFragmentTestBase {
- @Rule
- public ActivityTestRule<VideoFragmentTestActivity> activityTestRule =
- new ActivityTestRule<>(VideoFragmentTestActivity.class, false, false);
- private VideoFragmentTestActivity mActivity;
+ public static class Fragment_setSurfaceViewCallbackBeforeCreate extends VideoFragment {
+ boolean mSurfaceCreated;
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ setSurfaceHolderCallback(new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ mSurfaceCreated = true;
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ mSurfaceCreated = false;
+ }
+ });
+
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+ }
@Test
public void setSurfaceViewCallbackBeforeCreate() {
- Intent intent = new Intent();
- mActivity = activityTestRule.launchActivity(intent);
+ launchAndWaitActivity(Fragment_setSurfaceViewCallbackBeforeCreate.class, 1000);
+ Fragment_setSurfaceViewCallbackBeforeCreate fragment1 =
+ (Fragment_setSurfaceViewCallbackBeforeCreate) mActivity.getTestFragment();
+ assertNotNull(fragment1);
+ assertTrue(fragment1.mSurfaceCreated);
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
- mActivity.replaceVideoFragment();
+ mActivity.getFragmentManager().beginTransaction()
+ .replace(R.id.main_frame, new Fragment_setSurfaceViewCallbackBeforeCreate())
+ .commitAllowingStateLoss();
}
});
+ SystemClock.sleep(500);
- VideoFragment fragment = (VideoFragment) mActivity.getFragmentManager().findFragmentById(
- R.id.video_fragment);
- assertNotNull(fragment);
+ assertFalse(fragment1.mSurfaceCreated);
+
+ Fragment_setSurfaceViewCallbackBeforeCreate fragment2 =
+ (Fragment_setSurfaceViewCallbackBeforeCreate) mActivity.getTestFragment();
+ assertNotNull(fragment2);
+ assertTrue(fragment2.mSurfaceCreated);
+ assertNotSame(fragment1, fragment2);
}
@Test
public void setSurfaceViewCallbackAfterCreate() {
- Intent intent = new Intent();
- mActivity = activityTestRule.launchActivity(intent);
+ launchAndWaitActivity(VideoFragment.class, 1000);
+ VideoFragment fragment = (VideoFragment) mActivity.getTestFragment();
- VideoFragment fragment = (VideoFragment) mActivity.getFragmentManager().findFragmentById(
- R.id.video_fragment);
assertNotNull(fragment);
+ final boolean[] surfaceCreated = new boolean[1];
fragment.setSurfaceHolderCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
+ surfaceCreated[0] = true;
}
@Override
@@ -75,7 +120,130 @@
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
+ surfaceCreated[0] = false;
}
});
+ assertTrue(surfaceCreated[0]);
}
+
+ public static class Fragment_withVideoPlayer extends VideoFragment {
+ MediaPlayerGlue mGlue;
+ int mOnCreateCalled;
+ int mOnCreateViewCalled;
+ int mOnDestroyViewCalled;
+ int mOnDestroyCalled;
+ int mGlueAttachedToHost;
+ int mGlueDetachedFromHost;
+ int mGlueOnReadyForPlaybackCalled;
+
+ public Fragment_withVideoPlayer() {
+ setRetainInstance(true);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mOnCreateCalled++;
+ super.onCreate(savedInstanceState);
+ mGlue = new MediaPlayerGlue(getActivity()) {
+ @Override
+ protected void onDetachedFromHost() {
+ mGlueDetachedFromHost++;
+ super.onDetachedFromHost();
+ }
+
+ @Override
+ protected void onAttachedToHost(PlaybackGlueHost host) {
+ super.onAttachedToHost(host);
+ mGlueAttachedToHost++;
+ }
+ };
+ mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ mGlue.setArtist("Leanback");
+ mGlue.setTitle("Leanback team at work");
+ mGlue.setMediaSource(
+ Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
+ mGlue.setPlayerCallback(new PlaybackGlue.PlayerCallback() {
+ @Override
+ public void onReadyForPlayback() {
+ mGlueOnReadyForPlaybackCalled++;
+ mGlue.play();
+ }
+ });
+ mGlue.setHost(new VideoFragmentGlueHost(this));
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mOnCreateViewCalled++;
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ @Override
+ public void onDestroyView() {
+ mOnDestroyViewCalled++;
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onDestroy() {
+ mOnDestroyCalled++;
+ super.onDestroy();
+ }
+ }
+
+ @Test
+ public void mediaPlayerGlueInVideoFragment() {
+ launchAndWaitActivity(Fragment_withVideoPlayer.class, 1000);
+ final Fragment_withVideoPlayer fragment = (Fragment_withVideoPlayer)
+ mActivity.getTestFragment();
+
+ PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return fragment.mGlue.isMediaPlaying();
+ }
+ });
+
+ assertEquals(1, fragment.mOnCreateCalled);
+ assertEquals(1, fragment.mOnCreateViewCalled);
+ assertEquals(0, fragment.mOnDestroyViewCalled);
+ assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+ View fragmentViewBeforeRecreate = fragment.getView();
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.recreate();
+ }
+ });
+
+ PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return fragment.mOnCreateViewCalled == 2 && fragment.mGlue.isMediaPlaying();
+ }
+ });
+ View fragmentViewAfterRecreate = fragment.getView();
+
+ Assert.assertNotSame(fragmentViewBeforeRecreate, fragmentViewAfterRecreate);
+ assertEquals(1, fragment.mOnCreateCalled);
+ assertEquals(2, fragment.mOnCreateViewCalled);
+ assertEquals(1, fragment.mOnDestroyViewCalled);
+
+ assertEquals(1, fragment.mGlueAttachedToHost);
+ assertEquals(0, fragment.mGlueDetachedFromHost);
+ assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+
+ mActivity.finish();
+ PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return fragment.mGlueDetachedFromHost == 1;
+ }
+ });
+ assertEquals(2, fragment.mOnDestroyViewCalled);
+ assertEquals(1, fragment.mOnDestroyCalled);
+ }
+
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTestActivity.java
deleted file mode 100644
index e2a8f48..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTestActivity.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-
-/**
- * Test activity containing {@link VideoFragment}.
- */
-public class VideoFragmentTestActivity extends Activity {
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.video_fragment_with_controls);
- }
-
- public void replaceVideoFragment() {
- getFragmentManager().beginTransaction()
- .replace(R.id.video_fragment, new VideoTestFragment())
- .commitAllowingStateLoss();
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
new file mode 100644
index 0000000..dff3c0c
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
@@ -0,0 +1,252 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from VideoFragmentTest.java. DO NOT MODIFY. */
+
+/*
+ * 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.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.media.MediaPlayerGlue;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.media.PlaybackGlueHost;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.view.LayoutInflater;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
+
+import junit.framework.Assert;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class VideoSupportFragmentTest extends SingleSupportFragmentTestBase {
+
+ public static class Fragment_setSurfaceViewCallbackBeforeCreate extends VideoSupportFragment {
+ boolean mSurfaceCreated;
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ setSurfaceHolderCallback(new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ mSurfaceCreated = true;
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ mSurfaceCreated = false;
+ }
+ });
+
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+ }
+
+ @Test
+ public void setSurfaceViewCallbackBeforeCreate() {
+ launchAndWaitActivity(Fragment_setSurfaceViewCallbackBeforeCreate.class, 1000);
+ Fragment_setSurfaceViewCallbackBeforeCreate fragment1 =
+ (Fragment_setSurfaceViewCallbackBeforeCreate) mActivity.getTestFragment();
+ assertNotNull(fragment1);
+ assertTrue(fragment1.mSurfaceCreated);
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.getSupportFragmentManager().beginTransaction()
+ .replace(R.id.main_frame, new Fragment_setSurfaceViewCallbackBeforeCreate())
+ .commitAllowingStateLoss();
+ }
+ });
+ SystemClock.sleep(500);
+
+ assertFalse(fragment1.mSurfaceCreated);
+
+ Fragment_setSurfaceViewCallbackBeforeCreate fragment2 =
+ (Fragment_setSurfaceViewCallbackBeforeCreate) mActivity.getTestFragment();
+ assertNotNull(fragment2);
+ assertTrue(fragment2.mSurfaceCreated);
+ assertNotSame(fragment1, fragment2);
+ }
+
+ @Test
+ public void setSurfaceViewCallbackAfterCreate() {
+ launchAndWaitActivity(VideoSupportFragment.class, 1000);
+ VideoSupportFragment fragment = (VideoSupportFragment) mActivity.getTestFragment();
+
+ assertNotNull(fragment);
+
+ final boolean[] surfaceCreated = new boolean[1];
+ fragment.setSurfaceHolderCallback(new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ surfaceCreated[0] = true;
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ surfaceCreated[0] = false;
+ }
+ });
+ assertTrue(surfaceCreated[0]);
+ }
+
+ public static class Fragment_withVideoPlayer extends VideoSupportFragment {
+ MediaPlayerGlue mGlue;
+ int mOnCreateCalled;
+ int mOnCreateViewCalled;
+ int mOnDestroyViewCalled;
+ int mOnDestroyCalled;
+ int mGlueAttachedToHost;
+ int mGlueDetachedFromHost;
+ int mGlueOnReadyForPlaybackCalled;
+
+ public Fragment_withVideoPlayer() {
+ setRetainInstance(true);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mOnCreateCalled++;
+ super.onCreate(savedInstanceState);
+ mGlue = new MediaPlayerGlue(getActivity()) {
+ @Override
+ protected void onDetachedFromHost() {
+ mGlueDetachedFromHost++;
+ super.onDetachedFromHost();
+ }
+
+ @Override
+ protected void onAttachedToHost(PlaybackGlueHost host) {
+ super.onAttachedToHost(host);
+ mGlueAttachedToHost++;
+ }
+ };
+ mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ mGlue.setArtist("Leanback");
+ mGlue.setTitle("Leanback team at work");
+ mGlue.setMediaSource(
+ Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
+ mGlue.setPlayerCallback(new PlaybackGlue.PlayerCallback() {
+ @Override
+ public void onReadyForPlayback() {
+ mGlueOnReadyForPlaybackCalled++;
+ mGlue.play();
+ }
+ });
+ mGlue.setHost(new VideoSupportFragmentGlueHost(this));
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mOnCreateViewCalled++;
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ @Override
+ public void onDestroyView() {
+ mOnDestroyViewCalled++;
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onDestroy() {
+ mOnDestroyCalled++;
+ super.onDestroy();
+ }
+ }
+
+ @Test
+ public void mediaPlayerGlueInVideoSupportFragment() {
+ launchAndWaitActivity(Fragment_withVideoPlayer.class, 1000);
+ final Fragment_withVideoPlayer fragment = (Fragment_withVideoPlayer)
+ mActivity.getTestFragment();
+
+ PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return fragment.mGlue.isMediaPlaying();
+ }
+ });
+
+ assertEquals(1, fragment.mOnCreateCalled);
+ assertEquals(1, fragment.mOnCreateViewCalled);
+ assertEquals(0, fragment.mOnDestroyViewCalled);
+ assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+ View fragmentViewBeforeRecreate = fragment.getView();
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.recreate();
+ }
+ });
+
+ PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return fragment.mOnCreateViewCalled == 2 && fragment.mGlue.isMediaPlaying();
+ }
+ });
+ View fragmentViewAfterRecreate = fragment.getView();
+
+ Assert.assertNotSame(fragmentViewBeforeRecreate, fragmentViewAfterRecreate);
+ assertEquals(1, fragment.mOnCreateCalled);
+ assertEquals(2, fragment.mOnCreateViewCalled);
+ assertEquals(1, fragment.mOnDestroyViewCalled);
+
+ assertEquals(1, fragment.mGlueAttachedToHost);
+ assertEquals(0, fragment.mGlueDetachedFromHost);
+ assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+
+ mActivity.finish();
+ PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return fragment.mGlueDetachedFromHost == 1;
+ }
+ });
+ assertEquals(2, fragment.mOnDestroyViewCalled);
+ assertEquals(1, fragment.mOnDestroyCalled);
+ }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoTestFragment.java
deleted file mode 100644
index a51231f..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoTestFragment.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.support.v17.leanback.app;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.SurfaceHolder;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * {@link VideoFragment} subclass used for testing.
- */
-public class VideoTestFragment extends VideoFragment {
- @Override
- public View onCreateView(
- LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-
- setSurfaceHolderCallback(new SurfaceHolder.Callback() {
- @Override
- public void surfaceCreated(SurfaceHolder holder) {
- }
-
- @Override
- public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
- }
-
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- }
- });
-
- return super.onCreateView(inflater, container, savedInstanceState);
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java b/v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java
new file mode 100644
index 0000000..a2956d6
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2017 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.support.v17.leanback.media;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.testutils.PollingCheck;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class MediaPlayerGlueTest {
+
+ /**
+ * Mockito spy not working on API 19 if class has package private method (b/35387610)
+ */
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void mediaPlayer() {
+ // create a MediaPlayerGlue with updatePeriod = 100ms
+ final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ final MediaPlayerGlue[] result = new MediaPlayerGlue[1];
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ result[0] = new MediaPlayerGlue(context);
+ }
+ });
+ final MediaPlayerGlue glue = Mockito.spy(result[0]);
+ Mockito.when(glue.getUpdatePeriod()).thenReturn(100);
+
+ final PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+ glue.setHost(host);
+ glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ final boolean[] ready = new boolean[] {false};
+ glue.setPlayerCallback(new PlaybackGlue.PlayerCallback() {
+ @Override
+ public void onReadyForPlayback() {
+ glue.play();
+ ready[0] = true;
+
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ glue.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/track_01"));
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ready[0];
+ }
+ });
+
+ // Test enableProgressUpdating(true) and enableProgressUpdating(false);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ glue.enableProgressUpdating(true);
+ }
+ });
+ Mockito.reset(glue);
+ SystemClock.sleep(1000);
+ Mockito.verify(glue, atLeastOnce()).updateProgress();
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ glue.enableProgressUpdating(false);
+ }
+ });
+ Mockito.reset(glue);
+ SystemClock.sleep(1000);
+ Mockito.verify(glue, never()).updateProgress();
+
+ // Test onStart()/onStop() will pause the updateProgress.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ host.notifyOnStart();
+ }
+ });
+ Mockito.reset(glue);
+ SystemClock.sleep(1000);
+ Mockito.verify(glue, atLeastOnce()).updateProgress();
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ host.notifyOnStop();
+ }
+ });
+ Mockito.reset(glue);
+ SystemClock.sleep(1000);
+ Mockito.verify(glue, never()).updateProgress();
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ host.notifyOnDestroy();
+ }
+ });
+ assertNull(glue.getHost());
+ Mockito.verify(glue, times(1)).onDetachedFromHost();
+ Mockito.verify(glue, times(1)).release();
+ }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
new file mode 100644
index 0000000..199ab3e
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 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.support.v17.leanback.media;
+
+/**
+ * Fake PlaybackGlueHost used by test.
+ */
+public class PlaybackGlueHostImpl extends PlaybackGlueHost {
+
+ HostCallback mHostCallback;
+
+ @Override
+ public void setHostCallback(HostCallback callback) {
+ mHostCallback = callback;
+ }
+
+ void notifyOnStart() {
+ if (mHostCallback != null) {
+ mHostCallback.onHostStart();
+ }
+ }
+
+ void notifyOnStop() {
+ if (mHostCallback != null) {
+ mHostCallback.onHostStop();
+ }
+ }
+
+ void notifyOnResume() {
+ if (mHostCallback != null) {
+ mHostCallback.onHostResume();
+ }
+ }
+
+ void notifyOnPause() {
+ if (mHostCallback != null) {
+ mHostCallback.onHostPause();
+ }
+ }
+
+ void notifyOnDestroy() {
+ if (mHostCallback != null) {
+ mHostCallback.onHostDestroy();
+ }
+ }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java
new file mode 100644
index 0000000..3932ea6
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 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.support.v17.leanback.media;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.times;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PlaybackGlueTest {
+
+
+ public static class PlaybackGlueImpl extends PlaybackGlue {
+
+ public PlaybackGlueImpl(Context context) {
+ super(context);
+ }
+ }
+
+ @Test
+ public void glueAndHostInteraction() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ PlaybackGlue glue = Mockito.spy(new PlaybackGlueImpl(context));
+ PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+ glue.setHost(host);
+ Mockito.verify(glue, times(1)).onAttachedToHost(host);
+ assertSame(glue, host.mGlue);
+ assertSame(host, glue.getHost());
+
+ host.notifyOnStart();
+ Mockito.verify(glue, times(1)).onHostStart();
+
+ host.notifyOnResume();
+ Mockito.verify(glue, times(1)).onHostResume();
+
+ host.notifyOnPause();
+ Mockito.verify(glue, times(1)).onHostPause();
+
+ host.notifyOnStop();
+ Mockito.verify(glue, times(1)).onHostStop();
+
+ PlaybackGlue glue2 = Mockito.spy(new PlaybackGlueImpl(context));
+ glue2.setHost(host);
+ Mockito.verify(glue, times(1)).onDetachedFromHost();
+ Mockito.verify(glue2, times(1)).onAttachedToHost(host);
+ assertSame(glue2, host.mGlue);
+ assertSame(host, glue2.getHost());
+ assertNull(glue.getHost());
+
+ host.notifyOnDestroy();
+ assertNull(glue2.getHost());
+ assertNull(host.mGlue);
+ Mockito.verify(glue2, times(1)).onDetachedFromHost();
+ }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
index 8a38894..7dab382 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
@@ -21,7 +21,6 @@
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
-
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
@@ -40,6 +39,7 @@
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerViewAccessibilityDelegate;
@@ -2988,4 +2988,193 @@
assertTrue(selectedPosition2 < selectedPosition1);
}
+ @Test
+ public void testAnimateOutResetByScrollTo() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear_with_button_onleft);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+ mGridView.getChildAt(0).getTop());
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ public void run() {
+ mGridView.animateOut();
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
+ }
+ });
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ public void run() {
+ mGridView.scrollToPosition(0);
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+ }
+ });
+
+ assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+ mGridView.getChildAt(0).getTop());
+ }
+
+ @Test
+ public void testAnimateOutResetByFocusChange() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear_with_button_onleft);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+ mGridView.getChildAt(0).getTop());
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ public void run() {
+ mGridView.animateOut();
+ mActivity.findViewById(R.id.button).requestFocus();
+ }
+ });
+ assertTrue(mActivity.findViewById(R.id.button).hasFocus());
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
+ }
+ });
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ public void run() {
+ mGridView.requestFocus();
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+ }
+ });
+
+ assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+ mGridView.getChildAt(0).getTop());
+ }
+
+ @Test
+ public void testHorizontalAnimateOutResetByScrollTo() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ assertEquals("First view is aligned with padding left", mGridView.getPaddingLeft(),
+ mGridView.getChildAt(0).getLeft());
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ public void run() {
+ mGridView.animateOut();
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getChildAt(0).getLeft() > mGridView.getPaddingLeft();
+ }
+ });
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ public void run() {
+ mGridView.scrollToPosition(0);
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+ }
+ });
+
+ assertEquals("First view is aligned with padding left", mGridView.getPaddingLeft(),
+ mGridView.getChildAt(0).getLeft());
+ }
+
+ @Test
+ public void testHorizontalAnimateOutRtl() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear_rtl);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ assertEquals("First view is aligned with padding right",
+ mGridView.getWidth() - mGridView.getPaddingRight(),
+ mGridView.getChildAt(0).getRight());
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ public void run() {
+ mGridView.animateOut();
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getChildAt(0).getRight()
+ < mGridView.getWidth() - mGridView.getPaddingRight();
+ }
+ });
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ public void run() {
+ mGridView.smoothScrollToPosition(0);
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+ }
+ });
+
+ assertEquals("First view is aligned with padding right",
+ mGridView.getWidth() - mGridView.getPaddingRight(),
+ mGridView.getChildAt(0).getRight());
+ }
+
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerActivity.java
new file mode 100644
index 0000000..11c4d3c
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerActivity.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 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.support.v17.leanback.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+
+
+public class TimePickerActivity extends Activity {
+
+ public static final String EXTRA_LAYOUT_RESOURCE_ID = "layoutResourceId";
+
+ int mLayoutId;
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mLayoutId = getIntent().getIntExtra(EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.timepicker_with_other_widgets);
+ setContentView(mLayoutId);
+ }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerTest.java
new file mode 100644
index 0000000..0132e10
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerTest.java
@@ -0,0 +1,489 @@
+/*
+ * Copyright (C) 2017 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.support.v17.leanback.widget;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.widget.picker.TimePicker;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class TimePickerTest {
+
+ private static final String TAG = "TimePickerTest";
+ private static final long TRANSITION_LENGTH = 1000;
+ private static final long UPDATE_LENGTH = 1000;
+
+
+ Context mContext;
+ View mViewAbove;
+ TimePicker mTimePicker12HourView;
+ TimePicker mTimePicker24HourView;
+ View mViewBelow;
+
+ @Rule
+ public ActivityTestRule<TimePickerActivity> mActivityTestRule =
+ new ActivityTestRule<>(TimePickerActivity.class, false, false);
+ private TimePickerActivity mActivity;
+
+ public void initActivity(Intent intent) throws Throwable {
+ mActivity = mActivityTestRule.launchActivity(intent);
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ mTimePicker12HourView = (TimePicker) mActivity.findViewById(R.id.time_picker12);
+ mTimePicker12HourView.setActivatedVisibleItemCount(3);
+ mTimePicker12HourView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mTimePicker12HourView.setActivated(!mTimePicker12HourView.isActivated());
+ }
+ });
+
+ if (intent.getIntExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.timepicker_with_other_widgets) == R.layout.timepicker_with_other_widgets) {
+ mViewAbove = mActivity.findViewById(R.id.above_picker);
+ mViewBelow = mActivity.findViewById(R.id.below_picker);
+ mTimePicker24HourView = (TimePicker) mActivity.findViewById(R.id.time_picker24);
+ mTimePicker24HourView.setActivatedVisibleItemCount(3);
+ mTimePicker24HourView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mTimePicker24HourView.setActivated(!mTimePicker24HourView.isActivated());
+ }
+ });
+ } else if (intent.getIntExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.timepicker_with_other_widgets) == R.layout.timepicker_alone) {
+ // A layout with only a TimePicker widget that is initially activated.
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker12HourView.setActivated(true);
+ }
+ });
+ Thread.sleep(500);
+ }
+ }
+
+ @Test
+ public void testSetHourIn24hFormat() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.timepicker_with_other_widgets);
+ initActivity(intent);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker24HourView.setHour(0);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker24HourView.getHour(), is(0));
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker24HourView.setHour(11);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker24HourView.getHour(), is(11));
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker24HourView.setHour(12);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker24HourView.getHour(), is(12));
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker24HourView.setHour(13);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker24HourView.getHour(), is(13));
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker24HourView.setHour(23);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker24HourView.getHour(), is(23));
+ }
+
+ @Test
+ public void testSetHourIn12hFormat() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.timepicker_with_other_widgets);
+ initActivity(intent);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker12HourView.setHour(0);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker12HourView.getHour(), is(0));
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker12HourView.setHour(11);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker12HourView.getHour(), is(11));
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker12HourView.setHour(12);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker12HourView.getHour(), is(12));
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker12HourView.setHour(13);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker12HourView.getHour(), is(13));
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker12HourView.setHour(23);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker12HourView.getHour(), is(23));
+ }
+
+ @Test
+ public void testSetMinuteIn24hFormat() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.timepicker_with_other_widgets);
+ initActivity(intent);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker24HourView.setMinute(0);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker24HourView.getMinute(), is(0));
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker24HourView.setMinute(11);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker24HourView.getMinute(), is(11));
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker24HourView.setMinute(59);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker24HourView.getMinute(), is(59));
+ }
+
+ @Test
+ public void testSetMinuteIn12hFormat() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.timepicker_with_other_widgets);
+ initActivity(intent);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker12HourView.setMinute(0);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker12HourView.getMinute(), is(0));
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker12HourView.setMinute(11);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker12HourView.getMinute(), is(11));
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker12HourView.setMinute(59);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+ mTimePicker12HourView.getMinute(), is(59));
+
+ }
+
+ @Test
+ public void testAmToPmTransition() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.timepicker_with_other_widgets);
+ initActivity(intent);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker12HourView.setHour(0);
+ mTimePicker12HourView.setMinute(47);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 12-hour mode returns a different hour in getHour()",
+ mTimePicker12HourView.getHour(), is(0));
+ assertThat("TimePicker in 12-hour mode returns a different hour in getMinute()",
+ mTimePicker12HourView.getMinute(), is(47));
+
+ // traverse to the AM/PM column of 12 hour TimePicker widget
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ Thread.sleep(TRANSITION_LENGTH);
+ // Click once to activate
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(TRANSITION_LENGTH);
+ sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+ Thread.sleep(TRANSITION_LENGTH);
+ sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+ Thread.sleep(TRANSITION_LENGTH);
+ // scroll down to PM value
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ Thread.sleep(TRANSITION_LENGTH);
+ // Click now to deactivate
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(TRANSITION_LENGTH);
+
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+ mTimePicker12HourView.getHour(), is(12));
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+ mTimePicker12HourView.getMinute(), is(47));
+ }
+
+ @Test
+ public void testPmToAmTransition() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.timepicker_with_other_widgets);
+ initActivity(intent);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker12HourView.setHour(12);
+ mTimePicker12HourView.setMinute(47);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker in 12-hour mode returns a different hour in getHour()",
+ mTimePicker12HourView.getHour(), is(12));
+ assertThat("TimePicker in 12-hour mode returns a different hour in getMinute()",
+ mTimePicker12HourView.getMinute(), is(47));
+
+ // traverse to the AM/PM column of 12 hour TimePicker widget
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ Thread.sleep(TRANSITION_LENGTH);
+ // Click once to activate
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(TRANSITION_LENGTH);
+ sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+ Thread.sleep(TRANSITION_LENGTH);
+ sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+ Thread.sleep(TRANSITION_LENGTH);
+ // scroll down to PM value
+ sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+ Thread.sleep(TRANSITION_LENGTH);
+ // Click now to deactivate
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(TRANSITION_LENGTH);
+
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+ mTimePicker12HourView.getHour(), is(0));
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+ mTimePicker12HourView.getMinute(), is(47));
+ }
+
+ @Test
+ public void test12To24HourFormatTransition() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.timepicker_with_other_widgets);
+ initActivity(intent);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker12HourView.setHour(14);
+ mTimePicker12HourView.setMinute(47);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker should be in 12-hour format.", mTimePicker12HourView.is24Hour(),
+ is(false));
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker12HourView.setIs24Hour(true);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker should now be in 24-hour format.", mTimePicker12HourView.is24Hour(),
+ is(true));
+ // The hour and minute should not be changed.
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+ mTimePicker12HourView.getHour(), is(14));
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+ mTimePicker12HourView.getMinute(), is(47));
+ }
+
+ @Test
+ public void test24To12HourFormatTransition() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.timepicker_with_other_widgets);
+ initActivity(intent);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker24HourView.setHour(14);
+ mTimePicker24HourView.setMinute(47);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker should be in 12-hour format.", mTimePicker24HourView.is24Hour(),
+ is(true));
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker24HourView.setIs24Hour(false);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+ assertThat("TimePicker should now be in 24-hour format.", mTimePicker24HourView.is24Hour(),
+ is(false));
+ // The hour and minute should not be changed.
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+ mTimePicker24HourView.getHour(), is(14));
+ assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+ mTimePicker24HourView.getMinute(), is(47));
+ }
+
+ @Test
+ public void testInitiallyActiveTimePicker()
+ throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.timepicker_alone);
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTimePicker12HourView.setHour(14);
+ mTimePicker12HourView.setMinute(47);
+ }
+ });
+ Thread.sleep(UPDATE_LENGTH);
+
+ ViewGroup mTimePickerInnerView = (ViewGroup) mTimePicker12HourView.findViewById(
+ R.id.picker);
+
+ assertThat("The first column of TimePicker should initially hold focus",
+ mTimePickerInnerView.getChildAt(0).hasFocus(), is(true));
+
+ // focus on first column
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ Thread.sleep(TRANSITION_LENGTH);
+ assertThat("The first column of TimePicker should still hold focus after scrolling down",
+ mTimePickerInnerView.getChildAt(0).hasFocus(), is(true));
+
+ // focus on second column
+ sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+ Thread.sleep(TRANSITION_LENGTH);
+ assertThat("The second column of TimePicker should hold focus after scrolling right",
+ mTimePickerInnerView.getChildAt(2).hasFocus(), is(true));
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ Thread.sleep(TRANSITION_LENGTH);
+ assertThat("The second column of TimePicker should still hold focus after scrolling down",
+ mTimePickerInnerView.getChildAt(2).hasFocus(), is(true));
+
+ // focus on third column
+ sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+ Thread.sleep(TRANSITION_LENGTH);
+ assertThat("The third column of TimePicker should hold focus after scrolling right",
+ mTimePickerInnerView.getChildAt(4).hasFocus(), is(true));
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+ Thread.sleep(TRANSITION_LENGTH);
+ assertThat("The third column of TimePicker should still hold focus after scrolling down",
+ mTimePickerInnerView.getChildAt(4).hasFocus(), is(true));
+ }
+
+ private void sendKeys(int ...keys) {
+ for (int i = 0; i < keys.length; i++) {
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+ }
+ }
+}
diff --git a/v17/leanback/tests/res/layout/horizontal_linear_rtl.xml b/v17/leanback/tests/res/layout/horizontal_linear_rtl.xml
new file mode 100644
index 0000000..bb11772
--- /dev/null
+++ b/v17/leanback/tests/res/layout/horizontal_linear_rtl.xml
@@ -0,0 +1,41 @@
+<!--
+ ~ Copyright (C) 2017 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
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:lb="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical"
+ android:layoutDirection="rtl"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ >
+ <android.support.v17.leanback.widget.HorizontalGridViewEx
+ android:id="@+id/gridview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:background="#00ffff"
+ android:horizontalSpacing="12dip"
+ android:verticalSpacing="24dip"
+ lb:numberOfColumns="1"
+ lb:columnWidth="150dip"
+ android:paddingBottom="12dip"
+ android:paddingLeft="12dip"
+ android:paddingRight="12dip"
+ android:paddingTop="12dip" />
+</LinearLayout>
diff --git a/v17/leanback/tests/res/layout/playback_controls.xml b/v17/leanback/tests/res/layout/playback_controls.xml
deleted file mode 100644
index 7f8910f..0000000
--- a/v17/leanback/tests/res/layout/playback_controls.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent" >
-
- <fragment
- android:id="@+id/playback_controls_fragment"
- android:name="android.support.v17.leanback.app.PlaybackTestFragment"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
-
-</FrameLayout>
\ No newline at end of file
diff --git a/v17/leanback/tests/res/layout/playback_controls_with_video.xml b/v17/leanback/tests/res/layout/playback_controls_with_video.xml
index cbf2a91..1850638 100644
--- a/v17/leanback/tests/res/layout/playback_controls_with_video.xml
+++ b/v17/leanback/tests/res/layout/playback_controls_with_video.xml
@@ -26,7 +26,7 @@
android:layout_gravity="center" />
<fragment
- android:id="@+id/playback_controls_fragment"
+ android:id="@+id/main_frame"
android:name="android.support.v17.leanback.app.PlaybackOverlayTestFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
diff --git a/v17/leanback/tests/res/layout/playback_support_controls.xml b/v17/leanback/tests/res/layout/playback_support_controls.xml
deleted file mode 100644
index 9e0e092..0000000
--- a/v17/leanback/tests/res/layout/playback_support_controls.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent" >
-
- <fragment
- android:id="@+id/playback_controls_fragment"
- android:name="android.support.v17.leanback.app.PlaybackSupportTestFragment"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
-
-</FrameLayout>
\ No newline at end of file
diff --git a/v17/leanback/tests/res/layout/rows.xml b/v17/leanback/tests/res/layout/single_fragment.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/rows.xml
rename to v17/leanback/tests/res/layout/single_fragment.xml
diff --git a/v17/leanback/tests/res/layout/timepicker_alone.xml b/v17/leanback/tests/res/layout/timepicker_alone.xml
new file mode 100644
index 0000000..2ca4484
--- /dev/null
+++ b/v17/leanback/tests/res/layout/timepicker_alone.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 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.
+-->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <android.support.v17.leanback.widget.picker.TimePicker
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/time_picker12"
+ android:importantForAccessibility="yes"
+ app:is24HourFormat="false"
+ app:useCurrentTime="true"
+ android:focusable="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true" />
+</RelativeLayout>
\ No newline at end of file
diff --git a/v17/leanback/tests/res/layout/timepicker_with_other_widgets.xml b/v17/leanback/tests/res/layout/timepicker_with_other_widgets.xml
new file mode 100644
index 0000000..677442a
--- /dev/null
+++ b/v17/leanback/tests/res/layout/timepicker_with_other_widgets.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 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.
+-->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <RelativeLayout android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center">
+ <TextView
+ android:id="@+id/above_picker"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Above Picker Some Text"
+ android:textAlignment="center"
+ android:focusable="true"
+ android:background="?android:attr/selectableItemBackground"
+ />
+ <android.support.v17.leanback.widget.picker.TimePicker
+ android:id="@+id/time_picker12"
+ android:importantForAccessibility="yes"
+ app:is24HourFormat="false"
+ app:useCurrentTime="true"
+ android:focusable="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/above_picker"
+ android:gravity="center" />
+ <android.support.v17.leanback.widget.picker.TimePicker
+ android:id="@+id/time_picker24"
+ android:importantForAccessibility="yes"
+ app:is24HourFormat="true"
+ app:useCurrentTime="true"
+ android:focusable="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/time_picker12"
+ android:gravity="center" />
+ <TextView
+ android:id="@+id/below_picker"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Below Picker Some Text"
+ android:textAlignment="center"
+ android:layout_below="@id/time_picker24"
+ android:focusable="true"
+ android:background="?android:attr/selectableItemBackground"/>
+ </RelativeLayout>
+</RelativeLayout>
diff --git a/v17/leanback/tests/res/raw/track_01.mp3 b/v17/leanback/tests/res/raw/track_01.mp3
new file mode 100755
index 0000000..9762383
--- /dev/null
+++ b/v17/leanback/tests/res/raw/track_01.mp3
Binary files differ
diff --git a/v17/leanback/tests/res/raw/video.mp4 b/v17/leanback/tests/res/raw/video.mp4
new file mode 100644
index 0000000..3f709fb
--- /dev/null
+++ b/v17/leanback/tests/res/raw/video.mp4
Binary files differ
diff --git a/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
index 66633b2..64df96c 100644
--- a/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
@@ -511,7 +511,7 @@
int count = 0;
while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
final int pos = layoutState.mCurrentPosition;
- layoutPrefetchRegistry.addPosition(pos, layoutState.mScrollingOffset);
+ layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset));
final int spanSize = mSpanSizeLookup.getSpanSize(pos);
remainingSpan -= spanSize;
layoutState.mCurrentPosition += layoutState.mItemDirection;
@@ -693,7 +693,7 @@
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
- result.mFocusable |= view.isFocusable();
+ result.mFocusable |= view.hasFocusable();
}
Arrays.fill(mSet, null);
}
@@ -1048,7 +1048,7 @@
break;
}
- if (candidate.isFocusable() && spanGroupIndex != focusableSpanGroupIndex) {
+ if (candidate.hasFocusable() && spanGroupIndex != focusableSpanGroupIndex) {
// We are past the allowable span group index for the next focusable item.
// The search only continues if no focusable weak candidates have been found up
// until this point, in order to find the best unfocusable candidate to become
@@ -1062,19 +1062,19 @@
final LayoutParams candidateLp = (LayoutParams) candidate.getLayoutParams();
final int candidateStart = candidateLp.mSpanIndex;
final int candidateEnd = candidateLp.mSpanIndex + candidateLp.mSpanSize;
- if (candidate.isFocusable() && candidateStart == prevSpanStart
+ if (candidate.hasFocusable() && candidateStart == prevSpanStart
&& candidateEnd == prevSpanEnd) {
return candidate; // perfect match
}
boolean assignAsWeek = false;
- if ((candidate.isFocusable() && focusableWeakCandidate == null)
- || (!candidate.isFocusable() && unfocusableWeakCandidate == null)) {
+ if ((candidate.hasFocusable() && focusableWeakCandidate == null)
+ || (!candidate.hasFocusable() && unfocusableWeakCandidate == null)) {
assignAsWeek = true;
} else {
int maxStart = Math.max(candidateStart, prevSpanStart);
int minEnd = Math.min(candidateEnd, prevSpanEnd);
int overlap = minEnd - maxStart;
- if (candidate.isFocusable()) {
+ if (candidate.hasFocusable()) {
if (overlap > focusableWeakCandidateOverlap) {
assignAsWeek = true;
} else if (overlap == focusableWeakCandidateOverlap
@@ -1095,7 +1095,7 @@
}
if (assignAsWeek) {
- if (candidate.isFocusable()) {
+ if (candidate.hasFocusable()) {
focusableWeakCandidate = candidate;
focusableWeakCandidateSpanIndex = candidateLp.mSpanIndex;
focusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd)
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
index c7840fb..ac14206 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
@@ -1202,7 +1202,7 @@
LayoutPrefetchRegistry layoutPrefetchRegistry) {
final int pos = layoutState.mCurrentPosition;
if (pos >= 0 && pos < state.getItemCount()) {
- layoutPrefetchRegistry.addPosition(pos, layoutState.mScrollingOffset);
+ layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset));
}
}
@@ -1607,7 +1607,7 @@
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
- result.mFocusable = view.isFocusable();
+ result.mFocusable = view.hasFocusable();
}
@Override
@@ -1992,7 +1992,7 @@
} else {
nextFocus = getChildClosestToEnd();
}
- if (nextFocus.isFocusable()) {
+ if (nextFocus.hasFocusable()) {
if (nextCandidate == null) {
return null;
}
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index 917b2eb..5f06408 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -2334,11 +2334,17 @@
}
}
if (result != null && !result.hasFocusable()) {
+ if (getFocusedChild() == null) {
+ // Scrolling to this unfocusable view is not meaningful since there is no currently
+ // focused view which RV needs to keep visible.
+ return super.focusSearch(focused, direction);
+ }
// If the next view returned by onFocusSearchFailed in layout manager has no focusable
// views, we still scroll to that view in order to make it visible on the screen.
// If it's focusable, framework already calls RV's requestChildFocus which handles
// bringing this newly focused item onto the screen.
requestChildOnScreen(result, null);
+ return focused;
}
return isPreferredNextFocus(focused, result, direction)
? result : super.focusSearch(focused, direction);
@@ -9206,11 +9212,14 @@
* @param parent The parent RecyclerView.
* @param dx The scrolling in x-axis direction to be performed.
* @param dy The scrolling in y-axis direction to be performed.
- * @return Whether after the given scrolling, the currently focused item in still visible
- * (within RV's bounds).
+ * @return {@code false} if the focused child is not at least partially visible after
+ * scrolling or no focused child exists, {@code true} otherwise.
*/
private boolean isFocusedChildVisibleAfterScrolling(RecyclerView parent, int dx, int dy) {
final View focusedChild = parent.getFocusedChild();
+ if (focusedChild == null) {
+ return false;
+ }
final int parentLeft = getPaddingLeft();
final int parentTop = getPaddingTop();
final int parentRight = getWidth() - getPaddingRight();
diff --git a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
index 4592fe1..bbf4acd 100644
--- a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -1672,7 +1672,7 @@
updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine);
}
recycle(recycler, mLayoutState);
- if (mLayoutState.mStopInFocusable && view.isFocusable()) {
+ if (mLayoutState.mStopInFocusable && view.hasFocusable()) {
if (lp.mFullSpan) {
mRemainingSpans.clear();
} else {
@@ -2772,7 +2772,7 @@
|| (!mReverseLayout && getPosition(view) >= referenceChildPosition)) {
break;
}
- if (view.isFocusable()) {
+ if (view.hasFocusable()) {
candidate = view;
} else {
break;
@@ -2785,7 +2785,7 @@
|| (!mReverseLayout && getPosition(view) <= referenceChildPosition)) {
break;
}
- if (view.isFocusable()) {
+ if (view.hasFocusable()) {
candidate = view;
} else {
break;
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
index 067689c..2da67af 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
@@ -21,7 +21,9 @@
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
@@ -43,6 +45,8 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@@ -399,6 +403,108 @@
}
@Test
+ public void unfocusableScrollingWhenFocusCleared() throws Throwable {
+ // The maximum number of child views that can be visible at any time.
+ final int visibleChildCount = 5;
+ final int consecutiveFocusablesCount = 2;
+ final int consecutiveUnFocusablesCount = 18;
+ final TestAdapter adapter = new TestAdapter(
+ consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
+ RecyclerView mAttachedRv;
+
+ @Override
+ public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+ // Good to have colors for debugging
+ StateListDrawable stl = new StateListDrawable();
+ stl.addState(new int[]{android.R.attr.state_focused},
+ new ColorDrawable(Color.RED));
+ stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+ //noinspection deprecation used to support kitkat tests
+ testViewHolder.itemView.setBackgroundDrawable(stl);
+ return testViewHolder;
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+ mAttachedRv = recyclerView;
+ }
+
+ @Override
+ public void onBindViewHolder(TestViewHolder holder,
+ int position) {
+ super.onBindViewHolder(holder, position);
+ if (position < consecutiveFocusablesCount) {
+ holder.itemView.setFocusable(true);
+ holder.itemView.setFocusableInTouchMode(true);
+ } else {
+ holder.itemView.setFocusable(false);
+ holder.itemView.setFocusableInTouchMode(false);
+ }
+ // This height ensures that some portion of #visibleChildCount'th child is
+ // off-bounds, creating more interesting test scenario.
+ holder.itemView.setMinimumHeight((mAttachedRv.getHeight()
+ + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount);
+ }
+ };
+ setupByConfig(new Config(VERTICAL, false, false).adapter(adapter), false);
+ waitForFirstLayout();
+
+ // adapter position of the currently focused item.
+ int focusIndex = 0;
+ View newFocused = mRecyclerView.getChildAt(focusIndex);
+ requestFocus(newFocused, true);
+ RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
+ focusIndex);
+ assertThat("Child at position " + focusIndex + " should be focused",
+ toFocus.itemView.hasFocus(), is(true));
+
+ final View nextView = focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_DOWN, true);
+ focusIndex++;
+ assertThat("Child at position " + focusIndex + " should be focused",
+ mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView.hasFocus(),
+ is(true));
+ final CountDownLatch focusLatch = new CountDownLatch(1);
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ nextView.setOnFocusChangeListener(new View.OnFocusChangeListener(){
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ assertNull("Focus just got cleared and no children should be holding"
+ + " focus now.", mRecyclerView.getFocusedChild());
+ try {
+ // Calling focusSearch should be a no-op here since even though there
+ // are unfocusable views down to scroll to, none of RV's children hold
+ // focus at this stage.
+ View focusedChild = focusSearch(v, View.FOCUS_DOWN, true);
+ assertNull("Calling focusSearch should be no-op when no children hold"
+ + "focus", focusedChild);
+ // No scrolling should have happened, so any unfocusables that were
+ // invisible should still be invisible.
+ RecyclerView.ViewHolder unforcusablePartiallyVisibleChild =
+ mRecyclerView.findViewHolderForAdapterPosition(
+ visibleChildCount - 1);
+ assertFalse("Child view at adapter pos " + (visibleChildCount - 1)
+ + " should not be fully visible.",
+ isViewFullyInBound(mRecyclerView,
+ unforcusablePartiallyVisibleChild.itemView));
+ } catch (Throwable t) {
+ postExceptionToInstrumentation(t);
+ }
+ }
+ });
+ nextView.clearFocus();
+ focusLatch.countDown();
+ }
+ });
+ assertTrue(focusLatch.await(2, TimeUnit.SECONDS));
+ assertThat("Child at position " + focusIndex + " should no longer be focused",
+ mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView.hasFocus(),
+ is(false));
+ }
+
+ @Test
public void removeAnchorItem() throws Throwable {
removeAnchorItemTest(
new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout(