19395f90b26e072b2748db315aaffef27c257a82dMaurice Lam/* 29395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * Copyright (C) 2017 The Android Open Source Project 39395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * 49395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * Licensed under the Apache License, Version 2.0 (the "License"); 59395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * you may not use this file except in compliance with the License. 69395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * You may obtain a copy of the License at 79395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * 89395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * http://www.apache.org/licenses/LICENSE-2.0 99395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * 109395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * Unless required by applicable law or agreed to in writing, software 119395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * distributed under the License is distributed on an "AS IS" BASIS, 129395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 139395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * See the License for the specific language governing permissions and 149395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * limitations under the License. 159395f90b26e072b2748db315aaffef27c257a82dMaurice Lam */ 169395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 179395f90b26e072b2748db315aaffef27c257a82dMaurice Lampackage com.android.setupwizardlib.view; 189395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 199395f90b26e072b2748db315aaffef27c257a82dMaurice Lamimport android.annotation.TargetApi; 209395f90b26e072b2748db315aaffef27c257a82dMaurice Lamimport android.content.Context; 219395f90b26e072b2748db315aaffef27c257a82dMaurice Lamimport android.content.res.TypedArray; 229395f90b26e072b2748db315aaffef27c257a82dMaurice Lamimport android.graphics.SurfaceTexture; 239395f90b26e072b2748db315aaffef27c257a82dMaurice Lamimport android.graphics.drawable.Animatable; 249395f90b26e072b2748db315aaffef27c257a82dMaurice Lamimport android.media.MediaPlayer; 259395f90b26e072b2748db315aaffef27c257a82dMaurice Lamimport android.os.Build.VERSION_CODES; 269395f90b26e072b2748db315aaffef27c257a82dMaurice Lamimport android.support.annotation.RawRes; 279395f90b26e072b2748db315aaffef27c257a82dMaurice Lamimport android.support.annotation.VisibleForTesting; 289395f90b26e072b2748db315aaffef27c257a82dMaurice Lamimport android.util.AttributeSet; 299395f90b26e072b2748db315aaffef27c257a82dMaurice Lamimport android.view.Surface; 309395f90b26e072b2748db315aaffef27c257a82dMaurice Lamimport android.view.TextureView; 319395f90b26e072b2748db315aaffef27c257a82dMaurice Lamimport android.view.View; 329395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 339395f90b26e072b2748db315aaffef27c257a82dMaurice Lamimport com.android.setupwizardlib.R; 349395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 359395f90b26e072b2748db315aaffef27c257a82dMaurice Lam/** 369395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * A view for displaying videos in a continuous loop (without audio). This is typically used for 379395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * animated illustrations. 389395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * 399395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * <p>The video can be specified using {@code app:suwVideo}, specifying the raw resource to the mp4 409395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * video. Optionally, {@code app:suwLoopStartMs} can be used to specify which part of the video it 419395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * should loop back to 429395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * 439395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * <p>For optimal file size, use avconv or other video compression tool to remove the unused audio 449395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * track and reduce the size of your video asset: 459395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * avconv -i [input file] -vcodec h264 -crf 20 -an [output_file] 469395f90b26e072b2748db315aaffef27c257a82dMaurice Lam */ 479395f90b26e072b2748db315aaffef27c257a82dMaurice Lam@TargetApi(VERSION_CODES.ICE_CREAM_SANDWICH) 489395f90b26e072b2748db315aaffef27c257a82dMaurice Lampublic class IllustrationVideoView extends TextureView implements Animatable, 499395f90b26e072b2748db315aaffef27c257a82dMaurice Lam TextureView.SurfaceTextureListener, 509395f90b26e072b2748db315aaffef27c257a82dMaurice Lam MediaPlayer.OnPreparedListener, 519395f90b26e072b2748db315aaffef27c257a82dMaurice Lam MediaPlayer.OnSeekCompleteListener, 529395f90b26e072b2748db315aaffef27c257a82dMaurice Lam MediaPlayer.OnInfoListener { 539395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 549395f90b26e072b2748db315aaffef27c257a82dMaurice Lam protected float mAspectRatio = 1.0f; // initial guess until we know 559395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 56ffb2d8f2de805c6e717053caef48db1839e00d64Maurice Lam protected MediaPlayer mMediaPlayer; 579395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 589395f90b26e072b2748db315aaffef27c257a82dMaurice Lam private @RawRes int mVideoResId = 0; 599395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 609395f90b26e072b2748db315aaffef27c257a82dMaurice Lam @VisibleForTesting Surface mSurface; 619395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 629395f90b26e072b2748db315aaffef27c257a82dMaurice Lam public IllustrationVideoView(Context context, AttributeSet attrs) { 639395f90b26e072b2748db315aaffef27c257a82dMaurice Lam super(context, attrs); 649395f90b26e072b2748db315aaffef27c257a82dMaurice Lam final TypedArray a = context.obtainStyledAttributes(attrs, 659395f90b26e072b2748db315aaffef27c257a82dMaurice Lam R.styleable.SuwIllustrationVideoView); 669395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mVideoResId = a.getResourceId(R.styleable.SuwIllustrationVideoView_suwVideo, 0); 679395f90b26e072b2748db315aaffef27c257a82dMaurice Lam a.recycle(); 689395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 69198bd0ddb15ad479e0d0d7214645a3c067f271a1Maurice Lam // By default the video scales without interpolation, resulting in jagged edges in the 70198bd0ddb15ad479e0d0d7214645a3c067f271a1Maurice Lam // video. This works around it by making the view go through scaling, which will apply 71198bd0ddb15ad479e0d0d7214645a3c067f271a1Maurice Lam // anti-aliasing effects. 72198bd0ddb15ad479e0d0d7214645a3c067f271a1Maurice Lam setScaleX(0.9999999f); 73198bd0ddb15ad479e0d0d7214645a3c067f271a1Maurice Lam setScaleX(0.9999999f); 74198bd0ddb15ad479e0d0d7214645a3c067f271a1Maurice Lam 759395f90b26e072b2748db315aaffef27c257a82dMaurice Lam setSurfaceTextureListener(this); 769395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 779395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 789395f90b26e072b2748db315aaffef27c257a82dMaurice Lam @Override 799395f90b26e072b2748db315aaffef27c257a82dMaurice Lam protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 809395f90b26e072b2748db315aaffef27c257a82dMaurice Lam int width = MeasureSpec.getSize(widthMeasureSpec); 819395f90b26e072b2748db315aaffef27c257a82dMaurice Lam int height = MeasureSpec.getSize(heightMeasureSpec); 829395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 839395f90b26e072b2748db315aaffef27c257a82dMaurice Lam if (height < width * mAspectRatio) { 849395f90b26e072b2748db315aaffef27c257a82dMaurice Lam // Height constraint is tighter. Need to scale down the width to fit aspect ratio. 859395f90b26e072b2748db315aaffef27c257a82dMaurice Lam width = (int) (height / mAspectRatio); 869395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } else { 879395f90b26e072b2748db315aaffef27c257a82dMaurice Lam // Width constraint is tighter. Need to scale down the height to fit aspect ratio. 889395f90b26e072b2748db315aaffef27c257a82dMaurice Lam height = (int) (width * mAspectRatio); 899395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 909395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 919395f90b26e072b2748db315aaffef27c257a82dMaurice Lam super.onMeasure( 929395f90b26e072b2748db315aaffef27c257a82dMaurice Lam MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 939395f90b26e072b2748db315aaffef27c257a82dMaurice Lam MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); 949395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 959395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 969395f90b26e072b2748db315aaffef27c257a82dMaurice Lam /** 979395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * Set the video to be played by this view. 989395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * 999395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * @param resId Resource ID of the video, typically an MP4 under res/raw. 1009395f90b26e072b2748db315aaffef27c257a82dMaurice Lam */ 1019395f90b26e072b2748db315aaffef27c257a82dMaurice Lam public void setVideoResource(@RawRes int resId) { 1029395f90b26e072b2748db315aaffef27c257a82dMaurice Lam if (resId != mVideoResId) { 1039395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mVideoResId = resId; 1049395f90b26e072b2748db315aaffef27c257a82dMaurice Lam createMediaPlayer(); 1059395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1069395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1079395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 1089395f90b26e072b2748db315aaffef27c257a82dMaurice Lam @Override 1099395f90b26e072b2748db315aaffef27c257a82dMaurice Lam public void onWindowFocusChanged(boolean hasWindowFocus) { 1109395f90b26e072b2748db315aaffef27c257a82dMaurice Lam super.onWindowFocusChanged(hasWindowFocus); 1119395f90b26e072b2748db315aaffef27c257a82dMaurice Lam if (hasWindowFocus) { 1129395f90b26e072b2748db315aaffef27c257a82dMaurice Lam start(); 1139395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } else { 1149395f90b26e072b2748db315aaffef27c257a82dMaurice Lam stop(); 1159395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1169395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1179395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 1189395f90b26e072b2748db315aaffef27c257a82dMaurice Lam /** 1199395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * Creates a media player for the current URI. The media player will be started immediately if 1209395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * the view's window is visible. If there is an existing media player, it will be released. 1219395f90b26e072b2748db315aaffef27c257a82dMaurice Lam */ 1229395f90b26e072b2748db315aaffef27c257a82dMaurice Lam private void createMediaPlayer() { 1239395f90b26e072b2748db315aaffef27c257a82dMaurice Lam if (mMediaPlayer != null) { 1249395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mMediaPlayer.release(); 1259395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1269395f90b26e072b2748db315aaffef27c257a82dMaurice Lam if (mSurface == null || mVideoResId == 0) { 1279395f90b26e072b2748db315aaffef27c257a82dMaurice Lam return; 1289395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1299395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 1309395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mMediaPlayer = MediaPlayer.create(getContext(), mVideoResId); 1319395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 1329395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mMediaPlayer.setSurface(mSurface); 1339395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mMediaPlayer.setOnPreparedListener(this); 1349395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mMediaPlayer.setOnSeekCompleteListener(this); 1359395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mMediaPlayer.setOnInfoListener(this); 1369395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 1379395f90b26e072b2748db315aaffef27c257a82dMaurice Lam float aspectRatio = (float) mMediaPlayer.getVideoHeight() / mMediaPlayer.getVideoWidth(); 1389395f90b26e072b2748db315aaffef27c257a82dMaurice Lam if (mAspectRatio != aspectRatio) { 1399395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mAspectRatio = aspectRatio; 1409395f90b26e072b2748db315aaffef27c257a82dMaurice Lam requestLayout(); 1419395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1429395f90b26e072b2748db315aaffef27c257a82dMaurice Lam if (getWindowVisibility() == View.VISIBLE) { 1439395f90b26e072b2748db315aaffef27c257a82dMaurice Lam start(); 1449395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1459395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1469395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 1479395f90b26e072b2748db315aaffef27c257a82dMaurice Lam /** 1489395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * Whether the media player should play the video in a continuous loop. The default value is 1499395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * true. 1509395f90b26e072b2748db315aaffef27c257a82dMaurice Lam */ 1519395f90b26e072b2748db315aaffef27c257a82dMaurice Lam protected boolean shouldLoop() { 1529395f90b26e072b2748db315aaffef27c257a82dMaurice Lam return true; 1539395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1549395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 1559395f90b26e072b2748db315aaffef27c257a82dMaurice Lam /** 1569395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * Release any resources used by this view. This is automatically called in 1579395f90b26e072b2748db315aaffef27c257a82dMaurice Lam * onSurfaceTextureDestroyed so in most cases you don't have to call this. 1589395f90b26e072b2748db315aaffef27c257a82dMaurice Lam */ 1599395f90b26e072b2748db315aaffef27c257a82dMaurice Lam public void release() { 1609395f90b26e072b2748db315aaffef27c257a82dMaurice Lam if (mMediaPlayer != null) { 1619395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mMediaPlayer.stop(); 1629395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mMediaPlayer.release(); 1639395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mMediaPlayer = null; 1649395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1659395f90b26e072b2748db315aaffef27c257a82dMaurice Lam if (mSurface != null) { 1669395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mSurface.release(); 1679395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mSurface = null; 1689395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1699395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1709395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 1719395f90b26e072b2748db315aaffef27c257a82dMaurice Lam /* SurfaceTextureListener methods */ 1729395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 1739395f90b26e072b2748db315aaffef27c257a82dMaurice Lam @Override 1749395f90b26e072b2748db315aaffef27c257a82dMaurice Lam public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { 1759395f90b26e072b2748db315aaffef27c257a82dMaurice Lam // Keep the view hidden until video starts 1769395f90b26e072b2748db315aaffef27c257a82dMaurice Lam setVisibility(View.INVISIBLE); 1779395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mSurface = new Surface(surfaceTexture); 1789395f90b26e072b2748db315aaffef27c257a82dMaurice Lam createMediaPlayer(); 1799395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1809395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 1819395f90b26e072b2748db315aaffef27c257a82dMaurice Lam @Override 1829395f90b26e072b2748db315aaffef27c257a82dMaurice Lam public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { 1839395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1849395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 1859395f90b26e072b2748db315aaffef27c257a82dMaurice Lam @Override 1869395f90b26e072b2748db315aaffef27c257a82dMaurice Lam public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { 1879395f90b26e072b2748db315aaffef27c257a82dMaurice Lam release(); 1889395f90b26e072b2748db315aaffef27c257a82dMaurice Lam return true; 1899395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1909395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 1919395f90b26e072b2748db315aaffef27c257a82dMaurice Lam @Override 1929395f90b26e072b2748db315aaffef27c257a82dMaurice Lam public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { 1939395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 1949395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 1959395f90b26e072b2748db315aaffef27c257a82dMaurice Lam /* Animatable methods */ 1969395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 1979395f90b26e072b2748db315aaffef27c257a82dMaurice Lam @Override 1989395f90b26e072b2748db315aaffef27c257a82dMaurice Lam public void start() { 1999395f90b26e072b2748db315aaffef27c257a82dMaurice Lam if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) { 2009395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mMediaPlayer.start(); 2019395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 2029395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 2039395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 2049395f90b26e072b2748db315aaffef27c257a82dMaurice Lam @Override 2059395f90b26e072b2748db315aaffef27c257a82dMaurice Lam public void stop() { 2069395f90b26e072b2748db315aaffef27c257a82dMaurice Lam if (mMediaPlayer != null) { 2079395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mMediaPlayer.pause(); 2089395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 2099395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 2109395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 2119395f90b26e072b2748db315aaffef27c257a82dMaurice Lam @Override 2129395f90b26e072b2748db315aaffef27c257a82dMaurice Lam public boolean isRunning() { 2139395f90b26e072b2748db315aaffef27c257a82dMaurice Lam return mMediaPlayer != null && mMediaPlayer.isPlaying(); 2149395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 2159395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 2169395f90b26e072b2748db315aaffef27c257a82dMaurice Lam /* MediaPlayer callbacks */ 2179395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 2189395f90b26e072b2748db315aaffef27c257a82dMaurice Lam @Override 2199395f90b26e072b2748db315aaffef27c257a82dMaurice Lam public boolean onInfo(MediaPlayer mp, int what, int extra) { 2209395f90b26e072b2748db315aaffef27c257a82dMaurice Lam if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) { 2219395f90b26e072b2748db315aaffef27c257a82dMaurice Lam // Video available, show view now 2229395f90b26e072b2748db315aaffef27c257a82dMaurice Lam setVisibility(View.VISIBLE); 2239395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 2249395f90b26e072b2748db315aaffef27c257a82dMaurice Lam return false; 2259395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 2269395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 2279395f90b26e072b2748db315aaffef27c257a82dMaurice Lam @Override 2289395f90b26e072b2748db315aaffef27c257a82dMaurice Lam public void onPrepared(MediaPlayer mp) { 2299395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mp.setLooping(shouldLoop()); 2309395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 2319395f90b26e072b2748db315aaffef27c257a82dMaurice Lam 2329395f90b26e072b2748db315aaffef27c257a82dMaurice Lam @Override 2339395f90b26e072b2748db315aaffef27c257a82dMaurice Lam public void onSeekComplete(MediaPlayer mp) { 2349395f90b26e072b2748db315aaffef27c257a82dMaurice Lam mp.start(); 2359395f90b26e072b2748db315aaffef27c257a82dMaurice Lam } 236616fd7a886200e2366cfc0dca4b9050bf61a6f30Ajay Nadathur 237616fd7a886200e2366cfc0dca4b9050bf61a6f30Ajay Nadathur public int getCurrentPosition() { 238616fd7a886200e2366cfc0dca4b9050bf61a6f30Ajay Nadathur return mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition(); 239616fd7a886200e2366cfc0dca4b9050bf61a6f30Ajay Nadathur } 2409395f90b26e072b2748db315aaffef27c257a82dMaurice Lam} 241