1 /* 2 * Copyright (C) 2022 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.providers.media.photopicker.ui.remotepreview; 18 19 import static android.provider.CloudMediaProviderContract.EXTRA_AUTHORITY; 20 import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED; 21 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER; 22 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED; 23 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_STATE_CALLBACK; 24 import static android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER; 25 26 import static com.android.providers.media.PickerUriResolver.createSurfaceControllerUri; 27 28 import android.annotation.Nullable; 29 import android.content.Context; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.IBinder; 33 import android.os.Looper; 34 import android.os.RemoteException; 35 import android.os.SystemProperties; 36 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PlaybackState; 37 import android.provider.ICloudMediaSurfaceController; 38 import android.provider.ICloudMediaSurfaceStateChangedCallback; 39 import android.util.ArrayMap; 40 import android.util.Log; 41 import android.view.Surface; 42 import android.view.SurfaceHolder; 43 import android.view.SurfaceView; 44 45 import com.android.providers.media.photopicker.PickerSyncController; 46 import com.android.providers.media.photopicker.data.MuteStatus; 47 import com.android.providers.media.photopicker.data.model.Item; 48 import com.android.providers.media.photopicker.ui.PreviewVideoHolder; 49 50 import java.util.Map; 51 52 /** 53 * Manages playback of videos on a {@link Surface} with a 54 * {@link android.provider.CloudMediaProvider.CloudMediaSurfaceController} populated remotely. 55 * 56 * <p>This class is not thread-safe and the methods are meant to be always called on the main 57 * thread. 58 */ 59 public final class RemotePreviewHandler { 60 61 private static final String TAG = "RemotePreviewHandler"; 62 63 private final Context mContext; 64 private final MuteStatus mMuteStatus; 65 private final ArrayMap<SurfaceHolder, RemotePreviewSession> 66 mSessionMap = new ArrayMap<>(); 67 private final Map<String, SurfaceControllerProxy> mControllers = 68 new ArrayMap<>(); 69 private final SurfaceHolder.Callback mSurfaceHolderCallback = new PreviewSurfaceCallback(); 70 private final SurfaceStateChangedCallbackWrapper mSurfaceStateChangedCallbackWrapper = 71 new SurfaceStateChangedCallbackWrapper(); 72 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 73 private final ItemPreviewState mCurrentPreviewState = new ItemPreviewState(); 74 private final PlayerControlsVisibilityStatus mPlayerControlsVisibilityStatus = 75 new PlayerControlsVisibilityStatus(); 76 77 private boolean mIsInBackground = false; 78 private int mSurfaceCounter = 0; 79 80 /** 81 * Returns {@code true} if remote preview is enabled. 82 */ isRemotePreviewEnabled()83 public static boolean isRemotePreviewEnabled() { 84 return SystemProperties.getBoolean("sys.photopicker.remote_preview", true); 85 } 86 RemotePreviewHandler(Context context, MuteStatus muteStatus)87 public RemotePreviewHandler(Context context, MuteStatus muteStatus) { 88 mContext = context; 89 mMuteStatus = muteStatus; 90 } 91 92 /** 93 * Prepares the given {@link SurfaceView} for remote preview of the given {@link Item}. 94 * 95 * @param viewHolder {@link PreviewVideoHolder} for the media item under preview 96 * @param item {@link Item} to be previewed 97 */ onViewAttachedToWindow(PreviewVideoHolder viewHolder, Item item)98 public void onViewAttachedToWindow(PreviewVideoHolder viewHolder, Item item) { 99 final RemotePreviewSession session = createRemotePreviewSession(item, viewHolder); 100 final SurfaceHolder holder = viewHolder.getSurfaceHolder(); 101 102 mSessionMap.put(holder, session); 103 // Ensure that we don't add the same callback twice, since we don't remove callbacks 104 // anywhere else. 105 holder.removeCallback(mSurfaceHolderCallback); 106 holder.addCallback(mSurfaceHolderCallback); 107 } 108 109 /** 110 * Handle page selected event for the given {@link Item}. 111 * 112 * <p>This is where we start the playback for the {@link Item}. 113 * 114 * @param item {@link Item} to be played 115 * @return true if the given {@link Item} can be played, else false 116 */ onHandlePageSelected(Item item)117 public boolean onHandlePageSelected(Item item) { 118 if (!item.isVideo()) { 119 // Clear state of the previous player controls visibility state. Controls visibility 120 // state will only be tracked and used for contiguous videos in the preview. 121 mPlayerControlsVisibilityStatus.setShouldShowPlayerControlsForNextItem(true); 122 return false; 123 } 124 125 Log.i(TAG, "onHandlePageSelected() called, attempting to start playback."); 126 RemotePreviewSession session = getSessionForItem(item); 127 if (session == null) { 128 Log.w(TAG, "No RemotePreviewSession found."); 129 return false; 130 } 131 132 mCurrentPreviewState.item = item; 133 mCurrentPreviewState.viewHolder = session.getPreviewVideoHolder(); 134 135 session.requestPlayMedia(); 136 return true; 137 } 138 139 /** 140 * Handle onStop called from activity/fragment lifecycle. 141 */ onStop()142 public void onStop() { 143 mIsInBackground = true; 144 } 145 146 /** 147 * Handle onDestroy called from activity/fragment lifecycle. 148 * 149 * <p>This is where the surface controllers are destroyed and their references are released. 150 */ onDestroy()151 public void onDestroy() { 152 Log.i(TAG, "onDestroy() called, destroying all surface controllers."); 153 destroyAllSurfaceControllers(); 154 } 155 createRemotePreviewSession(Item item, PreviewVideoHolder previewVideoHolder)156 private RemotePreviewSession createRemotePreviewSession(Item item, 157 PreviewVideoHolder previewVideoHolder) { 158 String authority = item.getContentUri().getAuthority(); 159 SurfaceControllerProxy controller = getSurfaceController(authority, false); 160 if (controller == null) { 161 Log.w(TAG, "Failed to create RemotePreviewSession for " + authority 162 + ". Fallback to openPreview"); 163 controller = getSurfaceController(authority, true); 164 } 165 166 return new RemotePreviewSession(mSurfaceCounter++, item.getId(), authority, controller, 167 previewVideoHolder, mMuteStatus, mPlayerControlsVisibilityStatus, mContext); 168 } 169 restorePreviewState(SurfaceHolder holder)170 private void restorePreviewState(SurfaceHolder holder) { 171 RemotePreviewSession session = createRemotePreviewSession(mCurrentPreviewState.item, 172 mCurrentPreviewState.viewHolder); 173 if (session == null) { 174 throw new IllegalStateException("Failed to restore preview state."); 175 } 176 177 mSessionMap.put(holder, session); 178 session.surfaceCreated(); 179 session.requestPlayMedia(); 180 } 181 getSessionForItem(Item item)182 private RemotePreviewSession getSessionForItem(Item item) { 183 String mediaId = item.getId(); 184 String authority = item.getContentUri().getAuthority(); 185 for (RemotePreviewSession session : mSessionMap.values()) { 186 if (session.getMediaId().equals(mediaId) && session.getAuthority().equals(authority)) { 187 return session; 188 } 189 } 190 return null; 191 } 192 getSessionForSurfaceId(int surfaceId)193 private RemotePreviewSession getSessionForSurfaceId(int surfaceId) { 194 for (RemotePreviewSession session : mSessionMap.values()) { 195 if (session.getSurfaceId() == surfaceId) { 196 return session; 197 } 198 } 199 return null; 200 } 201 202 @Nullable getSurfaceController(String authority, boolean localControllerFallback)203 private SurfaceControllerProxy getSurfaceController(String authority, 204 boolean localControllerFallback) { 205 if (mControllers.containsKey(authority)) { 206 return mControllers.get(authority); 207 } 208 209 SurfaceControllerProxy controller = null; 210 try { 211 controller = createController(authority, localControllerFallback); 212 if (controller != null) { 213 mControllers.put(authority, controller); 214 } 215 } catch (RuntimeException e) { 216 Log.e(TAG, "Could not create SurfaceController.", e); 217 } 218 return controller; 219 } 220 destroyAllSurfaceControllers()221 private void destroyAllSurfaceControllers() { 222 for (SurfaceControllerProxy controller : mControllers.values()) { 223 try { 224 controller.onDestroy(); 225 } catch (RemoteException e) { 226 Log.e(TAG, "Failed to destroy SurfaceController.", e); 227 } 228 } 229 mControllers.clear(); 230 } 231 createController(String authority, boolean localControllerFallback)232 private SurfaceControllerProxy createController(String authority, 233 boolean localControllerFallback) { 234 Log.i(TAG, "Creating new SurfaceController for authority: " + authority 235 + ". localControllerFallback: " + localControllerFallback); 236 Bundle extras = new Bundle(); 237 extras.putBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, true); 238 extras.putBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, mMuteStatus.isVolumeMuted()); 239 extras.putBinder(EXTRA_SURFACE_STATE_CALLBACK, mSurfaceStateChangedCallbackWrapper); 240 241 if (localControllerFallback) { 242 extras.putString(EXTRA_AUTHORITY, authority); 243 authority = PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY; 244 } 245 246 final Bundle surfaceControllerBundle = mContext.getContentResolver().call( 247 createSurfaceControllerUri(authority), 248 METHOD_CREATE_SURFACE_CONTROLLER, /* arg */ null, extras); 249 IBinder binder = surfaceControllerBundle.getBinder(EXTRA_SURFACE_CONTROLLER); 250 return binder != null ? new SurfaceControllerProxy( 251 ICloudMediaSurfaceController.Stub.asInterface(binder)) 252 : null; 253 } 254 255 /** 256 * Wrapper class for {@link android.provider.ICloudMediaSurfaceStateChangedCallback} interface 257 * implementation. 258 */ 259 private final class SurfaceStateChangedCallbackWrapper extends 260 ICloudMediaSurfaceStateChangedCallback.Stub { 261 262 @Override setPlaybackState(int surfaceId, @PlaybackState int playbackState, @Nullable Bundle playbackStateInfo)263 public void setPlaybackState(int surfaceId, @PlaybackState int playbackState, 264 @Nullable Bundle playbackStateInfo) { 265 Log.d(TAG, "Received onPlaybackEvent for surfaceId: " + surfaceId + 266 " ; playbackState: " + playbackState + " ; playbackStateInfo: " + 267 playbackStateInfo); 268 269 mMainThreadHandler.post(() -> { 270 final RemotePreviewSession session = getSessionForSurfaceId(surfaceId); 271 if (session == null) { 272 Log.w(TAG, "No RemotePreviewSession found."); 273 return; 274 } 275 session.setPlaybackState(playbackState, playbackStateInfo); 276 }); 277 } 278 } 279 280 private final class PreviewSurfaceCallback implements SurfaceHolder.Callback { 281 282 @Override surfaceCreated(SurfaceHolder holder)283 public void surfaceCreated(SurfaceHolder holder) { 284 Log.i(TAG, "Surface created: " + holder); 285 286 if (mIsInBackground) { 287 // This indicates that the app has just come to foreground, and we need to 288 // restore the preview state. 289 restorePreviewState(holder); 290 mIsInBackground = false; 291 return; 292 } 293 294 Surface surface = holder.getSurface(); 295 RemotePreviewSession session = mSessionMap.get(holder); 296 session.surfaceCreated(); 297 } 298 299 @Override surfaceChanged(SurfaceHolder holder, int format, int width, int height)300 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 301 Log.i(TAG, "Surface changed: " + holder + " format: " + format + " width: " + width 302 + " height: " + height); 303 304 RemotePreviewSession session = mSessionMap.get(holder); 305 session.surfaceChanged(format, width, height); 306 } 307 308 @Override surfaceDestroyed(SurfaceHolder holder)309 public void surfaceDestroyed(SurfaceHolder holder) { 310 Log.i(TAG, "Surface destroyed: " + holder); 311 312 RemotePreviewSession session = mSessionMap.get(holder); 313 session.surfaceDestroyed(); 314 mSessionMap.remove(holder); 315 } 316 } 317 318 private static final class ItemPreviewState { 319 Item item; 320 PreviewVideoHolder viewHolder; 321 } 322 } 323