1 /* 2 * Copyright (C) 2016 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.settings.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.util.TypedValue; 28 import android.view.Surface; 29 import android.view.TextureView; 30 import android.view.View; 31 import android.widget.ImageView; 32 import android.widget.LinearLayout; 33 34 import androidx.annotation.VisibleForTesting; 35 import androidx.preference.Preference; 36 import androidx.preference.PreferenceViewHolder; 37 38 import com.android.settings.R; 39 40 /** 41 * A full width preference that hosts a MP4 video. 42 */ 43 public class VideoPreference extends Preference { 44 45 private static final String TAG = "VideoPreference"; 46 private final Context mContext; 47 48 private Uri mVideoPath; 49 @VisibleForTesting 50 MediaPlayer mMediaPlayer; 51 @VisibleForTesting 52 boolean mAnimationAvailable; 53 @VisibleForTesting 54 boolean mVideoReady; 55 private boolean mVideoPaused; 56 private float mAspectRatio = 1.0f; 57 private int mPreviewResource; 58 private boolean mViewVisible; 59 private Surface mSurface; 60 private int mAnimationId; 61 private int mHeight = LinearLayout.LayoutParams.MATCH_PARENT - 1; // video height in pixels 62 VideoPreference(Context context)63 public VideoPreference(Context context) { 64 super(context); 65 mContext = context; 66 initialize(context, null); 67 } 68 VideoPreference(Context context, AttributeSet attrs)69 public VideoPreference(Context context, AttributeSet attrs) { 70 super(context, attrs); 71 mContext = context; 72 initialize(context, attrs); 73 } 74 initialize(Context context, AttributeSet attrs)75 private void initialize(Context context, AttributeSet attrs) { 76 TypedArray attributes = context.getTheme().obtainStyledAttributes( 77 attrs, 78 R.styleable.VideoPreference, 79 0, 0); 80 try { 81 // if these are already set that means they were set dynamically and don't need 82 // to be loaded from xml 83 mAnimationAvailable = false; 84 mAnimationId = mAnimationId == 0 85 ? attributes.getResourceId(R.styleable.VideoPreference_animation, 0) 86 : mAnimationId; 87 mVideoPath = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 88 .authority(context.getPackageName()) 89 .appendPath(String.valueOf(mAnimationId)) 90 .build(); 91 mPreviewResource = mPreviewResource == 0 92 ? attributes.getResourceId(R.styleable.VideoPreference_preview, 0) 93 : mPreviewResource; 94 if (mPreviewResource == 0 && mAnimationId == 0) { 95 setVisible(false); 96 return; 97 } 98 initMediaPlayer(); 99 if (mMediaPlayer != null && mMediaPlayer.getDuration() > 0) { 100 setVisible(true); 101 setLayoutResource(R.layout.video_preference); 102 mAnimationAvailable = true; 103 updateAspectRatio(); 104 } else { 105 setVisible(false); 106 } 107 } catch (Exception e) { 108 Log.w(TAG, "Animation resource not found. Will not show animation."); 109 } finally { 110 attributes.recycle(); 111 } 112 } 113 114 @Override onBindViewHolder(PreferenceViewHolder holder)115 public void onBindViewHolder(PreferenceViewHolder holder) { 116 super.onBindViewHolder(holder); 117 118 if (!mAnimationAvailable) { 119 return; 120 } 121 122 final TextureView video = (TextureView) holder.findViewById(R.id.video_texture_view); 123 final ImageView imageView = (ImageView) holder.findViewById(R.id.video_preview_image); 124 final ImageView playButton = (ImageView) holder.findViewById(R.id.video_play_button); 125 final AspectRatioFrameLayout layout = (AspectRatioFrameLayout) holder.findViewById( 126 R.id.video_container); 127 128 imageView.setImageResource(mPreviewResource); 129 layout.setAspectRatio(mAspectRatio); 130 if (mHeight >= LinearLayout.LayoutParams.MATCH_PARENT) { 131 layout.setLayoutParams(new LinearLayout.LayoutParams( 132 LinearLayout.LayoutParams.MATCH_PARENT, mHeight)); 133 } 134 updateViewStates(imageView, playButton); 135 136 video.setOnClickListener(v -> updateViewStates(imageView, playButton)); 137 138 video.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { 139 @Override 140 public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, 141 int height) { 142 if (mMediaPlayer != null) { 143 mSurface = new Surface(surfaceTexture); 144 mMediaPlayer.setSurface(mSurface); 145 } 146 } 147 148 @Override 149 public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, 150 int height) { 151 } 152 153 @Override 154 public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { 155 imageView.setVisibility(View.VISIBLE); 156 return false; 157 } 158 159 @Override 160 public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { 161 if (!mViewVisible) { 162 return; 163 } 164 if (mVideoReady) { 165 if (imageView.getVisibility() == View.VISIBLE) { 166 imageView.setVisibility(View.GONE); 167 } 168 if (!mVideoPaused && mMediaPlayer != null && !mMediaPlayer.isPlaying()) { 169 mMediaPlayer.start(); 170 playButton.setVisibility(View.GONE); 171 } 172 } 173 if (mMediaPlayer != null && !mMediaPlayer.isPlaying() && 174 playButton.getVisibility() != View.VISIBLE) { 175 playButton.setVisibility(View.VISIBLE); 176 } 177 } 178 }); 179 } 180 181 @VisibleForTesting updateViewStates(ImageView imageView, ImageView playButton)182 void updateViewStates(ImageView imageView, ImageView playButton) { 183 if (mMediaPlayer != null) { 184 if (mMediaPlayer.isPlaying()) { 185 mMediaPlayer.pause(); 186 playButton.setVisibility(View.VISIBLE); 187 imageView.setVisibility(View.VISIBLE); 188 mVideoPaused = true; 189 } else { 190 imageView.setVisibility(View.GONE); 191 playButton.setVisibility(View.GONE); 192 mMediaPlayer.start(); 193 mVideoPaused = false; 194 } 195 } 196 } 197 198 @Override onDetached()199 public void onDetached() { 200 releaseMediaPlayer(); 201 super.onDetached(); 202 } 203 onViewVisible(boolean videoPaused)204 public void onViewVisible(boolean videoPaused) { 205 mViewVisible = true; 206 mVideoPaused = videoPaused; 207 initMediaPlayer(); 208 } 209 onViewInvisible()210 public void onViewInvisible() { 211 mViewVisible = false; 212 releaseMediaPlayer(); 213 } 214 215 /** 216 * Sets the video for this preference. If a previous video was set this one will override it 217 * and properly release any resources and re-initialize the preference to play the new video. 218 * 219 * @param videoId The raw res id of the video 220 * @param previewId The drawable res id of the preview image to use if the video fails to load. 221 */ setVideo(int videoId, int previewId)222 public void setVideo(int videoId, int previewId) { 223 mAnimationId = videoId; 224 mPreviewResource = previewId; 225 releaseMediaPlayer(); 226 initialize(mContext, null); 227 } 228 initMediaPlayer()229 private void initMediaPlayer() { 230 if (mMediaPlayer == null) { 231 mMediaPlayer = MediaPlayer.create(mContext, mVideoPath); 232 // when the playback res is invalid or others, MediaPlayer create may fail 233 // and return null, so need add the null judgement. 234 if (mMediaPlayer != null) { 235 mMediaPlayer.seekTo(0); 236 mMediaPlayer.setOnSeekCompleteListener(mp -> mVideoReady = true); 237 mMediaPlayer.setOnPreparedListener(mediaPlayer -> mediaPlayer.setLooping(true)); 238 if (mSurface != null) { 239 mMediaPlayer.setSurface(mSurface); 240 } 241 } 242 } 243 } 244 releaseMediaPlayer()245 private void releaseMediaPlayer() { 246 if (mMediaPlayer != null) { 247 mMediaPlayer.stop(); 248 mMediaPlayer.reset(); 249 mMediaPlayer.release(); 250 mMediaPlayer = null; 251 mVideoReady = false; 252 } 253 } 254 isAnimationAvailable()255 public boolean isAnimationAvailable() { 256 return mAnimationAvailable; 257 } 258 isVideoPaused()259 public boolean isVideoPaused() { 260 return mVideoPaused; 261 } 262 263 /** 264 * sets the height of the video preference 265 * @param height in dp 266 */ setHeight(float height)267 public void setHeight(float height) { 268 mHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, height, 269 mContext.getResources().getDisplayMetrics()); 270 } 271 272 @VisibleForTesting updateAspectRatio()273 void updateAspectRatio() { 274 mAspectRatio = mMediaPlayer.getVideoWidth() / (float) mMediaPlayer.getVideoHeight(); 275 } 276 } 277