diff options
author | Maurice Lam <yukl@google.com> | 2017-05-17 17:05:43 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2017-05-17 17:05:45 +0000 |
commit | 7077ee929c2fac8d52fa3b9a9f24020b443261f1 (patch) | |
tree | 99e2d4387dcf98f9e9bbafa0dd8f746e0176889c /library | |
parent | f25c6a25b66684b046486527af57ded9757de2c7 (diff) | |
parent | 9395f90b26e072b2748db315aaffef27c257a82d (diff) | |
download | setupwizard-7077ee929c2fac8d52fa3b9a9f24020b443261f1.tar.gz |
Merge "Add IllustrationVideoView to setup wizard library"
Diffstat (limited to 'library')
3 files changed, 412 insertions, 0 deletions
diff --git a/library/main/res/values/attrs.xml b/library/main/res/values/attrs.xml index ec6489e..36d5fb7 100644 --- a/library/main/res/values/attrs.xml +++ b/library/main/res/values/attrs.xml @@ -93,6 +93,10 @@ <attr name="suwHeader" /> </declare-styleable> + <declare-styleable name="SuwIllustrationVideoView"> + <attr name="suwVideo" format="reference" /> + </declare-styleable> + <declare-styleable name="SuwGlifLayout"> <attr name="suwBackgroundPatterned" format="boolean" /> <attr name="suwBackgroundBaseColor" format="color" /> diff --git a/library/main/src/com/android/setupwizardlib/view/IllustrationVideoView.java b/library/main/src/com/android/setupwizardlib/view/IllustrationVideoView.java new file mode 100644 index 0000000..989f3e6 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/view/IllustrationVideoView.java @@ -0,0 +1,230 @@ +/* + * 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 com.android.setupwizardlib.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.SurfaceTexture; +import android.graphics.drawable.Animatable; +import android.media.MediaPlayer; +import android.os.Build.VERSION_CODES; +import android.support.annotation.RawRes; +import android.support.annotation.VisibleForTesting; +import android.util.AttributeSet; +import android.view.Surface; +import android.view.TextureView; +import android.view.View; + +import com.android.setupwizardlib.R; + +/** + * A view for displaying videos in a continuous loop (without audio). This is typically used for + * animated illustrations. + * + * <p>The video can be specified using {@code app:suwVideo}, specifying the raw resource to the mp4 + * video. Optionally, {@code app:suwLoopStartMs} can be used to specify which part of the video it + * should loop back to + * + * <p>For optimal file size, use avconv or other video compression tool to remove the unused audio + * track and reduce the size of your video asset: + * avconv -i [input file] -vcodec h264 -crf 20 -an [output_file] + */ +@TargetApi(VERSION_CODES.ICE_CREAM_SANDWICH) +public class IllustrationVideoView extends TextureView implements Animatable, + TextureView.SurfaceTextureListener, + MediaPlayer.OnPreparedListener, + MediaPlayer.OnSeekCompleteListener, + MediaPlayer.OnInfoListener { + + protected float mAspectRatio = 1.0f; // initial guess until we know + + @VisibleForTesting MediaPlayer mMediaPlayer; + + private @RawRes int mVideoResId = 0; + + @VisibleForTesting Surface mSurface; + + public IllustrationVideoView(Context context, AttributeSet attrs) { + super(context, attrs); + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.SuwIllustrationVideoView); + mVideoResId = a.getResourceId(R.styleable.SuwIllustrationVideoView_suwVideo, 0); + a.recycle(); + + setSurfaceTextureListener(this); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + + if (height < width * mAspectRatio) { + // Height constraint is tighter. Need to scale down the width to fit aspect ratio. + width = (int) (height / mAspectRatio); + } else { + // Width constraint is tighter. Need to scale down the height to fit aspect ratio. + height = (int) (width * mAspectRatio); + } + + super.onMeasure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + } + + /** + * Set the video to be played by this view. + * + * @param resId Resource ID of the video, typically an MP4 under res/raw. + */ + public void setVideoResource(@RawRes int resId) { + if (resId != mVideoResId) { + mVideoResId = resId; + createMediaPlayer(); + } + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + if (hasWindowFocus) { + start(); + } else { + stop(); + } + } + + /** + * Creates a media player for the current URI. The media player will be started immediately if + * the view's window is visible. If there is an existing media player, it will be released. + */ + private void createMediaPlayer() { + if (mMediaPlayer != null) { + mMediaPlayer.release(); + } + if (mSurface == null || mVideoResId == 0) { + return; + } + + mMediaPlayer = MediaPlayer.create(getContext(), mVideoResId); + + mMediaPlayer.setSurface(mSurface); + mMediaPlayer.setOnPreparedListener(this); + mMediaPlayer.setOnSeekCompleteListener(this); + mMediaPlayer.setOnInfoListener(this); + + float aspectRatio = (float) mMediaPlayer.getVideoHeight() / mMediaPlayer.getVideoWidth(); + if (mAspectRatio != aspectRatio) { + mAspectRatio = aspectRatio; + requestLayout(); + } + if (getWindowVisibility() == View.VISIBLE) { + start(); + } + } + + /** + * Whether the media player should play the video in a continuous loop. The default value is + * true. + */ + protected boolean shouldLoop() { + return true; + } + + /** + * Release any resources used by this view. This is automatically called in + * onSurfaceTextureDestroyed so in most cases you don't have to call this. + */ + public void release() { + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + if (mSurface != null) { + mSurface.release(); + mSurface = null; + } + } + + /* SurfaceTextureListener methods */ + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { + // Keep the view hidden until video starts + setVisibility(View.INVISIBLE); + mSurface = new Surface(surfaceTexture); + createMediaPlayer(); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + release(); + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + } + + /* Animatable methods */ + + @Override + public void start() { + if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) { + mMediaPlayer.start(); + } + } + + @Override + public void stop() { + if (mMediaPlayer != null) { + mMediaPlayer.pause(); + } + } + + @Override + public boolean isRunning() { + return mMediaPlayer != null && mMediaPlayer.isPlaying(); + } + + /* MediaPlayer callbacks */ + + @Override + public boolean onInfo(MediaPlayer mp, int what, int extra) { + if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) { + // Video available, show view now + setVisibility(View.VISIBLE); + } + return false; + } + + @Override + public void onPrepared(MediaPlayer mp) { + mp.setLooping(shouldLoop()); + } + + @Override + public void onSeekComplete(MediaPlayer mp) { + mp.start(); + } +} diff --git a/library/test/robotest/src/com/android/setupwizardlib/view/IllustrationVideoViewTest.java b/library/test/robotest/src/com/android/setupwizardlib/view/IllustrationVideoViewTest.java new file mode 100644 index 0000000..ffa228d --- /dev/null +++ b/library/test/robotest/src/com/android/setupwizardlib/view/IllustrationVideoViewTest.java @@ -0,0 +1,178 @@ +/* + * 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 com.android.setupwizardlib.view; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.robolectric.RuntimeEnvironment.application; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.media.MediaPlayer; +import android.os.Build.VERSION_CODES; +import android.support.annotation.RawRes; +import android.view.Surface; + +import com.android.setupwizardlib.BuildConfig; +import com.android.setupwizardlib.R; +import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner; +import com.android.setupwizardlib.view.IllustrationVideoViewTest.ShadowMockMediaPlayer; +import com.android.setupwizardlib.view.IllustrationVideoViewTest.ShadowSurface; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.internal.Shadow; +import org.robolectric.shadows.ShadowMediaPlayer; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(SuwLibRobolectricTestRunner.class) +@Config( + constants = BuildConfig.class, + sdk = Config.NEWEST_SDK, + shadows = { + ShadowMockMediaPlayer.class, + ShadowSurface.class + }) +public class IllustrationVideoViewTest { + + @Mock + private SurfaceTexture mSurfaceTexture; + + private IllustrationVideoView mView; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @After + public void tearDown() { + ShadowMockMediaPlayer.reset(); + } + + @Test + public void testPausedWhenWindowFocusLost() { + createDefaultView(); + mView.start(); + + assertNotNull(mView.mMediaPlayer); + assertNotNull(mView.mSurface); + + mView.onWindowFocusChanged(false); + verify(ShadowMockMediaPlayer.getMock()).pause(); + } + + @Test + public void testStartedWhenWindowFocusRegained() { + testPausedWhenWindowFocusLost(); + + // Clear verifications for calls in the other test + reset(ShadowMockMediaPlayer.getMock()); + + mView.onWindowFocusChanged(true); + verify(ShadowMockMediaPlayer.getMock()).start(); + } + + @Test + public void testSurfaceReleasedWhenTextureDestroyed() { + createDefaultView(); + mView.start(); + + assertNotNull(mView.mMediaPlayer); + assertNotNull(mView.mSurface); + + mView.onSurfaceTextureDestroyed(mSurfaceTexture); + verify(ShadowMockMediaPlayer.getMock()).release(); + } + + @Test + public void testXmlSetVideoResId() { + createDefaultView(); + assertEquals(android.R.color.white, ShadowMockMediaPlayer.sResId); + } + + @Test + public void testSetVideoResId() { + createDefaultView(); + + @RawRes int black = android.R.color.black; + mView.setVideoResource(black); + + assertEquals(android.R.color.black, ShadowMockMediaPlayer.sResId); + } + + private void createDefaultView() { + mView = new IllustrationVideoView( + application, + Robolectric.buildAttributeSet() + // Any resource attribute should work, since the media player is mocked + .addAttribute(R.attr.suwVideo, "@android:color/white") + .build()); + mView.onSurfaceTextureAvailable(mSurfaceTexture, 500, 500); + } + + @Implements(MediaPlayer.class) + public static class ShadowMockMediaPlayer extends ShadowMediaPlayer { + + private static MediaPlayer sMediaPlayer = mock(MediaPlayer.class); + private static int sResId; + + public static void reset() { + sMediaPlayer = mock(MediaPlayer.class); + sResId = 0; + } + + @Implementation + public static MediaPlayer create(Context context, int resId) { + sResId = resId; + return sMediaPlayer; + } + + public static MediaPlayer getMock() { + return sMediaPlayer; + } + } + + @Implements(Surface.class) + @TargetApi(VERSION_CODES.HONEYCOMB) + public static class ShadowSurface extends org.robolectric.shadows.ShadowSurface { + + @RealObject + private Surface mRealSurface; + + public void __constructor__(SurfaceTexture surfaceTexture) { + // Call the constructor on the real object, so that critical fields such as mLock is + // initialized properly. + Shadow.invokeConstructor(Surface.class, mRealSurface, + ReflectionHelpers.ClassParameter.from(SurfaceTexture.class, surfaceTexture)); + super.__constructor__(surfaceTexture); + } + } +} |