• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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