Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 34721d3f authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add AnimatedVectorDrawable support for VideoPreference"

parents c2e4ae1c 953da2ee
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -119,6 +119,7 @@
    <declare-styleable name="VideoPreference">
        <attr name="animation" format="reference" />
        <attr name="preview" format="reference" />
        <attr name="vectorAnimation" format="reference" />
    </declare-styleable>

    <!-- For AspectRatioFrameLayout -->
+149 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.widget;

import android.content.ContentResolver;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.media.MediaPlayer;
import android.net.Uri;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;

/**
 * A {@link VideoPreference.AnimationController} containing a {@link
 * MediaPlayer}. The controller is used by {@link VideoPreference} to display
 * a mp4 resource.
 */
class MediaAnimationController implements VideoPreference.AnimationController {
    private MediaPlayer mMediaPlayer;
    private boolean mVideoReady;

    MediaAnimationController(Context context, int videoId) {
        final Uri videoPath = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
                .authority(context.getPackageName())
                .appendPath(String.valueOf(videoId))
                .build();
        mMediaPlayer = MediaPlayer.create(context, videoPath);
        // when the playback res is invalid or others, MediaPlayer create may fail
        // and return null, so need add the null judgement.
        if (mMediaPlayer != null) {
            mMediaPlayer.seekTo(0);
            mMediaPlayer.setOnSeekCompleteListener(mp -> mVideoReady = true);
            mMediaPlayer.setOnPreparedListener(mediaPlayer -> mediaPlayer.setLooping(true));
        }
    }

    @Override
    public int getVideoWidth() {
        return mMediaPlayer.getVideoWidth();
    }

    @Override
    public int getVideoHeight() {
        return mMediaPlayer.getVideoHeight();
    }

    @Override
    public void pause() {
        mMediaPlayer.pause();
    }

    @Override
    public void start() {
        mMediaPlayer.start();
    }

    @Override
    public boolean isPlaying() {
        return mMediaPlayer.isPlaying();
    }

    @Override
    public int getDuration() {
        return mMediaPlayer.getDuration();
    }

    @Override
    public void attachView(TextureView video, View preview, View playButton) {
        updateViewStates(preview, playButton);
        video.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width,
                    int height) {
                if (mMediaPlayer != null) {
                    final Surface surface = new Surface(surfaceTexture);
                    mMediaPlayer.setSurface(surface);
                }
            }

            @Override
            public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width,
                    int height) {
            }

            @Override
            public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
                preview.setVisibility(View.VISIBLE);
                return false;
            }

            @Override
            public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
                if (mVideoReady) {
                    if (preview.getVisibility() == View.VISIBLE) {
                        preview.setVisibility(View.GONE);
                    }
                    if (mMediaPlayer != null
                            && !mMediaPlayer.isPlaying()) {
                        mMediaPlayer.start();
                        playButton.setVisibility(View.GONE);
                    }
                }
                if (mMediaPlayer != null && !mMediaPlayer.isPlaying()
                        && playButton.getVisibility() != View.VISIBLE) {
                    playButton.setVisibility(View.VISIBLE);
                }
            }
        });
        video.setOnClickListener(v -> updateViewStates(preview, playButton));
    }

    @Override
    public void release() {
        if (mMediaPlayer != null) {
            mMediaPlayer.stop();
            mMediaPlayer.reset();
            mMediaPlayer.release();
            mMediaPlayer = null;
            mVideoReady = false;
        }
    }

    private void updateViewStates(View imageView, View playButton) {
        if (mMediaPlayer.isPlaying()) {
            mMediaPlayer.pause();
            playButton.setVisibility(View.VISIBLE);
            imageView.setVisibility(View.VISIBLE);
        } else {
            imageView.setVisibility(View.GONE);
            playButton.setVisibility(View.GONE);
            mMediaPlayer.start();
        }
    }
}
+111 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.widget;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.TextureView;
import android.view.View;

import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;

/**
 * A {@link VideoPreference.AnimationController} containing a {@link
 * AnimatedVectorDrawableCompat}. The controller is used by {@link VideoPreference}
 * to display AnimatedVectorDrawable content.
 */
class VectorAnimationController implements VideoPreference.AnimationController {
    private AnimatedVectorDrawableCompat mAnimatedVectorDrawableCompat;
    private Drawable mPreviewDrawable;
    private Animatable2Compat.AnimationCallback mAnimationCallback;

    /**
     * Called by a preference panel fragment to finish itself.
     *
     * @param context Application Context
     * @param animationId An {@link android.graphics.drawable.AnimationDrawable} resource id
     */
    VectorAnimationController(Context context, int animationId) {
        mAnimatedVectorDrawableCompat = AnimatedVectorDrawableCompat.create(context, animationId);
        mAnimationCallback = new Animatable2Compat.AnimationCallback() {
            @Override
            public void onAnimationEnd(Drawable drawable) {
                mAnimatedVectorDrawableCompat.start();
            }
        };
    }

    @Override
    public int getVideoWidth() {
        return mAnimatedVectorDrawableCompat.getIntrinsicWidth();
    }

    @Override
    public int getVideoHeight() {
        return mAnimatedVectorDrawableCompat.getIntrinsicHeight();
    }

    @Override
    public void pause() {
        mAnimatedVectorDrawableCompat.stop();
    }

    @Override
    public void start() {
        mAnimatedVectorDrawableCompat.start();
    }

    @Override
    public boolean isPlaying() {
        return mAnimatedVectorDrawableCompat.isRunning();
    }

    @Override
    public int getDuration() {
        // We can't get duration from AnimatedVectorDrawable, just return a non zero value.
        return 5000;
    }

    @Override
    public void attachView(TextureView video, View preview, View playButton) {
        mPreviewDrawable = preview.getForeground();
        video.setVisibility(View.GONE);
        updateViewStates(preview, playButton);
        preview.setOnClickListener(v -> updateViewStates(preview, playButton));
    }

    @Override
    public void release() {
        mAnimatedVectorDrawableCompat.stop();
        mAnimatedVectorDrawableCompat.clearAnimationCallbacks();
    }

    private void updateViewStates(View imageView, View playButton) {
        if (mAnimatedVectorDrawableCompat.isRunning()) {
            mAnimatedVectorDrawableCompat.stop();
            mAnimatedVectorDrawableCompat.clearAnimationCallbacks();
            playButton.setVisibility(View.VISIBLE);
            imageView.setForeground(mPreviewDrawable);
        } else {
            playButton.setVisibility(View.GONE);
            imageView.setForeground(mAnimatedVectorDrawableCompat);
            mAnimatedVectorDrawableCompat.start();
            mAnimatedVectorDrawableCompat.registerAnimationCallback(mAnimationCallback);
        }
    }
}
+81 −116
Original line number Diff line number Diff line
@@ -16,16 +16,11 @@

package com.android.settings.widget;

import android.content.ContentResolver;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.SurfaceTexture;
import android.media.MediaPlayer;
import android.net.Uri;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.ImageView;
@@ -34,30 +29,27 @@ import android.widget.LinearLayout;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;

import com.android.settings.R;

/**
 * A full width preference that hosts a MP4 video.
 * A full width preference that hosts a MP4 video or a {@link AnimatedVectorDrawableCompat}.
 */
public class VideoPreference extends Preference {

    private static final String TAG = "VideoPreference";
    private final Context mContext;

    private Uri mVideoPath;
    @VisibleForTesting
    MediaPlayer mMediaPlayer;
    AnimationController mAnimationController;
    @VisibleForTesting
    boolean mAnimationAvailable;
    @VisibleForTesting
    boolean mVideoReady;
    private boolean mVideoPaused;
    private float mAspectRatio = 1.0f;
    private int mPreviewResource;
    private boolean mViewVisible;
    private Surface mSurface;
    private int mPreviewId;
    private int mAnimationId;
    private int mVectorAnimationId;
    private int mHeight = LinearLayout.LayoutParams.MATCH_PARENT - 1; // video height in pixels

    public VideoPreference(Context context) {
@@ -84,19 +76,17 @@ public class VideoPreference extends Preference {
            mAnimationId = mAnimationId == 0
                ? attributes.getResourceId(R.styleable.VideoPreference_animation, 0)
                : mAnimationId;
            mVideoPath = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
                    .authority(context.getPackageName())
                    .appendPath(String.valueOf(mAnimationId))
                    .build();
            mPreviewResource = mPreviewResource == 0
            mPreviewId = mPreviewId == 0
                    ? attributes.getResourceId(R.styleable.VideoPreference_preview, 0)
                : mPreviewResource;
            if (mPreviewResource == 0 && mAnimationId == 0) {
                    : mPreviewId;
            mVectorAnimationId = attributes.getResourceId(
                    R.styleable.VideoPreference_vectorAnimation, 0);
            if (mPreviewId == 0 && mAnimationId == 0 && mVectorAnimationId == 0) {
                setVisible(false);
                return;
            }
            initMediaPlayer();
            if (mMediaPlayer != null && mMediaPlayer.getDuration() > 0) {
            initAnimationController();
            if (mAnimationController != null && mAnimationController.getDuration() > 0) {
                setVisible(true);
                setLayoutResource(R.layout.video_preference);
                mAnimationAvailable = true;
@@ -120,96 +110,33 @@ public class VideoPreference extends Preference {
        }

        final TextureView video = (TextureView) holder.findViewById(R.id.video_texture_view);
        final ImageView imageView = (ImageView) holder.findViewById(R.id.video_preview_image);
        final ImageView previewImage = (ImageView) holder.findViewById(R.id.video_preview_image);
        final ImageView playButton = (ImageView) holder.findViewById(R.id.video_play_button);
        final AspectRatioFrameLayout layout = (AspectRatioFrameLayout) holder.findViewById(
                R.id.video_container);

        imageView.setImageResource(mPreviewResource);
        previewImage.setImageResource(mPreviewId);
        layout.setAspectRatio(mAspectRatio);
        if (mHeight >= LinearLayout.LayoutParams.MATCH_PARENT) {
            layout.setLayoutParams(new LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.MATCH_PARENT, mHeight));
        }
        updateViewStates(imageView, playButton);

        video.setOnClickListener(v -> updateViewStates(imageView, playButton));

        video.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width,
                    int height) {
                if (mMediaPlayer != null) {
                    mSurface = new Surface(surfaceTexture);
                    mMediaPlayer.setSurface(mSurface);
                }
            }

            @Override
            public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width,
                    int height) {
            }

            @Override
            public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
                imageView.setVisibility(View.VISIBLE);
                return false;
            }

            @Override
            public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
                if (!mViewVisible) {
                    return;
                }
                if (mVideoReady) {
                    if (imageView.getVisibility() == View.VISIBLE) {
                        imageView.setVisibility(View.GONE);
                    }
                    if (!mVideoPaused && mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
                        mMediaPlayer.start();
                        playButton.setVisibility(View.GONE);
                    }
                }
                if (mMediaPlayer != null && !mMediaPlayer.isPlaying() &&
                        playButton.getVisibility() != View.VISIBLE) {
                    playButton.setVisibility(View.VISIBLE);
                }
            }
        });
    }

    @VisibleForTesting
    void updateViewStates(ImageView imageView, ImageView playButton) {
        if (mMediaPlayer != null) {
            if (mMediaPlayer.isPlaying()) {
                mMediaPlayer.pause();
                playButton.setVisibility(View.VISIBLE);
                imageView.setVisibility(View.VISIBLE);
                mVideoPaused = true;
            } else {
                imageView.setVisibility(View.GONE);
                playButton.setVisibility(View.GONE);
                mMediaPlayer.start();
                mVideoPaused = false;
            }
        }
        mAnimationController.attachView(video, previewImage, playButton);
    }

    @Override
    public void onDetached() {
        releaseMediaPlayer();
        releaseAnimationController();
        super.onDetached();
    }

    public void onViewVisible(boolean videoPaused) {
        mViewVisible = true;
        mVideoPaused = videoPaused;
        initMediaPlayer();
        initAnimationController();
    }

    public void onViewInvisible() {
        mViewVisible = false;
        releaseMediaPlayer();
        releaseAnimationController();
    }

    /**
@@ -221,34 +148,25 @@ public class VideoPreference extends Preference {
     */
    public void setVideo(int videoId, int previewId) {
        mAnimationId = videoId;
        mPreviewResource = previewId;
        releaseMediaPlayer();
        mPreviewId = previewId;
        releaseAnimationController();
        initialize(mContext, null);
    }

    private void initMediaPlayer() {
        if (mMediaPlayer == null) {
            mMediaPlayer = MediaPlayer.create(mContext, mVideoPath);
            // when the playback res is invalid or others, MediaPlayer create may fail
            // and return null, so need add the null judgement.
            if (mMediaPlayer != null) {
                mMediaPlayer.seekTo(0);
                mMediaPlayer.setOnSeekCompleteListener(mp -> mVideoReady = true);
                mMediaPlayer.setOnPreparedListener(mediaPlayer -> mediaPlayer.setLooping(true));
                if (mSurface != null) {
                    mMediaPlayer.setSurface(mSurface);
                }
    private void initAnimationController() {
        if (mVectorAnimationId != 0) {
            mAnimationController = new VectorAnimationController(mContext, mVectorAnimationId);
            return;
        }
        if (mAnimationId != 0) {
            mAnimationController = new MediaAnimationController(mContext, mAnimationId);
        }
    }

    private void releaseMediaPlayer() {
        if (mMediaPlayer != null) {
            mMediaPlayer.stop();
            mMediaPlayer.reset();
            mMediaPlayer.release();
            mMediaPlayer = null;
            mVideoReady = false;
    private void releaseAnimationController() {
        if (mAnimationController != null) {
            mAnimationController.release();
            mAnimationController = null;
        }
    }

@@ -262,6 +180,7 @@ public class VideoPreference extends Preference {

    /**
     * sets the height of the video preference
     *
     * @param height in dp
     */
    public void setHeight(float height) {
@@ -271,6 +190,52 @@ public class VideoPreference extends Preference {

    @VisibleForTesting
    void updateAspectRatio() {
        mAspectRatio = mMediaPlayer.getVideoWidth() / (float) mMediaPlayer.getVideoHeight();
        mAspectRatio = mAnimationController.getVideoWidth()
                / (float) mAnimationController.getVideoHeight();
    }

    /**
     * Handle animation operations.
     */
    interface AnimationController {
        /**
         * Pauses the animation.
         */
        void pause();

        /**
         * Starts the animation.
         */
        void start();

        /**
         * Releases the animation object.
         */
        void release();

        /**
         * Attaches the animation to UI view.
         */
        void attachView(TextureView video, View preview, View playButton);

        /**
         * Returns the animation Width.
         */
        int getVideoWidth();

        /**
         * Returns the animation Height.
         */
        int getVideoHeight();

        /**
         * Returns the animation duration.
         */
        int getDuration();

        /**
         * Returns if the animation is playing.
         */
        boolean isPlaying();
    }
}
+51 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.testutils.shadow;

import static org.robolectric.shadows.ShadowMediaPlayer.State.INITIALIZED;

import android.content.Context;
import android.media.MediaPlayer;
import android.net.Uri;

import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowMediaPlayer;
import org.robolectric.shadows.util.DataSource;

@Implements(MediaPlayer.class)
public class ShadowSettingsMediaPlayer extends ShadowMediaPlayer {

    @Implementation
    public static MediaPlayer create(Context context, Uri uri) {
        final DataSource ds = DataSource.toDataSource(context, uri);
        addMediaInfo(ds, new ShadowMediaPlayer.MediaInfo());

        final MediaPlayer mp = new MediaPlayer();
        final ShadowMediaPlayer shadow = Shadow.extract(mp);
        try {
            shadow.setDataSource(ds);
            shadow.setState(INITIALIZED);
            mp.prepare();
        } catch (Exception e) {
            return null;
        }

        return mp;
    }
}
Loading