Commit 3a2e44cc authored by Simeon Anfinrud's avatar Simeon Anfinrud Committed by Commit Bot

[Chromecast] Reactive WebContents presentation.

No more detatchWebContentsIfAny() and similar helpers.
This boils the presentation of WebContents into a simple state
machine compatible with Observables.

This refactor includes covering the business logic of
CastWebContentsSurfaceHelper in unittests. The implementation
details of CastWebContentsView are still not unit-tested, but
are now better isolated from other logic.

Bug: Internal b/36777136
Test: cast_shell_junit_tests
Change-Id: Ia555b4d3ea9ee07607bf54cc339b950fe068d220
Reviewed-on: https://chromium-review.googlesource.com/1003412Reviewed-by: default avatarLuke Halliwell <halliwell@chromium.org>
Commit-Queue: Simeon Anfinrud <sanfin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#551464}
parent fce48036
...@@ -112,6 +112,7 @@ android_library("cast_shell_java") { ...@@ -112,6 +112,7 @@ android_library("cast_shell_java") {
"$java_src_dir/org/chromium/chromecast/shell/CastWebContentsIntentUtils.java", "$java_src_dir/org/chromium/chromecast/shell/CastWebContentsIntentUtils.java",
"$java_src_dir/org/chromium/chromecast/shell/CastWebContentsService.java", "$java_src_dir/org/chromium/chromecast/shell/CastWebContentsService.java",
"$java_src_dir/org/chromium/chromecast/shell/CastWebContentsSurfaceHelper.java", "$java_src_dir/org/chromium/chromecast/shell/CastWebContentsSurfaceHelper.java",
"$java_src_dir/org/chromium/chromecast/shell/CastWebContentsView.java",
"$java_src_dir/org/chromium/chromecast/shell/LogcatElision.java", "$java_src_dir/org/chromium/chromecast/shell/LogcatElision.java",
"$java_src_dir/org/chromium/chromecast/shell/ElidedLogcatProvider.java", "$java_src_dir/org/chromium/chromecast/shell/ElidedLogcatProvider.java",
] ]
...@@ -160,6 +161,7 @@ junit_binary("cast_shell_junit_tests") { ...@@ -160,6 +161,7 @@ junit_binary("cast_shell_junit_tests") {
"junit/src/org/chromium/chromecast/shell/CastWebContentsActivityTest.java", "junit/src/org/chromium/chromecast/shell/CastWebContentsActivityTest.java",
"junit/src/org/chromium/chromecast/shell/CastWebContentsComponentTest.java", "junit/src/org/chromium/chromecast/shell/CastWebContentsComponentTest.java",
"junit/src/org/chromium/chromecast/shell/CastWebContentsIntentUtilsTest.java", "junit/src/org/chromium/chromecast/shell/CastWebContentsIntentUtilsTest.java",
"junit/src/org/chromium/chromecast/shell/CastWebContentsSurfaceHelperTest.java",
"junit/src/org/chromium/chromecast/shell/LocalBroadcastReceiverScopeTest.java", "junit/src/org/chromium/chromecast/shell/LocalBroadcastReceiverScopeTest.java",
"junit/src/org/chromium/chromecast/shell/LogcatElisionUnitTest.java", "junit/src/org/chromium/chromecast/shell/LogcatElisionUnitTest.java",
"junit/src/org/chromium/chromecast/shell/ElidedLogcatProviderUnitTest.java", "junit/src/org/chromium/chromecast/shell/ElidedLogcatProviderUnitTest.java",
......
...@@ -7,7 +7,9 @@ package org.chromium.chromecast.shell; ...@@ -7,7 +7,9 @@ package org.chromium.chromecast.shell;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.graphics.Color;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
...@@ -19,6 +21,7 @@ import android.widget.Toast; ...@@ -19,6 +21,7 @@ import android.widget.Toast;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.base.annotations.RemovableInRelease; import org.chromium.base.annotations.RemovableInRelease;
import org.chromium.chromecast.base.Both; import org.chromium.chromecast.base.Both;
import org.chromium.chromecast.base.CastSwitches;
import org.chromium.chromecast.base.Controller; import org.chromium.chromecast.base.Controller;
import org.chromium.chromecast.base.Observable; import org.chromium.chromecast.base.Observable;
import org.chromium.chromecast.base.ScopeFactories; import org.chromium.chromecast.base.ScopeFactories;
...@@ -86,8 +89,12 @@ public class CastWebContentsActivity extends Activity { ...@@ -86,8 +89,12 @@ public class CastWebContentsActivity extends Activity {
setContentView(R.layout.cast_web_contents_activity); setContentView(R.layout.cast_web_contents_activity);
mSurfaceHelper = new CastWebContentsSurfaceHelper(this, /* hostActivity */ mSurfaceHelper = new CastWebContentsSurfaceHelper(this, /* hostActivity */
(FrameLayout) findViewById(R.id.web_contents_container), CastWebContentsView.onLayout(getApplicationContext(),
false /* showInFragment */); (FrameLayout) findViewById(R.id.web_contents_container),
CastSwitches.getSwitchValueColor(
CastSwitches.CAST_APP_BACKGROUND_COLOR, Color.BLACK),
mResumedState),
(Uri uri) -> mIsFinishingState.set("Delayed teardown for URI: " + uri));
})); }));
// Initialize the audio manager in onCreate() if tests haven't already. // Initialize the audio manager in onCreate() if tests haven't already.
...@@ -157,9 +164,6 @@ public class CastWebContentsActivity extends Activity { ...@@ -157,9 +164,6 @@ public class CastWebContentsActivity extends Activity {
if (DEBUG) Log.d(TAG, "onPause"); if (DEBUG) Log.d(TAG, "onPause");
super.onPause(); super.onPause();
mResumedState.reset(); mResumedState.reset();
if (mSurfaceHelper != null) {
mSurfaceHelper.onPause();
}
} }
@Override @Override
...@@ -167,9 +171,6 @@ public class CastWebContentsActivity extends Activity { ...@@ -167,9 +171,6 @@ public class CastWebContentsActivity extends Activity {
if (DEBUG) Log.d(TAG, "onResume"); if (DEBUG) Log.d(TAG, "onResume");
super.onResume(); super.onResume();
mResumedState.set(Unit.unit()); mResumedState.set(Unit.unit());
if (mSurfaceHelper != null) {
mSurfaceHelper.onResume();
}
} }
@Override @Override
......
...@@ -7,6 +7,8 @@ package org.chromium.chromecast.shell; ...@@ -7,6 +7,8 @@ package org.chromium.chromecast.shell;
import android.app.Fragment; import android.app.Fragment;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
...@@ -16,6 +18,9 @@ import android.widget.Toast; ...@@ -16,6 +18,9 @@ import android.widget.Toast;
import org.chromium.base.ContextUtils; import org.chromium.base.ContextUtils;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.chromecast.base.CastSwitches;
import org.chromium.chromecast.base.Controller;
import org.chromium.chromecast.base.Unit;
/** /**
* Fragment for displaying a WebContents in CastShell. * Fragment for displaying a WebContents in CastShell.
...@@ -31,14 +36,12 @@ import org.chromium.base.Log; ...@@ -31,14 +36,12 @@ import org.chromium.base.Log;
public class CastWebContentsFragment extends Fragment { public class CastWebContentsFragment extends Fragment {
private static final String TAG = "cr_CastWebContentFrg"; private static final String TAG = "cr_CastWebContentFrg";
private Context mPackageContext; private final Controller<Unit> mResumedState = new Controller<>();
private Context mPackageContext;
private CastWebContentsSurfaceHelper mSurfaceHelper; private CastWebContentsSurfaceHelper mSurfaceHelper;
private View mFragmentRootView; private View mFragmentRootView;
private String mAppId; private String mAppId;
private int mInitialVisiblityPriority; private int mInitialVisiblityPriority;
@Override @Override
...@@ -61,12 +64,11 @@ public class CastWebContentsFragment extends Fragment { ...@@ -61,12 +64,11 @@ public class CastWebContentsFragment extends Fragment {
} }
if (mFragmentRootView == null) { if (mFragmentRootView == null) {
mFragmentRootView = inflater.cloneInContext(getContext()) mFragmentRootView = inflater.cloneInContext(getContext())
.inflate(R.layout.cast_web_contents_activity, null); .inflate(R.layout.cast_web_contents_activity, null);
} }
return mFragmentRootView; return mFragmentRootView;
} }
@Override @Override
public Context getContext() { public Context getContext() {
if (mPackageContext == null) { if (mPackageContext == null) {
...@@ -88,8 +90,13 @@ public class CastWebContentsFragment extends Fragment { ...@@ -88,8 +90,13 @@ public class CastWebContentsFragment extends Fragment {
} }
mSurfaceHelper = new CastWebContentsSurfaceHelper(getActivity(), /* hostActivity */ mSurfaceHelper = new CastWebContentsSurfaceHelper(getActivity(), /* hostActivity */
(FrameLayout) getView().findViewById(R.id.web_contents_container), CastWebContentsView.onLayout(getContext(),
true /* showInFragment */); (FrameLayout) getView().findViewById(R.id.web_contents_container),
CastSwitches.getSwitchValueColor(
CastSwitches.CAST_APP_BACKGROUND_COLOR, Color.BLACK),
mResumedState),
(Uri uri) -> sendIntentSync(CastWebContentsIntentUtils.onWebContentStopped(uri)));
Bundle bundle = getArguments(); Bundle bundle = getArguments();
CastWebContentsSurfaceHelper.StartParams params = CastWebContentsSurfaceHelper.StartParams params =
CastWebContentsSurfaceHelper.StartParams.fromBundle(bundle); CastWebContentsSurfaceHelper.StartParams.fromBundle(bundle);
...@@ -106,18 +113,14 @@ public class CastWebContentsFragment extends Fragment { ...@@ -106,18 +113,14 @@ public class CastWebContentsFragment extends Fragment {
public void onPause() { public void onPause() {
Log.d(TAG, "onPause"); Log.d(TAG, "onPause");
super.onPause(); super.onPause();
if (mSurfaceHelper != null) { mResumedState.reset();
mSurfaceHelper.onPause();
}
} }
@Override @Override
public void onResume() { public void onResume() {
Log.d(TAG, "onResume"); Log.d(TAG, "onResume");
super.onResume(); super.onResume();
if (mSurfaceHelper != null) { mResumedState.set(Unit.unit());
mSurfaceHelper.onResume();
}
} }
@Override @Override
......
...@@ -13,13 +13,11 @@ import android.widget.Toast; ...@@ -13,13 +13,11 @@ import android.widget.Toast;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.base.annotations.JNINamespace; import org.chromium.base.annotations.JNINamespace;
import org.chromium.chromecast.base.Both;
import org.chromium.chromecast.base.Controller; import org.chromium.chromecast.base.Controller;
import org.chromium.chromecast.base.Observable;
import org.chromium.chromecast.base.Unit; import org.chromium.chromecast.base.Unit;
import org.chromium.components.content_view.ContentView;
import org.chromium.content_public.browser.ContentViewCore;
import org.chromium.content_public.browser.WebContents; import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
/** /**
* Service for "displaying" a WebContents in CastShell. * Service for "displaying" a WebContents in CastShell.
...@@ -34,11 +32,31 @@ public class CastWebContentsService extends Service { ...@@ -34,11 +32,31 @@ public class CastWebContentsService extends Service {
private static final int CAST_NOTIFICATION_ID = 100; private static final int CAST_NOTIFICATION_ID = 100;
private final Controller<Unit> mLifetimeController = new Controller<>(); private final Controller<Unit> mLifetimeController = new Controller<>();
private final Controller<WebContents> mWebContentsState = new Controller<>();
private String mInstanceId; private String mInstanceId;
private CastAudioManager mAudioManager; private CastAudioManager mAudioManager;
private WindowAndroid mWindow;
private ContentViewCore mContentViewCore; {
private ContentView mContentView; // Construct an Observable that is deactivated when either mWebContentsState or
// mLifetimeController is reset, and has the activation data of mWebContentsState.
Observable<WebContents> hasWebContentsState =
mWebContentsState.and(mLifetimeController).map(Both::getFirst);
// React to web contents by presenting them in a headless view.
hasWebContentsState.watch(CastWebContentsView.withoutLayout(this));
hasWebContentsState.watch(() -> {
if (DEBUG) Log.d(TAG, "show web contents");
// TODO(thoren): Notification.Builder(Context) is deprecated in O. Use the
// (Context, String) constructor when CastWebContentsService starts supporting O.
Notification notification = new Notification.Builder(this).build();
startForeground(CAST_NOTIFICATION_ID, notification);
return () -> {
if (DEBUG) Log.d(TAG, "detach web contents");
stopForeground(true /*removeNotification*/);
// Inform CastContentWindowAndroid we're detaching.
CastWebContentsComponent.onComponentClosed(mInstanceId);
};
});
}
protected void handleIntent(Intent intent) { protected void handleIntent(Intent intent) {
intent.setExtrasClassLoader(WebContents.class.getClassLoader()); intent.setExtrasClassLoader(WebContents.class.getClassLoader());
...@@ -50,8 +68,7 @@ public class CastWebContentsService extends Service { ...@@ -50,8 +68,7 @@ public class CastWebContentsService extends Service {
return; return;
} }
detachWebContentsIfAny(); mWebContentsState.set(webContents);
showWebContents(webContents);
} }
@Override @Override
...@@ -65,14 +82,11 @@ public class CastWebContentsService extends Service { ...@@ -65,14 +82,11 @@ public class CastWebContentsService extends Service {
@Override @Override
public void onCreate() { public void onCreate() {
if (DEBUG) Log.d(TAG, "onCreate"); if (DEBUG) Log.d(TAG, "onCreate");
if (!CastBrowserHelper.initializeBrowser(getApplicationContext())) { if (!CastBrowserHelper.initializeBrowser(getApplicationContext())) {
Toast.makeText(this, R.string.browser_process_initialization_failed, Toast.LENGTH_SHORT) Toast.makeText(this, R.string.browser_process_initialization_failed, Toast.LENGTH_SHORT)
.show(); .show();
stopSelf(); stopSelf();
} }
mWindow = new WindowAndroid(this);
CastAudioManager.getAudioManager(this).requestAudioFocusWhen( CastAudioManager.getAudioManager(this).requestAudioFocusWhen(
mLifetimeController, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); mLifetimeController, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
mLifetimeController.set(Unit.unit()); mLifetimeController.set(Unit.unit());
...@@ -81,42 +95,7 @@ public class CastWebContentsService extends Service { ...@@ -81,42 +95,7 @@ public class CastWebContentsService extends Service {
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
if (DEBUG) Log.d(TAG, "onBind"); if (DEBUG) Log.d(TAG, "onBind");
handleIntent(intent); handleIntent(intent);
return null; return null;
} }
// Sets webContents to be the currently displayed webContents.
// TODO(thoren): Notification.Builder(Context) is deprecated in O. Use the (Context, String)
// constructor when CastWebContentsService starts supporting O.
@SuppressWarnings("deprecation")
private void showWebContents(WebContents webContents) {
if (DEBUG) Log.d(TAG, "showWebContents");
Notification notification = new Notification.Builder(this).build();
startForeground(CAST_NOTIFICATION_ID, notification);
mContentView = ContentView.createContentView(this, webContents);
// TODO(derekjchow): productVersion
mContentViewCore = ContentViewCore.create(this, "", webContents,
ViewAndroidDelegate.createBasicDelegate(mContentView), mContentView, mWindow);
// Enable display of current webContents.
webContents.onShow();
}
// Remove the currently displayed webContents. no-op if nothing is being displayed.
private void detachWebContentsIfAny() {
if (DEBUG) Log.d(TAG, "detachWebContentsIfAny");
stopForeground(true /*removeNotification*/);
if (mContentView != null) {
mContentView = null;
mContentViewCore = null;
// Inform CastContentWindowAndroid we're detaching.
CastWebContentsComponent.onComponentClosed(mInstanceId);
}
}
} }
...@@ -5,29 +5,26 @@ ...@@ -5,29 +5,26 @@
package org.chromium.chromecast.shell; package org.chromium.chromecast.shell;
import android.app.Activity; import android.app.Activity;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.graphics.Color;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.widget.FrameLayout;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.base.annotations.JNINamespace; import org.chromium.base.annotations.JNINamespace;
import org.chromium.chromecast.base.CastSwitches; import org.chromium.base.annotations.RemovableInRelease;
import org.chromium.chromecast.base.Both;
import org.chromium.chromecast.base.Consumer;
import org.chromium.chromecast.base.Controller; import org.chromium.chromecast.base.Controller;
import org.chromium.chromecast.base.Observable;
import org.chromium.chromecast.base.ScopeFactories;
import org.chromium.chromecast.base.ScopeFactory;
import org.chromium.chromecast.base.Unit; import org.chromium.chromecast.base.Unit;
import org.chromium.components.content_view.ContentView;
import org.chromium.content.browser.ActivityContentVideoViewEmbedder; import org.chromium.content.browser.ActivityContentVideoViewEmbedder;
import org.chromium.content.browser.ContentVideoViewEmbedder; import org.chromium.content.browser.ContentVideoViewEmbedder;
import org.chromium.content.browser.ContentViewRenderView;
import org.chromium.content_public.browser.ContentViewCore;
import org.chromium.content_public.browser.WebContents; import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
/** /**
* A util class for CastWebContentsActivity and CastWebContentsFragment to show * A util class for CastWebContentsActivity and CastWebContentsFragment to show
...@@ -47,20 +44,18 @@ class CastWebContentsSurfaceHelper { ...@@ -47,20 +44,18 @@ class CastWebContentsSurfaceHelper {
private static final int TEARDOWN_GRACE_PERIOD_TIMEOUT_MILLIS = 300; private static final int TEARDOWN_GRACE_PERIOD_TIMEOUT_MILLIS = 300;
private final Controller<Unit> mResumedState = new Controller<>(); private final Controller<Unit> mCreatedState = new Controller<>();
// Activated when we have an instance URI, from which the instance ID is derived.
// Is (re)activated when new StartParams are received, and deactivated in onDestroy().
private final Controller<Uri> mHasUriState = new Controller<>(); private final Controller<Uri> mHasUriState = new Controller<>();
// Activated when we have WebContents to display.
private final Controller<WebContents> mWebContentsState = new Controller<>();
private final Activity mHostActivity; private final Consumer<Uri> mFinishCallback;
private final boolean mShowInFragment;
private final Handler mHandler; private final Handler mHandler;
private final FrameLayout mCastWebContentsLayout;
private Uri mUri;
private String mInstanceId; private String mInstanceId;
private ContentViewRenderView mContentViewRenderView; private ContentVideoViewEmbedderSetter mContentVideoViewEmbedderSetter;
private WindowAndroid mWindow;
private ContentViewCore mContentViewCore;
private ContentView mContentView;
// TODO(vincentli) interrupt touch event from Fragment's root view when it's false. // TODO(vincentli) interrupt touch event from Fragment's root view when it's false.
private boolean mTouchInputEnabled = false; private boolean mTouchInputEnabled = false;
...@@ -111,25 +106,24 @@ class CastWebContentsSurfaceHelper { ...@@ -111,25 +106,24 @@ class CastWebContentsSurfaceHelper {
/** /**
* @param hostActivity Activity hosts the view showing WebContents * @param hostActivity Activity hosts the view showing WebContents
* @param castWebContentsLayout view group to add ContentViewRenderView and ContentView * @param webContentsView A ScopeFactory that displays incoming WebContents.
* @param showInFragment true if the cast web view is hosted by a CastWebContentsFragment * @param finishCallback Invoked to tell host to finish.
*/ */
CastWebContentsSurfaceHelper( CastWebContentsSurfaceHelper(Activity hostActivity, ScopeFactory<WebContents> webContentsView,
Activity hostActivity, FrameLayout castWebContentsLayout, boolean showInFragment) { Consumer<Uri> finishCallback) {
mHostActivity = hostActivity; mFinishCallback = finishCallback;
mShowInFragment = showInFragment;
mCastWebContentsLayout = castWebContentsLayout;
mCastWebContentsLayout.setBackgroundColor(CastSwitches.getSwitchValueColor(
CastSwitches.CAST_APP_BACKGROUND_COLOR, Color.BLACK));
mHandler = new Handler(); mHandler = new Handler();
mContentVideoViewEmbedderSetter =
(WebContents webContents, ContentVideoViewEmbedder embedder)
-> nativeSetContentVideoViewEmbedder(webContents, embedder);
// Receive broadcasts indicating the screen turned off while we have active WebContents. // Receive broadcasts indicating the screen turned off while we have active WebContents.
mHasUriState.watch(() -> { mHasUriState.watch((Uri uri) -> {
IntentFilter filter = new IntentFilter(); IntentFilter filter = new IntentFilter();
filter.addAction(CastIntents.ACTION_SCREEN_OFF); filter.addAction(CastIntents.ACTION_SCREEN_OFF);
return new LocalBroadcastReceiverScope(filter, (Intent intent) -> { return new LocalBroadcastReceiverScope(filter, (Intent intent) -> {
detachWebContentsIfAny(); mWebContentsState.reset();
maybeFinishLater(); maybeFinishLater(uri);
}); });
}); });
...@@ -141,17 +135,16 @@ class CastWebContentsSurfaceHelper { ...@@ -141,17 +135,16 @@ class CastWebContentsSurfaceHelper {
String intentUri = CastWebContentsIntentUtils.getUriString(intent); String intentUri = CastWebContentsIntentUtils.getUriString(intent);
Log.d(TAG, "Intent action=" + intent.getAction() + "; URI=" + intentUri); Log.d(TAG, "Intent action=" + intent.getAction() + "; URI=" + intentUri);
if (!uri.toString().equals(intentUri)) { if (!uri.toString().equals(intentUri)) {
Log.d(TAG, "Current URI=" + mUri + "; intent URI=" + intentUri); Log.d(TAG, "Current URI=" + uri + "; intent URI=" + intentUri);
return; return;
} }
detachWebContentsIfAny(); mWebContentsState.reset();
maybeFinishLater(); maybeFinishLater(uri);
}); });
}); });
// Receive broadcasts indicating that touch input should be enabled. // Receive broadcasts indicating that touch input should be enabled.
// TODO(yyzhong) Handle this intent in an external activity hosting a cast fragment as // TODO(yyzhong) Handle this intent in an external activity hosting a cast fragment as well.
// well.
mHasUriState.watch((Uri uri) -> { mHasUriState.watch((Uri uri) -> {
IntentFilter filter = new IntentFilter(); IntentFilter filter = new IntentFilter();
filter.addAction(CastWebContentsIntentUtils.ACTION_ENABLE_TOUCH_INPUT); filter.addAction(CastWebContentsIntentUtils.ACTION_ENABLE_TOUCH_INPUT);
...@@ -159,141 +152,64 @@ class CastWebContentsSurfaceHelper { ...@@ -159,141 +152,64 @@ class CastWebContentsSurfaceHelper {
String intentUri = CastWebContentsIntentUtils.getUriString(intent); String intentUri = CastWebContentsIntentUtils.getUriString(intent);
Log.d(TAG, "Intent action=" + intent.getAction() + "; URI=" + intentUri); Log.d(TAG, "Intent action=" + intent.getAction() + "; URI=" + intentUri);
if (!uri.toString().equals(intentUri)) { if (!uri.toString().equals(intentUri)) {
Log.d(TAG, "Current URI=" + mUri + "; intent URI=" + intentUri); Log.d(TAG, "Current URI=" + uri + "; intent URI=" + intentUri);
return; return;
} }
mTouchInputEnabled = CastWebContentsIntentUtils.isTouchable(intent); mTouchInputEnabled = CastWebContentsIntentUtils.isTouchable(intent);
}); });
}); });
mResumedState.watch(() -> { // webContentsView is responsible for displaying each new WebContents.
if (mContentViewCore != null) { mWebContentsState.watch(webContentsView);
mContentViewCore.onResume();
} // Miscellaneous actions responding to WebContents lifecycle.
return () -> { mWebContentsState.watch((WebContents webContents) -> {
if (mContentViewCore != null) { // Set ContentVideoViewEmbedder to allow video playback.
mContentViewCore.onPause(); mContentVideoViewEmbedderSetter.set(
} webContents, new ActivityContentVideoViewEmbedder(hostActivity));
}; // Whenever our app is visible, volume controls should modify the music stream.
// For more information read:
// http://developer.android.com/training/managing-audio/volume-playback.html
hostActivity.setVolumeControlStream(AudioManager.STREAM_MUSIC);
// Notify CastWebContentsComponent when closed.
return () -> CastWebContentsComponent.onComponentClosed(mInstanceId);
}); });
// When onDestroy() is called after onNewStartParams(), log and reset StartParams states.
mHasUriState.andThen(Observable.not(mCreatedState))
.map(Both::getFirst)
.watch(ScopeFactories.onEnter((Uri uri) -> {
Log.d(TAG, "onDestroy: " + uri);
mWebContentsState.reset();
mHasUriState.reset();
}));
mCreatedState.set(Unit.unit());
} }
void onNewStartParams(final StartParams params) { void onNewStartParams(final StartParams params) {
mTouchInputEnabled = params.touchInputEnabled; mTouchInputEnabled = params.touchInputEnabled;
Log.d(TAG, "content_uri=" + params.uri); Log.d(TAG, "content_uri=" + params.uri);
mUri = params.uri; mHasUriState.set(params.uri);
mWebContentsState.set(params.webContents);
// Mutate instance ID cache only after observers have reacted to new web contents.
mInstanceId = params.uri.getPath(); mInstanceId = params.uri.getPath();
// Whenever our app is visible, volume controls should modify the music stream.
// For more information read:
// http://developer.android.com/training/managing-audio/volume-playback.html
mHostActivity.setVolumeControlStream(AudioManager.STREAM_MUSIC);
mHasUriState.set(mUri);
showWebContents(params.webContents);
} }
// Closes this activity if a new WebContents is not being displayed. // Closes this activity if a new WebContents is not being displayed.
private void maybeFinishLater() { private void maybeFinishLater(Uri uri) {
Log.d(TAG, "maybeFinishLater: " + mUri); Log.d(TAG, "maybeFinishLater: " + uri);
final String currentInstanceId = mInstanceId; final String currentInstanceId = mInstanceId;
mHandler.postDelayed(new Runnable() { mHandler.postDelayed(() -> {
@Override if (currentInstanceId != null && currentInstanceId.equals(mInstanceId)) {
public void run() { mFinishCallback.accept(uri);
if (currentInstanceId != null && currentInstanceId.equals(mInstanceId)) {
if (mShowInFragment) {
Log.d(TAG, "Sending intent: ON_WEB_CONTENT_STOPPED: URI=" + mUri);
CastWebContentsIntentUtils.getLocalBroadcastManager().sendBroadcastSync(
CastWebContentsIntentUtils.onWebContentStopped(mUri));
} else {
Log.d(TAG, "Finishing cast content activity of URI:" + mUri);
mHostActivity.finish();
}
}
} }
}, TEARDOWN_GRACE_PERIOD_TIMEOUT_MILLIS); }, TEARDOWN_GRACE_PERIOD_TIMEOUT_MILLIS);
} }
private Activity getActivity() {
return mHostActivity;
}
// Sets webContents to be the currently displayed webContents.
private void showWebContents(WebContents webContents) {
Log.d(TAG, "showWebContents: " + mUri);
detachWebContentsIfAny();
// Set ContentVideoViewEmbedder to allow video playback.
nativeSetContentVideoViewEmbedder(
webContents, new ActivityContentVideoViewEmbedder(getActivity()));
mWindow = new WindowAndroid(getActivity());
mContentViewRenderView = new ContentViewRenderView(getActivity()) {
@Override
protected void onReadyToRender() {
setOverlayVideoMode(true);
}
};
mContentViewRenderView.onNativeLibraryLoaded(mWindow);
// Setting the background color avoids rendering a white splash screen
// before the players are loaded. See https://crbug/307113 for details.
mContentViewRenderView.setSurfaceViewBackgroundColor(CastSwitches.getSwitchValueColor(
CastSwitches.CAST_APP_BACKGROUND_COLOR, Color.BLACK));
mCastWebContentsLayout.addView(mContentViewRenderView,
new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT));
Context context = getActivity().getApplicationContext();
mContentView = ContentView.createContentView(context, webContents);
// TODO(derekjchow): productVersion
mContentViewCore = ContentViewCore.create(context, "", webContents,
ViewAndroidDelegate.createBasicDelegate(mContentView), mContentView, mWindow);
// Enable display of current webContents.
webContents.onShow();
mCastWebContentsLayout.addView(mContentView,
new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT));
mContentView.setFocusable(true);
mContentView.requestFocus();
mContentViewRenderView.setCurrentWebContents(webContents);
}
// Remove the currently displayed webContents. no-op if nothing is being displayed.
private void detachWebContentsIfAny() {
Log.d(TAG, "Maybe detach web contents if any: " + mUri);
if (mContentView != null) {
mCastWebContentsLayout.removeView(mContentView);
mCastWebContentsLayout.removeView(mContentViewRenderView);
mContentViewCore.destroy();
mContentViewRenderView.destroy();
mWindow.destroy();
mContentView = null;
mContentViewCore = null;
mContentViewRenderView = null;
mWindow = null;
CastWebContentsComponent.onComponentClosed(mInstanceId);
Log.d(TAG, "Detach web contents done: " + mUri);
}
}
void onPause() {
Log.d(TAG, "onPause: " + mUri);
mResumedState.reset();
}
void onResume() {
Log.d(TAG, "onResume: " + mUri);
mResumedState.set(Unit.unit());
}
// Destroys all resources. After calling this method, this object must be dropped. // Destroys all resources. After calling this method, this object must be dropped.
void onDestroy() { void onDestroy() {
Log.d(TAG, "onDestroy: " + mUri); mCreatedState.reset();
detachWebContentsIfAny();
mHasUriState.reset();
} }
String getInstanceId() { String getInstanceId() {
...@@ -304,6 +220,15 @@ class CastWebContentsSurfaceHelper { ...@@ -304,6 +220,15 @@ class CastWebContentsSurfaceHelper {
return mTouchInputEnabled; return mTouchInputEnabled;
} }
@RemovableInRelease
void setContentVideoViewEmbedderSetterForTesting(ContentVideoViewEmbedderSetter cvves) {
mContentVideoViewEmbedderSetter = cvves;
}
interface ContentVideoViewEmbedderSetter {
void set(WebContents webContents, ContentVideoViewEmbedder embedder);
}
private native void nativeSetContentVideoViewEmbedder( private native void nativeSetContentVideoViewEmbedder(
WebContents webContents, ContentVideoViewEmbedder embedder); WebContents webContents, ContentVideoViewEmbedder embedder);
} }
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chromecast.shell;
import android.content.Context;
import android.widget.FrameLayout;
import org.chromium.chromecast.base.Observable;
import org.chromium.chromecast.base.ScopeFactory;
import org.chromium.components.content_view.ContentView;
import org.chromium.content.browser.ContentViewRenderView;
import org.chromium.content_public.browser.ContentViewCore;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
class CastWebContentsView {
public static ScopeFactory<WebContents> onLayout(
Context context, FrameLayout layout, int backgroundColor, Observable<?> resumedState) {
layout.setBackgroundColor(backgroundColor);
return (WebContents webContents) -> {
WindowAndroid window = new WindowAndroid(context);
ContentViewRenderView contentViewRenderView = new ContentViewRenderView(context) {
@Override
protected void onReadyToRender() {
setOverlayVideoMode(true);
}
};
contentViewRenderView.onNativeLibraryLoaded(window);
contentViewRenderView.setSurfaceViewBackgroundColor(backgroundColor);
FrameLayout.LayoutParams matchParent = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
layout.addView(contentViewRenderView, matchParent);
ContentView contentView = ContentView.createContentView(context, webContents);
// TODO(derekjchow): productVersion
ContentViewCore contentViewCore = ContentViewCore.create(context, "", webContents,
ViewAndroidDelegate.createBasicDelegate(contentView), contentView, window);
// Enable display of current webContents.
webContents.onShow();
layout.addView(contentView, matchParent);
contentView.setFocusable(true);
contentView.requestFocus();
contentViewRenderView.setCurrentWebContents(webContents);
resumedState.watch(() -> {
contentViewCore.onResume();
return contentViewCore::onPause;
});
return () -> {
layout.removeView(contentView);
layout.removeView(contentViewRenderView);
contentViewCore.destroy();
contentViewRenderView.destroy();
window.destroy();
};
};
}
public static ScopeFactory<WebContents> withoutLayout(Context context) {
return (WebContents webContents) -> {
WindowAndroid window = new WindowAndroid(context);
ContentView contentView = ContentView.createContentView(context, webContents);
// TODO(derekjchow): productVersion
ContentViewCore contentViewCore = ContentViewCore.create(context, "", webContents,
ViewAndroidDelegate.createBasicDelegate(contentView), contentView, window);
// Enable display of current webContents.
webContents.onShow();
return webContents::onHide;
};
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chromecast.shell;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Activity;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.net.Uri;
import android.os.PatternMatcher;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
import org.chromium.chromecast.base.Consumer;
import org.chromium.chromecast.base.Scope;
import org.chromium.chromecast.base.ScopeFactory;
import org.chromium.chromecast.shell.CastWebContentsSurfaceHelper.ContentVideoViewEmbedderSetter;
import org.chromium.chromecast.shell.CastWebContentsSurfaceHelper.StartParams;
import org.chromium.content.browser.ContentVideoViewEmbedder;
import org.chromium.content_public.browser.WebContents;
import org.chromium.testing.local.LocalRobolectricTestRunner;
import java.util.ArrayList;
import java.util.List;
/**
* Tests for CastWebContentsSurfaceHelper.
*/
@RunWith(LocalRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class CastWebContentsSurfaceHelperTest {
private @Mock Activity mActivity;
private @Mock ScopeFactory<WebContents> mWebContentsView;
private @Mock Consumer<Uri> mFinishCallback;
private CastWebContentsSurfaceHelper mSurfaceHelper;
private @Mock ContentVideoViewEmbedderSetter mContentVideoViewEmbedderSetter;
private static class StartParamsBuilder {
private String mId = "0";
private WebContents mWebContents = mock(WebContents.class);
private boolean mIsTouchInputEnabled = false;
public StartParamsBuilder withId(String id) {
mId = id;
return this;
}
public StartParamsBuilder withWebContents(WebContents webContents) {
mWebContents = webContents;
return this;
}
public StartParamsBuilder enableTouchInput(boolean enableTouchInput) {
mIsTouchInputEnabled = enableTouchInput;
return this;
}
public StartParams build() {
return new StartParams(CastWebContentsIntentUtils.getInstanceUri(mId), mWebContents,
mIsTouchInputEnabled);
}
}
private static class BroadcastAsserter {
private final Intent mExpectedIntent;
private final List<Intent> mReceivedIntents = new ArrayList<>();
private final LocalBroadcastReceiverScope mReceiver;
public BroadcastAsserter(Intent looksLike) {
mExpectedIntent = looksLike;
IntentFilter filter = new IntentFilter();
Uri instanceUri = looksLike.getData();
filter.addDataScheme(instanceUri.getScheme());
filter.addDataAuthority(instanceUri.getAuthority(), null);
filter.addDataPath(instanceUri.getPath(), PatternMatcher.PATTERN_LITERAL);
filter.addAction(looksLike.getAction());
mReceiver = new LocalBroadcastReceiverScope(filter, mReceivedIntents::add);
}
public void verify() {
assertEquals(1, mReceivedIntents.size());
Intent receivedIntent = mReceivedIntents.get(0);
assertEquals(mExpectedIntent.getAction(), receivedIntent.getAction());
assertEquals(mExpectedIntent.getData(), receivedIntent.getData());
mReceiver.close();
}
}
private void sendBroadcastSync(Intent intent) {
CastWebContentsIntentUtils.getLocalBroadcastManager().sendBroadcastSync(intent);
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mSurfaceHelper =
new CastWebContentsSurfaceHelper(mActivity, mWebContentsView, mFinishCallback);
mSurfaceHelper.setContentVideoViewEmbedderSetterForTesting(mContentVideoViewEmbedderSetter);
}
@Test
public void testActivatesWebContentsViewOnNewStartParams() {
WebContents webContents = mock(WebContents.class);
StartParams params = new StartParamsBuilder().withWebContents(webContents).build();
mSurfaceHelper.onNewStartParams(params);
verify(mWebContentsView).create(webContents);
}
@Test
public void testDeactivatesOldWebContentsViewOnNewStartParams() {
WebContents webContents1 = mock(WebContents.class);
StartParams params1 =
new StartParamsBuilder().withId("1").withWebContents(webContents1).build();
WebContents webContents2 = mock(WebContents.class);
StartParams params2 =
new StartParamsBuilder().withId("2").withWebContents(webContents2).build();
Scope scope1 = mock(Scope.class);
Scope scope2 = mock(Scope.class);
when(mWebContentsView.create(webContents1)).thenReturn(scope1);
when(mWebContentsView.create(webContents2)).thenReturn(scope2);
mSurfaceHelper.onNewStartParams(params1);
verify(mWebContentsView).create(webContents1);
mSurfaceHelper.onNewStartParams(params2);
verify(scope1).close();
verify(mWebContentsView).create(webContents2);
}
@Test
public void testSetsContentVideoViewEmbedderOnNewStartParams() {
WebContents webContents = mock(WebContents.class);
StartParams params = new StartParamsBuilder().withWebContents(webContents).build();
mSurfaceHelper.onNewStartParams(params);
verify(mContentVideoViewEmbedderSetter)
.set(eq(webContents), any(ContentVideoViewEmbedder.class));
}
@Test
public void testIsTouchInputEnabled() {
assertFalse(mSurfaceHelper.isTouchInputEnabled());
StartParams params1 = new StartParamsBuilder().enableTouchInput(true).build();
mSurfaceHelper.onNewStartParams(params1);
assertTrue(mSurfaceHelper.isTouchInputEnabled());
StartParams params2 = new StartParamsBuilder().enableTouchInput(false).build();
mSurfaceHelper.onNewStartParams(params2);
assertFalse(mSurfaceHelper.isTouchInputEnabled());
}
@Test
public void testInstanceId() {
assertNull(mSurfaceHelper.getInstanceId());
StartParams params1 = new StartParamsBuilder().withId("/0").build();
mSurfaceHelper.onNewStartParams(params1);
assertEquals("/0", mSurfaceHelper.getInstanceId());
StartParams params2 = new StartParamsBuilder().withId("/1").build();
mSurfaceHelper.onNewStartParams(params2);
assertEquals("/1", mSurfaceHelper.getInstanceId());
}
@Test
public void testScreenOffResetsWebContentsView() {
WebContents webContents = mock(WebContents.class);
StartParams params = new StartParamsBuilder().withWebContents(webContents).build();
Scope scope = mock(Scope.class);
when(mWebContentsView.create(webContents)).thenReturn(scope);
mSurfaceHelper.onNewStartParams(params);
// Send SCREEN_OFF broadcast.
sendBroadcastSync(new Intent(CastIntents.ACTION_SCREEN_OFF));
verify(scope).close();
}
@Test
public void testStopWebContentsIntentResetsWebContentsView() {
WebContents webContents = mock(WebContents.class);
StartParams params =
new StartParamsBuilder().withId("3").withWebContents(webContents).build();
Scope scope = mock(Scope.class);
when(mWebContentsView.create(webContents)).thenReturn(scope);
mSurfaceHelper.onNewStartParams(params);
// Send notification to stop web content
sendBroadcastSync(CastWebContentsIntentUtils.requestStopWebContents("3"));
verify(scope).close();
}
@Test
public void testStopWebContentsIntentWithWrongIdIsIgnored() {
WebContents webContents = mock(WebContents.class);
StartParams params =
new StartParamsBuilder().withId("2").withWebContents(webContents).build();
Scope scope = mock(Scope.class);
when(mWebContentsView.create(webContents)).thenReturn(scope);
mSurfaceHelper.onNewStartParams(params);
// Send notification to stop web content with different ID.
sendBroadcastSync(CastWebContentsIntentUtils.requestStopWebContents("4"));
verify(scope, never()).close();
}
@Test
public void testEnableTouchInputIntentMutatesIsTouchInputEnabled() {
WebContents webContents = mock(WebContents.class);
StartParams params = new StartParamsBuilder().withId("1").enableTouchInput(false).build();
mSurfaceHelper.onNewStartParams(params);
assertFalse(mSurfaceHelper.isTouchInputEnabled());
// Send broadcast to enable touch input.
sendBroadcastSync(CastWebContentsIntentUtils.enableTouchInput("1", true));
assertTrue(mSurfaceHelper.isTouchInputEnabled());
}
@Test
public void testEnableTouchInputIntentWithWrongIdIsIgnored() {
WebContents webContents = mock(WebContents.class);
StartParams params = new StartParamsBuilder().withId("1").enableTouchInput(false).build();
mSurfaceHelper.onNewStartParams(params);
assertFalse(mSurfaceHelper.isTouchInputEnabled());
// Send broadcast to enable touch input with different ID.
sendBroadcastSync(CastWebContentsIntentUtils.enableTouchInput("2", true));
assertFalse(mSurfaceHelper.isTouchInputEnabled());
}
@Test
public void testDisableTouchInputIntent() {
WebContents webContents = mock(WebContents.class);
StartParams params = new StartParamsBuilder().withId("1").enableTouchInput(true).build();
mSurfaceHelper.onNewStartParams(params);
assertTrue(mSurfaceHelper.isTouchInputEnabled());
// Send broadcast to enable touch input.
sendBroadcastSync(CastWebContentsIntentUtils.enableTouchInput("1", false));
assertFalse(mSurfaceHelper.isTouchInputEnabled());
}
@Test
public void testSetsVolumeControlStreamOfHostActivity() {
StartParams params = new StartParamsBuilder().build();
mSurfaceHelper.onNewStartParams(params);
verify(mActivity).setVolumeControlStream(AudioManager.STREAM_MUSIC);
}
@Test
public void testSendsComponentClosedBroadcastWhenWebContentsViewIsClosedAlt() {
StartParams params1 = new StartParamsBuilder().withId("1").build();
StartParams params2 = new StartParamsBuilder().withId("2").build();
mSurfaceHelper.onNewStartParams(params1);
// Listen for notification from surface helper that web contents closed.
BroadcastAsserter intentWasSent =
new BroadcastAsserter(CastWebContentsIntentUtils.onActivityStopped("1"));
mSurfaceHelper.onNewStartParams(params2);
intentWasSent.verify();
}
@Test
public void testFinishLaterCallbackIsRunAfterStopWebContents() {
StartParams params = new StartParamsBuilder().withId("0").build();
mSurfaceHelper.onNewStartParams(params);
sendBroadcastSync(CastWebContentsIntentUtils.requestStopWebContents("0"));
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
verify(mFinishCallback).accept(CastWebContentsIntentUtils.getInstanceUri("0"));
}
@Test
public void testFinishLaterCallbackIsRunAfterScreenOff() {
StartParams params = new StartParamsBuilder().withId("0").build();
mSurfaceHelper.onNewStartParams(params);
sendBroadcastSync(new Intent(CastIntents.ACTION_SCREEN_OFF));
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
verify(mFinishCallback).accept(CastWebContentsIntentUtils.getInstanceUri("0"));
}
@Test
public void testFinishLaterCallbackIsNotRunIfNewWebContentsIsReceived() {
StartParams params1 = new StartParamsBuilder().withId("1").build();
StartParams params2 = new StartParamsBuilder().withId("2").build();
mSurfaceHelper.onNewStartParams(params1);
sendBroadcastSync(CastWebContentsIntentUtils.requestStopWebContents("1"));
mSurfaceHelper.onNewStartParams(params2);
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
verify(mFinishCallback, never()).accept(any());
}
@Test
public void testOnDestroyClosesWebContentsView() {
WebContents webContents = mock(WebContents.class);
Scope scope = mock(Scope.class);
StartParams params = new StartParamsBuilder().withWebContents(webContents).build();
when(mWebContentsView.create(webContents)).thenReturn(scope);
mSurfaceHelper.onNewStartParams(params);
mSurfaceHelper.onDestroy();
verify(scope).close();
}
@Test
public void testOnDestroyNotifiesComponent() {
StartParams params = new StartParamsBuilder().withId("2").build();
mSurfaceHelper.onNewStartParams(params);
BroadcastAsserter intentWasSent =
new BroadcastAsserter(CastWebContentsIntentUtils.onActivityStopped("2"));
mSurfaceHelper.onDestroy();
intentWasSent.verify();
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment