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