1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.media;
6 
7 import android.annotation.SuppressLint;
8 import android.app.ActivityManager;
9 import android.app.PendingIntent;
10 import android.app.PictureInPictureParams;
11 import android.app.RemoteAction;
12 import android.content.BroadcastReceiver;
13 import android.content.Context;
14 import android.content.Intent;
15 import android.content.IntentFilter;
16 import android.content.res.Configuration;
17 import android.graphics.drawable.Icon;
18 import android.util.Rational;
19 import android.view.View;
20 import android.view.View.OnLayoutChangeListener;
21 import android.view.ViewGroup;
22 
23 import org.chromium.base.ContextUtils;
24 import org.chromium.base.MathUtils;
25 import org.chromium.base.annotations.CalledByNative;
26 import org.chromium.base.annotations.NativeMethods;
27 import org.chromium.chrome.R;
28 import org.chromium.chrome.browser.init.AsyncInitializationActivity;
29 import org.chromium.chrome.browser.tab.EmptyTabObserver;
30 import org.chromium.chrome.browser.tab.Tab;
31 import org.chromium.chrome.browser.tab.TabUtils;
32 import org.chromium.components.thinwebview.CompositorView;
33 import org.chromium.components.thinwebview.CompositorViewFactory;
34 import org.chromium.components.thinwebview.ThinWebViewConstraints;
35 import org.chromium.content_public.browser.MediaSession;
36 import org.chromium.content_public.browser.MediaSessionObserver;
37 import org.chromium.ui.base.ActivityWindowAndroid;
38 import org.chromium.ui.base.WindowAndroid;
39 
40 import java.util.ArrayList;
41 
42 /**
43  * A picture in picture activity which get created when requesting
44  * PiP from web API. The activity will connect to web API through
45  * OverlayWindowAndroid.
46  */
47 public class PictureInPictureActivity extends AsyncInitializationActivity {
48     private static final String ACTION_PLAY =
49             "org.chromium.chrome.browser.media.PictureInPictureActivity.Play";
50 
51     private static final float MAX_ASPECT_RATIO = 2.39f;
52     private static final float MIN_ASPECT_RATIO = 1 / 2.39f;
53 
54     private static long sNativeOverlayWindowAndroid;
55     private static Tab sInitiatorTab;
56     private static int sInitiatorTabTaskID;
57     private static InitiatorTabObserver sTabObserver;
58 
59     private CompositorView mCompositorView;
60     private MediaSessionObserver mMediaSessionObserver;
61 
62     private BroadcastReceiver mMediaSessionReceiver = new BroadcastReceiver() {
63         @Override
64         public void onReceive(Context context, Intent intent) {
65             if (sNativeOverlayWindowAndroid == 0) return;
66 
67             if (intent.getAction() == null || !intent.getAction().equals(ACTION_PLAY)) return;
68 
69             PictureInPictureActivityJni.get().play(sNativeOverlayWindowAndroid);
70         }
71     };
72 
73     private static class InitiatorTabObserver extends EmptyTabObserver {
74         private enum Status { OK, DESTROYED }
75         private PictureInPictureActivity mActivity;
76         private Status mStatus;
77 
InitiatorTabObserver()78         InitiatorTabObserver() {
79             mStatus = Status.OK;
80         }
81 
setActivity(PictureInPictureActivity activity)82         public void setActivity(PictureInPictureActivity activity) {
83             mActivity = activity;
84         }
85 
getStatus()86         public Status getStatus() {
87             return mStatus;
88         }
89 
90         @Override
onDestroyed(Tab tab)91         public void onDestroyed(Tab tab) {
92             if (tab.isClosing() || !isInitiatorTabAlive()) {
93                 mStatus = Status.DESTROYED;
94                 if (mActivity != null) mActivity.finish();
95             }
96         }
97 
98         @Override
onCrash(Tab tab)99         public void onCrash(Tab tab) {
100             mStatus = Status.DESTROYED;
101             if (mActivity != null) mActivity.finish();
102         }
103     }
104 
105     @Override
triggerLayoutInflation()106     protected void triggerLayoutInflation() {
107         onInitialLayoutInflationComplete();
108     }
109 
110     @Override
finishNativeInitialization()111     public void finishNativeInitialization() {
112         super.finishNativeInitialization();
113 
114         mCompositorView = CompositorViewFactory.create(
115                 this, getWindowAndroid(), new ThinWebViewConstraints());
116         addContentView(mCompositorView.getView(),
117                 new ViewGroup.LayoutParams(
118                         ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
119 
120         mCompositorView.getView().addOnLayoutChangeListener(new OnLayoutChangeListener() {
121             @Override
122             public void onLayoutChange(View v, int left, int top, int right, int bottom,
123                     int oldLeft, int oldTop, int oldRight, int oldBottom) {
124                 if (sNativeOverlayWindowAndroid == 0) return;
125 
126                 PictureInPictureActivityJni.get().onViewSizeChanged(
127                         sNativeOverlayWindowAndroid, right - left, bottom - top);
128             }
129         });
130 
131         PictureInPictureActivityJni.get().compositorViewCreated(
132                 sNativeOverlayWindowAndroid, mCompositorView);
133     }
134 
135     @Override
shouldStartGpuProcess()136     public boolean shouldStartGpuProcess() {
137         return true;
138     }
139 
140     @Override
141     @SuppressLint("NewAPI") // Picture-in-Picture API will not be enabled for oldver versions.
onStart()142     public void onStart() {
143         super.onStart();
144 
145         // Finish the activity if OverlayWindowAndroid has already been destroyed
146         // or InitiatorTab has been destroyed by user or crashed.
147         if (sNativeOverlayWindowAndroid == 0
148                 || sTabObserver.getStatus() == InitiatorTabObserver.Status.DESTROYED) {
149             this.finish();
150             return;
151         }
152 
153         sTabObserver.setActivity(this);
154 
155         registerReceiver(mMediaSessionReceiver, new IntentFilter(ACTION_PLAY));
156 
157         PictureInPictureActivityJni.get().onActivityStart(
158                 sNativeOverlayWindowAndroid, this, getWindowAndroid());
159 
160         // Add an observer to refresh the Picture-in-Picture params if the media
161         // session state changes.
162         MediaSession mediaSession = MediaSession.fromWebContents(sInitiatorTab.getWebContents());
163         mMediaSessionObserver = new MediaSessionObserver(mediaSession) {
164             @Override
165             public void mediaSessionStateChanged(boolean isControllable, boolean isSuspended) {
166                 setPictureInPictureParams(getPictureInPictureParams());
167             }
168         };
169 
170         enterPictureInPictureMode(getPictureInPictureParams());
171     }
172 
173     @Override
onStop()174     public void onStop() {
175         super.onStop();
176         if (mCompositorView != null) mCompositorView.destroy();
177     }
178 
179     @Override
onDestroy()180     public void onDestroy() {
181         super.onDestroy();
182         sNativeOverlayWindowAndroid = 0;
183         sInitiatorTab.removeObserver(sTabObserver);
184         sInitiatorTab = null;
185         sTabObserver = null;
186 
187         if (mMediaSessionObserver != null) {
188             mMediaSessionObserver.stopObserving();
189             mMediaSessionObserver = null;
190         }
191 
192         unregisterReceiver(mMediaSessionReceiver);
193     }
194 
195     @Override
onPictureInPictureModeChanged( boolean isInPictureInPictureMode, Configuration newConfig)196     public void onPictureInPictureModeChanged(
197             boolean isInPictureInPictureMode, Configuration newConfig) {
198         if (!isInPictureInPictureMode) this.finish();
199     }
200 
201     @Override
createWindowAndroid()202     protected ActivityWindowAndroid createWindowAndroid() {
203         return new ActivityWindowAndroid(this);
204     }
205 
206     @SuppressLint("NewApi")
isInitiatorTabAlive()207     private static boolean isInitiatorTabAlive() {
208         ActivityManager activityManager =
209                 (ActivityManager) ContextUtils.getApplicationContext().getSystemService(
210                         Context.ACTIVITY_SERVICE);
211         for (ActivityManager.AppTask appTask : activityManager.getAppTasks()) {
212             if (appTask.getTaskInfo().id == sInitiatorTabTaskID) return true;
213         }
214 
215         return false;
216     }
217 
218     @CalledByNative
close()219     private void close() {
220         this.finish();
221     }
222 
223     @SuppressLint("NewApi")
getPictureInPictureParams()224     private PictureInPictureParams getPictureInPictureParams() {
225         ArrayList<RemoteAction> actions = new ArrayList<>();
226 
227         // If the associated media session is not controllable then we should
228         // place a play button in the Picture-in-Picture window that will
229         // trigger playback.
230         if (mMediaSessionObserver != null
231                 && !mMediaSessionObserver.getMediaSession().isControllable()) {
232             PendingIntent pendingIntent = PendingIntent.getBroadcast(
233                     getApplicationContext(), 0, new Intent(ACTION_PLAY), 0);
234 
235             actions.add(new RemoteAction(Icon.createWithResource(getApplicationContext(),
236                                                  R.drawable.ic_play_arrow_white_36dp),
237                     getApplicationContext().getResources().getText(R.string.accessibility_play), "",
238                     pendingIntent));
239         }
240 
241         return new PictureInPictureParams.Builder().setActions(actions).build();
242     }
243 
244     @CalledByNative
245     @SuppressLint("NewApi")
updateVideoSize(int width, int height)246     private void updateVideoSize(int width, int height) {
247         PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder();
248 
249         float aspectRatio =
250                 MathUtils.clamp(width / (float) height, MIN_ASPECT_RATIO, MAX_ASPECT_RATIO);
251         width = (int) (height * aspectRatio);
252 
253         builder.setAspectRatio(new Rational(width, height));
254         setPictureInPictureParams(builder.build());
255     }
256 
257     @CalledByNative
createActivity(long nativeOverlayWindowAndroid, Object initiatorTab)258     private static void createActivity(long nativeOverlayWindowAndroid, Object initiatorTab) {
259         Context context = ContextUtils.getApplicationContext();
260         Intent intent = new Intent(context, PictureInPictureActivity.class);
261 
262         // Dissociate OverlayWindowAndroid if there is one already.
263         if (sNativeOverlayWindowAndroid != 0) {
264             PictureInPictureActivityJni.get().destroy(sNativeOverlayWindowAndroid);
265         }
266 
267         sNativeOverlayWindowAndroid = nativeOverlayWindowAndroid;
268         sInitiatorTab = (Tab) initiatorTab;
269         sInitiatorTabTaskID = TabUtils.getActivity(sInitiatorTab).getTaskId();
270 
271         sTabObserver = new InitiatorTabObserver();
272         sInitiatorTab.addObserver(sTabObserver);
273 
274         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
275         context.startActivity(intent);
276     }
277 
278     @CalledByNative
onWindowDestroyed(long nativeOverlayWindowAndroid)279     private static void onWindowDestroyed(long nativeOverlayWindowAndroid) {
280         if (sNativeOverlayWindowAndroid != nativeOverlayWindowAndroid) return;
281 
282         sNativeOverlayWindowAndroid = 0;
283     }
284 
285     @NativeMethods
286     interface Natives {
onActivityStart(long nativeOverlayWindowAndroid, PictureInPictureActivity self, WindowAndroid window)287         void onActivityStart(long nativeOverlayWindowAndroid, PictureInPictureActivity self,
288                 WindowAndroid window);
289 
destroy(long nativeOverlayWindowAndroid)290         void destroy(long nativeOverlayWindowAndroid);
291 
play(long nativeOverlayWindowAndroid)292         void play(long nativeOverlayWindowAndroid);
293 
compositorViewCreated(long nativeOverlayWindowAndroid, CompositorView compositorView)294         void compositorViewCreated(long nativeOverlayWindowAndroid, CompositorView compositorView);
295 
onViewSizeChanged(long nativeOverlayWindowAndroid, int width, int height)296         void onViewSizeChanged(long nativeOverlayWindowAndroid, int width, int height);
297     }
298 }
299