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 mAnimationId = mAnimationId == 0 84 ? attributes.getResourceId(R.styleable.VideoPreference_animation, 0) 85 : mAnimationId; 86 mVideoPath = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 87 .authority(context.getPackageName()) 88 .appendPath(String.valueOf(mAnimationId)) 89 .build(); 90 mPreviewResource = mPreviewResource == 0 91 ? attributes.getResourceId(R.styleable.VideoPreference_preview, 0) 92 : mPreviewResource; 93 if (mPreviewResource == 0 && mAnimationId == 0) { 94 return; 95 } 96 initMediaPlayer(); 97 if (mMediaPlayer != null && mMediaPlayer.getDuration() > 0) { 98 setVisible(true); 99 setLayoutResource(R.layout.video_preference); 100 mAnimationAvailable = true; 101 updateAspectRatio(); 102 } else { 103 setVisible(false); 104 } 105 } catch (Exception e) { 106 Log.w(TAG, "Animation resource not found. Will not show animation."); 107 } finally { 108 attributes.recycle(); 109 } 110 } 111 112 @Override onBindViewHolder(PreferenceViewHolder holder)113 public void onBindViewHolder(PreferenceViewHolder holder) { 114 super.onBindViewHolder(holder); 115 116 if (!mAnimationAvailable) { 117 return; 118 } 119 120 final TextureView video = (TextureView) holder.findViewById(R.id.video_texture_view); 121 final ImageView imageView = (ImageView) holder.findViewById(R.id.video_preview_image); 122 final ImageView playButton = (ImageView) holder.findViewById(R.id.video_play_button); 123 final AspectRatioFrameLayout layout = (AspectRatioFrameLayout) holder.findViewById( 124 R.id.video_container); 125 126 imageView.setImageResource(mPreviewResource); 127 layout.setAspectRatio(mAspectRatio); 128 if (mHeight >= LinearLayout.LayoutParams.MATCH_PARENT) { 129 layout.setLayoutParams(new LinearLayout.LayoutParams( 130 LinearLayout.LayoutParams.MATCH_PARENT, mHeight)); 131 } 132 updateViewStates(imageView, playButton); 133 134 video.setOnClickListener(v -> updateViewStates(imageView, playButton)); 135 136 video.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { 137 @Override 138 public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, 139 int height) { 140 if (mMediaPlayer != null) { 141 mSurface = new Surface(surfaceTexture); 142 mMediaPlayer.setSurface(mSurface); 143 } 144 } 145 146 @Override 147 public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, 148 int height) { 149 } 150 151 @Override 152 public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { 153 imageView.setVisibility(View.VISIBLE); 154 return false; 155 } 156 157 @Override 158 public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { 159 if (!mViewVisible) { 160 return; 161 } 162 if (mVideoReady) { 163 if (imageView.getVisibility() == View.VISIBLE) { 164 imageView.setVisibility(View.GONE); 165 } 166 if (!mVideoPaused && mMediaPlayer != null && !mMediaPlayer.isPlaying()) { 167 mMediaPlayer.start(); 168 playButton.setVisibility(View.GONE); 169 } 170 } 171 if (mMediaPlayer != null && !mMediaPlayer.isPlaying() && 172 playButton.getVisibility() != View.VISIBLE) { 173 playButton.setVisibility(View.VISIBLE); 174 } 175 } 176 }); 177 } 178 179 @VisibleForTesting updateViewStates(ImageView imageView, ImageView playButton)180 void updateViewStates(ImageView imageView, ImageView playButton) { 181 if (mMediaPlayer != null) { 182 if (mMediaPlayer.isPlaying()) { 183 mMediaPlayer.pause(); 184 playButton.setVisibility(View.VISIBLE); 185 imageView.setVisibility(View.VISIBLE); 186 mVideoPaused = true; 187 } else { 188 imageView.setVisibility(View.GONE); 189 playButton.setVisibility(View.GONE); 190 mMediaPlayer.start(); 191 mVideoPaused = false; 192 } 193 } 194 } 195 196 @Override onDetached()197 public void onDetached() { 198 releaseMediaPlayer(); 199 super.onDetached(); 200 } 201 onViewVisible(boolean videoPaused)202 public void onViewVisible(boolean videoPaused) { 203 mViewVisible = true; 204 mVideoPaused = videoPaused; 205 initMediaPlayer(); 206 } 207 onViewInvisible()208 public void onViewInvisible() { 209 mViewVisible = false; 210 releaseMediaPlayer(); 211 } 212 213 /** 214 * Sets the video for this preference. If a previous video was set this one will override it 215 * and properly release any resources and re-initialize the preference to play the new video. 216 * 217 * @param videoId The raw res id of the video 218 * @param previewId The drawable res id of the preview image to use if the video fails to load. 219 */ setVideo(int videoId, int previewId)220 public void setVideo(int videoId, int previewId) { 221 mAnimationId = videoId; 222 mPreviewResource = previewId; 223 releaseMediaPlayer(); 224 initialize(mContext, null); 225 } 226 initMediaPlayer()227 private void initMediaPlayer() { 228 if (mMediaPlayer == null) { 229 mMediaPlayer = MediaPlayer.create(mContext, mVideoPath); 230 // when the playback res is invalid or others, MediaPlayer create may fail 231 // and return null, so need add the null judgement. 232 if (mMediaPlayer != null) { 233 mMediaPlayer.seekTo(0); 234 mMediaPlayer.setOnSeekCompleteListener(mp -> mVideoReady = true); 235 mMediaPlayer.setOnPreparedListener(mediaPlayer -> mediaPlayer.setLooping(true)); 236 if (mSurface != null) { 237 mMediaPlayer.setSurface(mSurface); 238 } 239 } 240 } 241 } 242 releaseMediaPlayer()243 private void releaseMediaPlayer() { 244 if (mMediaPlayer != null) { 245 mMediaPlayer.stop(); 246 mMediaPlayer.reset(); 247 mMediaPlayer.release(); 248 mMediaPlayer = null; 249 mVideoReady = false; 250 } 251 } 252 isVideoPaused()253 public boolean isVideoPaused() { 254 return mVideoPaused; 255 } 256 257 /** 258 * sets the height of the video preference 259 * @param height in dp 260 */ setHeight(float height)261 public void setHeight(float height) { 262 mHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, height, 263 mContext.getResources().getDisplayMetrics()); 264 } 265 266 @VisibleForTesting updateAspectRatio()267 void updateAspectRatio() { 268 mAspectRatio = mMediaPlayer.getVideoWidth() / (float) mMediaPlayer.getVideoHeight(); 269 } 270 } 271