1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.developeroptions.widget; 18 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.content.res.TypedArray; 22 import android.graphics.SurfaceTexture; 23 import android.media.MediaPlayer; 24 import android.net.Uri; 25 import android.util.AttributeSet; 26 import android.util.Log; 27 import android.view.Surface; 28 import android.view.TextureView; 29 import android.view.View; 30 import android.widget.ImageView; 31 32 import androidx.annotation.VisibleForTesting; 33 import androidx.preference.Preference; 34 import androidx.preference.PreferenceViewHolder; 35 36 import com.android.car.developeroptions.R; 37 38 /** 39 * A full width preference that hosts a MP4 video. 40 */ 41 public class VideoPreference extends Preference { 42 43 private static final String TAG = "VideoPreference"; 44 private final Context mContext; 45 46 private Uri mVideoPath; 47 @VisibleForTesting 48 MediaPlayer mMediaPlayer; 49 @VisibleForTesting 50 boolean mAnimationAvailable; 51 @VisibleForTesting 52 boolean mVideoReady; 53 private boolean mVideoPaused; 54 private float mAspectRadio = 1.0f; 55 private int mPreviewResource; 56 private boolean mViewVisible; 57 private Surface mSurface; 58 private int mAnimationId; 59 VideoPreference(Context context)60 public VideoPreference(Context context) { 61 super(context); 62 mContext = context; 63 initialize(context, null); 64 } 65 VideoPreference(Context context, AttributeSet attrs)66 public VideoPreference(Context context, AttributeSet attrs) { 67 super(context, attrs); 68 mContext = context; 69 initialize(context, attrs); 70 } 71 initialize(Context context, AttributeSet attrs)72 private void initialize(Context context, AttributeSet attrs) { 73 TypedArray attributes = context.getTheme().obtainStyledAttributes( 74 attrs, 75 R.styleable.VideoPreference, 76 0, 0); 77 try { 78 // if these are already set that means they were set dynamically and don't need 79 // to be loaded from xml 80 mAnimationId = mAnimationId == 0 81 ? attributes.getResourceId(R.styleable.VideoPreference_animation, 0) 82 : mAnimationId; 83 mVideoPath = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 84 .authority(context.getPackageName()) 85 .appendPath(String.valueOf(mAnimationId)) 86 .build(); 87 mPreviewResource = mPreviewResource == 0 88 ? attributes.getResourceId(R.styleable.VideoPreference_preview, 0) 89 : mPreviewResource; 90 if (mPreviewResource == 0 && mAnimationId == 0) { 91 return; 92 } 93 initMediaPlayer(); 94 if (mMediaPlayer != null && mMediaPlayer.getDuration() > 0) { 95 setVisible(true); 96 setLayoutResource(R.layout.video_preference); 97 mAnimationAvailable = true; 98 updateAspectRatio(); 99 } else { 100 setVisible(false); 101 } 102 } catch (Exception e) { 103 Log.w(TAG, "Animation resource not found. Will not show animation."); 104 } finally { 105 attributes.recycle(); 106 } 107 } 108 109 @Override onBindViewHolder(PreferenceViewHolder holder)110 public void onBindViewHolder(PreferenceViewHolder holder) { 111 super.onBindViewHolder(holder); 112 113 if (!mAnimationAvailable) { 114 return; 115 } 116 117 final TextureView video = (TextureView) holder.findViewById(R.id.video_texture_view); 118 final ImageView imageView = (ImageView) holder.findViewById(R.id.video_preview_image); 119 final ImageView playButton = (ImageView) holder.findViewById(R.id.video_play_button); 120 final AspectRatioFrameLayout layout = (AspectRatioFrameLayout) holder.findViewById( 121 R.id.video_container); 122 123 imageView.setImageResource(mPreviewResource); 124 layout.setAspectRatio(mAspectRadio); 125 updateViewStates(imageView, playButton); 126 127 video.setOnClickListener(v -> updateViewStates(imageView, playButton)); 128 129 video.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { 130 @Override 131 public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, 132 int height) { 133 if (mMediaPlayer != null) { 134 mSurface = new Surface(surfaceTexture); 135 mMediaPlayer.setSurface(mSurface); 136 } 137 } 138 139 @Override 140 public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, 141 int height) { 142 } 143 144 @Override 145 public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { 146 imageView.setVisibility(View.VISIBLE); 147 return false; 148 } 149 150 @Override 151 public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { 152 if (!mViewVisible) { 153 return; 154 } 155 if (mVideoReady) { 156 if (imageView.getVisibility() == View.VISIBLE) { 157 imageView.setVisibility(View.GONE); 158 } 159 if (!mVideoPaused && mMediaPlayer != null && !mMediaPlayer.isPlaying()) { 160 mMediaPlayer.start(); 161 playButton.setVisibility(View.GONE); 162 } 163 } 164 if (mMediaPlayer != null && !mMediaPlayer.isPlaying() && 165 playButton.getVisibility() != View.VISIBLE) { 166 playButton.setVisibility(View.VISIBLE); 167 } 168 } 169 }); 170 } 171 172 @VisibleForTesting updateViewStates(ImageView imageView, ImageView playButton)173 void updateViewStates(ImageView imageView, ImageView playButton) { 174 if (mMediaPlayer != null) { 175 if (mMediaPlayer.isPlaying()) { 176 mMediaPlayer.pause(); 177 playButton.setVisibility(View.VISIBLE); 178 imageView.setVisibility(View.VISIBLE); 179 mVideoPaused = true; 180 } else { 181 imageView.setVisibility(View.GONE); 182 playButton.setVisibility(View.GONE); 183 mMediaPlayer.start(); 184 mVideoPaused = false; 185 } 186 } 187 } 188 189 @Override onDetached()190 public void onDetached() { 191 releaseMediaPlayer(); 192 super.onDetached(); 193 } 194 onViewVisible(boolean videoPaused)195 public void onViewVisible(boolean videoPaused) { 196 mViewVisible = true; 197 mVideoPaused = videoPaused; 198 initMediaPlayer(); 199 } 200 onViewInvisible()201 public void onViewInvisible() { 202 mViewVisible = false; 203 releaseMediaPlayer(); 204 } 205 206 /** 207 * Sets the video for this preference. If a previous video was set this one will override it 208 * and properly release any resources and re-initialize the preference to play the new video. 209 * 210 * @param videoId The raw res id of the video 211 * @param previewId The drawable res id of the preview image to use if the video fails to load. 212 */ setVideo(int videoId, int previewId)213 public void setVideo(int videoId, int previewId) { 214 mAnimationId = videoId; 215 mPreviewResource = previewId; 216 releaseMediaPlayer(); 217 initialize(mContext, null); 218 } 219 initMediaPlayer()220 private void initMediaPlayer() { 221 if (mMediaPlayer == null) { 222 mMediaPlayer = MediaPlayer.create(mContext, mVideoPath); 223 // when the playback res is invalid or others, MediaPlayer create may fail 224 // and return null, so need add the null judgement. 225 if (mMediaPlayer != null) { 226 mMediaPlayer.seekTo(0); 227 mMediaPlayer.setOnSeekCompleteListener(mp -> mVideoReady = true); 228 mMediaPlayer.setOnPreparedListener(mediaPlayer -> mediaPlayer.setLooping(true)); 229 if (mSurface != null) { 230 mMediaPlayer.setSurface(mSurface); 231 } 232 } 233 } 234 } 235 releaseMediaPlayer()236 private void releaseMediaPlayer() { 237 if (mMediaPlayer != null) { 238 mMediaPlayer.stop(); 239 mMediaPlayer.reset(); 240 mMediaPlayer.release(); 241 mMediaPlayer = null; 242 mVideoReady = false; 243 } 244 } 245 isVideoPaused()246 public boolean isVideoPaused() { 247 return mVideoPaused; 248 } 249 250 @VisibleForTesting updateAspectRatio()251 void updateAspectRatio() { 252 mAspectRadio = mMediaPlayer.getVideoWidth() / (float) mMediaPlayer.getVideoHeight(); 253 } 254 } 255