Commit 7de6b4bd authored by Evan Stade's avatar Evan Stade Committed by Commit Bot

WebLayer: add support for MediaRoute{Controller,Chooser}Dialog

These dialogs are shown when starting to cast or controlling the cast
session. Since they are DialogFragments, a client-side RemoteFragment
is needed to represent them. The implementation is modeled after
SiteSettingsFragmentImpl, although one key difference is that there
is no new Activity. PassthroughFragmentActivity is not refactored for
reuse because it should go away soon, as per crbug.com/1123216

After this change, casting still doesn't work, but the initial
route chooser dialog at least appears.

Bug: 1057100
Change-Id: I9930c6124ff5162a296dcc74637a8489dbb0d96d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2432319
Commit-Queue: Evan Stade <estade@chromium.org>
Reviewed-by: default avatarScott Violet <sky@chromium.org>
Reviewed-by: default avatarRobbie McElrath <rmcelrath@chromium.org>
Cr-Commit-Position: refs/heads/master@{#812524}
parent 71c10e67
......@@ -35,7 +35,6 @@ public abstract class MediaRouterClient {
* @param webContents a {@link WebContents} in a tab.
* @return a unique integer identifier for the associated tab.
*/
public abstract int getTabId(WebContents webContents);
/**
......
......@@ -131,6 +131,7 @@
#include "weblayer/browser/safe_browsing/real_time_url_lookup_service_factory.h"
#include "weblayer/browser/safe_browsing/safe_browsing_service.h"
#include "weblayer/browser/tts_environment_android_impl.h"
#include "weblayer/browser/weblayer_factory_impl_android.h"
#endif
#if defined(OS_LINUX) || defined(OS_CHROMEOS) || defined(OS_ANDROID)
......@@ -637,6 +638,9 @@ content::ControllerPresentationServiceDelegate*
ContentBrowserClientImpl::GetControllerPresentationServiceDelegate(
content::WebContents* web_contents) {
#if defined(OS_ANDROID)
if (WebLayerFactoryImplAndroid::GetClientMajorVersion() < 87)
return nullptr;
if (base::FeatureList::IsEnabled(features::kMediaRouter)) {
MediaRouterFactory::DoPlatformInitIfNeeded();
return media_router::PresentationServiceDelegateImpl::
......
......@@ -138,6 +138,7 @@ android_library("java") {
"org/chromium/weblayer_private/WebLayerTabModalPresenter.java",
"org/chromium/weblayer_private/WebMessageReplyProxyImpl.java",
"org/chromium/weblayer_private/WebShareServiceFactory.java",
"org/chromium/weblayer_private/media/MediaRouteDialogFragmentImpl.java",
"org/chromium/weblayer_private/media/MediaRouterClientImpl.java",
"org/chromium/weblayer_private/media/MediaSessionManager.java",
"org/chromium/weblayer_private/media/MediaStreamManager.java",
......@@ -412,6 +413,7 @@ android_aidl("aidl") {
"org/chromium/weblayer_private/interfaces/IFullscreenCallbackClient.aidl",
"org/chromium/weblayer_private/interfaces/IGoogleAccountsCallbackClient.aidl",
"org/chromium/weblayer_private/interfaces/IMediaCaptureCallbackClient.aidl",
"org/chromium/weblayer_private/interfaces/IMediaRouteDialogFragment.aidl",
"org/chromium/weblayer_private/interfaces/INavigation.aidl",
"org/chromium/weblayer_private/interfaces/INavigationController.aidl",
"org/chromium/weblayer_private/interfaces/INavigationControllerClient.aidl",
......
......@@ -32,6 +32,7 @@ import org.chromium.weblayer_private.interfaces.ITab;
import org.chromium.weblayer_private.interfaces.IUrlBarController;
import org.chromium.weblayer_private.interfaces.ObjectWrapper;
import org.chromium.weblayer_private.interfaces.StrictModeWorkaround;
import org.chromium.weblayer_private.media.MediaRouteDialogFragmentImpl;
import java.util.Arrays;
import java.util.List;
......@@ -548,6 +549,15 @@ public class BrowserImpl extends IBrowser.Stub implements View.OnAttachStateChan
updateAllTabsViewAttachedState();
}
public MediaRouteDialogFragmentImpl createMediaRouteDialogFragment() {
try {
return MediaRouteDialogFragmentImpl.fromRemoteFragment(
mClient.createMediaRouteDialogFragment());
} catch (RemoteException e) {
throw new APICallException(e);
}
}
private void updateAllTabsViewAttachedState() {
for (Object tab : getTabs()) {
((TabImpl) tab).updateViewAttachedStateFromBrowser();
......
......@@ -60,6 +60,17 @@ public abstract class RemoteFragmentImpl extends IRemoteFragment.Stub {
}
}
public void removeFragmentFromFragmentManager() {
if (WebLayerFactoryImpl.getClientMajorVersion() < 87) {
return;
}
try {
mClient.removeFragmentFromFragmentManager();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
// TODO(pshmakov): add dependency to androidx.annotation and put @CallSuper here.
public void onCreate(Bundle savedInstanceState) {
try {
......
......@@ -68,6 +68,7 @@ import org.chromium.ui.base.WindowAndroid;
import org.chromium.weblayer_private.interfaces.APICallException;
import org.chromium.weblayer_private.interfaces.IBrowserFragment;
import org.chromium.weblayer_private.interfaces.ICrashReporterController;
import org.chromium.weblayer_private.interfaces.IMediaRouteDialogFragment;
import org.chromium.weblayer_private.interfaces.IObjectWrapper;
import org.chromium.weblayer_private.interfaces.IProfile;
import org.chromium.weblayer_private.interfaces.IRemoteFragmentClient;
......@@ -76,6 +77,7 @@ import org.chromium.weblayer_private.interfaces.IWebLayer;
import org.chromium.weblayer_private.interfaces.IWebLayerClient;
import org.chromium.weblayer_private.interfaces.ObjectWrapper;
import org.chromium.weblayer_private.interfaces.StrictModeWorkaround;
import org.chromium.weblayer_private.media.MediaRouteDialogFragmentImpl;
import org.chromium.weblayer_private.media.MediaSessionManager;
import org.chromium.weblayer_private.media.MediaStreamManager;
import org.chromium.weblayer_private.metrics.MetricsServiceClient;
......@@ -336,6 +338,15 @@ public final class WebLayerImpl extends IWebLayer.Stub {
return fragment.asISiteSettingsFragment();
}
@Override
public IMediaRouteDialogFragment createMediaRouteDialogFragmentImpl(
IRemoteFragmentClient remoteFragmentClient) {
StrictModeWorkaround.apply();
MediaRouteDialogFragmentImpl fragment =
new MediaRouteDialogFragmentImpl(remoteFragmentClient);
return fragment.asIMediaRouteDialogFragment();
}
@Override
public IProfile getProfile(String profileName) {
StrictModeWorkaround.apply();
......
......@@ -4,10 +4,14 @@
package org.chromium.weblayer_private.interfaces;
import org.chromium.weblayer_private.interfaces.IRemoteFragment;
import org.chromium.weblayer_private.interfaces.ITab;
interface IBrowserClient {
void onActiveTabChanged(in int activeTabId) = 0;
void onTabAdded(in ITab tab) = 1;
void onTabRemoved(in int tabId) = 2;
// Added in 87.
IRemoteFragment createMediaRouteDialogFragment() = 3;
}
// Copyright 2020 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.weblayer_private.interfaces;
import org.chromium.weblayer_private.interfaces.IRemoteFragment;
interface IMediaRouteDialogFragment {
IRemoteFragment asRemoteFragment() = 0;
}
......@@ -34,4 +34,6 @@ interface IRemoteFragmentClient {
void requestPermissions(in String[] permissions, int requestCode) = 15;
// Since 84
IObjectWrapper /* View */ getView() = 16;
// Since 87
void removeFragmentFromFragmentManager() = 17;
}
......@@ -12,6 +12,7 @@ import org.chromium.weblayer_private.interfaces.ICrashReporterController;
import org.chromium.weblayer_private.interfaces.IObjectWrapper;
import org.chromium.weblayer_private.interfaces.IProfile;
import org.chromium.weblayer_private.interfaces.IRemoteFragmentClient;
import org.chromium.weblayer_private.interfaces.IMediaRouteDialogFragment;
import org.chromium.weblayer_private.interfaces.ISiteSettingsFragment;
import org.chromium.weblayer_private.interfaces.IWebLayerClient;
......@@ -101,4 +102,6 @@ interface IWebLayer {
// Added in Version 87.
IObjectWrapper getApplicationContext() = 20;
IMediaRouteDialogFragment createMediaRouteDialogFragmentImpl(
in IRemoteFragmentClient remoteFragmentClient) = 21;
}
// Copyright 2020 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.weblayer_private.media;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewStub;
import android.view.Window;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentController;
import androidx.fragment.app.FragmentHostCallback;
import androidx.fragment.app.FragmentManager;
import org.chromium.components.embedder_support.application.ClassLoaderContextWrapperFactory;
import org.chromium.weblayer_private.R;
import org.chromium.weblayer_private.RemoteFragmentImpl;
import org.chromium.weblayer_private.interfaces.IMediaRouteDialogFragment;
import org.chromium.weblayer_private.interfaces.IRemoteFragment;
import org.chromium.weblayer_private.interfaces.IRemoteFragmentClient;
import org.chromium.weblayer_private.interfaces.StrictModeWorkaround;
import java.lang.reflect.Constructor;
/**
* WebLayer's implementation of the client library's MediaRouteDialogFragment.
*
* This class is the impl-side representation of a client fragment which is added to the browser
* fragment, and is parent to MediaRouter-related {@link DialogFragment} instances. This class will
* automatically clean up the client-side fragment when the child fragment is detached.
*
* This class is modeled after {@link SiteSettingsFragmentImpl}, see it for details.
*/
public class MediaRouteDialogFragmentImpl extends RemoteFragmentImpl {
// The WebLayer-wrapped context object. This context gets assets and resources from WebLayer,
// not from the embedder. Use this for the most part, especially to resolve WebLayer-specific
// resource IDs.
private Context mContext;
private boolean mStarted;
private FragmentController mFragmentController;
/**
* A fake FragmentActivity needed to make the Fragment system happy.
*
* See {@link SiteSettingsFragmentImpl#PassthroughFragmentActivity}.
* TODO(crbug.com/1123216): remove this class.
*/
private static class PassthroughFragmentActivity extends FragmentActivity {
private static final Class<?>[] VIEW_CONSTRUCTOR_ARGS =
new Class[] {Context.class, AttributeSet.class};
private final MediaRouteDialogFragmentImpl mFragmentImpl;
int getThemeResource(int attr) {
TypedValue value = new TypedValue();
return getTheme().resolveAttribute(attr, value, true) ? value.resourceId : 0;
}
private PassthroughFragmentActivity(MediaRouteDialogFragmentImpl fragmentImpl) {
mFragmentImpl = fragmentImpl;
attachBaseContext(mFragmentImpl.getWebLayerContext());
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
getLayoutInflater().setFactory2(this);
}
AppCompatDelegate.create(this, null);
// TODO(estade): this is necessary because MediaRouter dialogs crash if the theme has an
// action bar. It's unclear why this is necessary when it's not in Chrome, and why
// ContextThemeWrapper doesn't work.
getTheme().applyStyle(R.style.Theme_BrowserUI, true);
}
@Override
public Object getSystemService(String name) {
if (Context.LAYOUT_INFLATER_SERVICE.equals(name)) {
return getLayoutInflater();
}
return getEmbedderActivity().getSystemService(name);
}
@Override
public LayoutInflater getLayoutInflater() {
return (LayoutInflater) getBaseContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (name.indexOf('.') == -1) {
return null;
}
Class<? extends View> clazz = null;
try {
clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);
LayoutInflater inflater = getLayoutInflater();
if (inflater.getFilter() != null && !inflater.getFilter().onLoadClass(clazz)) {
throw new InflateException(attrs.getPositionDescription()
+ ": Class not allowed to be inflated " + name);
}
Constructor<? extends View> constructor =
clazz.getConstructor(VIEW_CONSTRUCTOR_ARGS);
constructor.setAccessible(true);
View view = constructor.newInstance(new Object[] {context, attrs});
if (view instanceof ViewStub) {
// Use the same Context when inflating ViewStub later.
ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(inflater.cloneInContext(context));
}
return view;
} catch (Exception e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class "
+ (clazz == null ? "<unknown>" : clazz.getName()));
ie.initCause(e);
throw ie;
}
}
@Override
public Window getWindow() {
return getEmbedderActivity().getWindow();
}
@Override
public Context getApplicationContext() {
return getEmbedderActivity().getApplicationContext();
}
@Override
public void startActivity(Intent intent) {
getEmbedderActivity().startActivity(intent);
}
@Override
public void setTitle(int titleId) {
getEmbedderActivity().setTitle(mFragmentImpl.getWebLayerContext().getString(titleId));
}
@Override
public void setTitle(CharSequence title) {
getEmbedderActivity().setTitle(title);
}
private Activity getEmbedderActivity() {
return mFragmentImpl.getActivity();
}
}
private static class MediaRouteDialogFragmentHostCallback
extends FragmentHostCallback<Context> {
private final MediaRouteDialogFragmentImpl mFragmentImpl;
private MediaRouteDialogFragmentHostCallback(MediaRouteDialogFragmentImpl fragmentImpl) {
super(new PassthroughFragmentActivity(fragmentImpl), new Handler(), 0);
mFragmentImpl = fragmentImpl;
}
@Override
public Context onGetHost() {
return mFragmentImpl.getWebLayerContext();
}
@Override
public LayoutInflater onGetLayoutInflater() {
Context context = mFragmentImpl.getWebLayerContext();
return ((LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
.cloneInContext(context);
}
@Override
public boolean onHasView() {
return mFragmentImpl.getView() != null;
}
@Override
public View onFindViewById(int id) {
return onHasView() ? mFragmentImpl.getView().findViewById(id) : null;
}
}
public MediaRouteDialogFragmentImpl(IRemoteFragmentClient remoteFragmentClient) {
super(remoteFragmentClient);
}
@Override
public void onAttach(Context context) {
StrictModeWorkaround.apply();
super.onAttach(context);
mContext = ClassLoaderContextWrapperFactory.get(context);
mFragmentController =
FragmentController.createController(new MediaRouteDialogFragmentHostCallback(this));
// Remove the host fragment as soon as the media router dialog fragment is detached.
mFragmentController.getSupportFragmentManager().registerFragmentLifecycleCallbacks(
new FragmentManager.FragmentLifecycleCallbacks() {
@Override
public void onFragmentDetached(FragmentManager fm, Fragment f) {
MediaRouteDialogFragmentImpl.this.removeFragmentFromFragmentManager();
}
},
false);
}
@Override
public void onCreate(Bundle savedInstanceState) {
StrictModeWorkaround.apply();
mFragmentController.attachHost(null);
super.onCreate(savedInstanceState);
mFragmentController.dispatchCreate();
}
@Override
public void onDestroyView() {
StrictModeWorkaround.apply();
super.onDestroyView();
mFragmentController.dispatchDestroyView();
}
@Override
public void onDestroy() {
StrictModeWorkaround.apply();
super.onDestroy();
mFragmentController.dispatchDestroy();
}
@Override
public void onDetach() {
StrictModeWorkaround.apply();
super.onDetach();
mContext = null;
}
@Override
public void onStart() {
super.onStart();
if (!mStarted) {
mStarted = true;
mFragmentController.dispatchActivityCreated();
}
mFragmentController.noteStateNotSaved();
mFragmentController.execPendingActions();
mFragmentController.dispatchStart();
}
@Override
public void onStop() {
super.onStop();
mFragmentController.dispatchStop();
}
@Override
public void onResume() {
super.onResume();
mFragmentController.dispatchResume();
}
@Override
public void onPause() {
super.onPause();
mFragmentController.dispatchPause();
}
public IMediaRouteDialogFragment asIMediaRouteDialogFragment() {
return new IMediaRouteDialogFragment.Stub() {
@Override
public IRemoteFragment asRemoteFragment() {
StrictModeWorkaround.apply();
return MediaRouteDialogFragmentImpl.this;
}
};
}
public static MediaRouteDialogFragmentImpl fromRemoteFragment(IRemoteFragment remoteFragment) {
return (MediaRouteDialogFragmentImpl) remoteFragment;
}
public FragmentManager getSupportFragmentManager() {
return mFragmentController.getSupportFragmentManager();
}
private Context getWebLayerContext() {
return mContext;
}
}
......@@ -39,7 +39,10 @@ public class MediaRouterClientImpl extends MediaRouterClient {
@Override
public FragmentManager getSupportFragmentManager(WebContents initiator) {
return null;
return TabImpl.fromWebContents(initiator)
.getBrowser()
.createMediaRouteDialogFragment()
.getSupportFragmentManager();
}
@CalledByNative
......
......@@ -66,6 +66,7 @@ android_library("java") {
"org/chromium/weblayer/LoadError.java",
"org/chromium/weblayer/MediaCaptureCallback.java",
"org/chromium/weblayer/MediaCaptureController.java",
"org/chromium/weblayer/MediaRouteDialogFragment.java",
"org/chromium/weblayer/MediaSessionService.java",
"org/chromium/weblayer/NavigateParams.java",
"org/chromium/weblayer/Navigation.java",
......
......@@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment;
import org.chromium.weblayer_private.interfaces.APICallException;
import org.chromium.weblayer_private.interfaces.IBrowser;
import org.chromium.weblayer_private.interfaces.IBrowserClient;
import org.chromium.weblayer_private.interfaces.IRemoteFragment;
import org.chromium.weblayer_private.interfaces.ITab;
import org.chromium.weblayer_private.interfaces.ObjectWrapper;
import org.chromium.weblayer_private.interfaces.StrictModeWorkaround;
......@@ -30,6 +31,7 @@ import java.util.Set;
public class Browser {
// Set to null once destroyed (or for tests).
private IBrowser mImpl;
private BrowserFragment mFragment;
private final ObserverList<TabListCallback> mTabListCallbacks;
private final UrlBarController mUrlBarController;
......@@ -40,8 +42,9 @@ public class Browser {
mUrlBarController = null;
}
Browser(IBrowser impl) {
Browser(IBrowser impl, BrowserFragment fragment) {
mImpl = impl;
mFragment = fragment;
mTabListCallbacks = new ObserverList<TabListCallback>();
try {
......@@ -72,6 +75,7 @@ public class Browser {
// Called prior to notifying IBrowser of destroy().
void prepareForDestroy() {
mFragment = null;
for (TabListCallback callback : mTabListCallbacks) {
callback.onWillDestroyBrowserAndAllTabs();
}
......@@ -385,5 +389,11 @@ public class Browser {
callback.onTabRemoved(tab);
}
}
@Override
public IRemoteFragment createMediaRouteDialogFragment() {
StrictModeWorkaround.apply();
return MediaRouteDialogFragment.create(mFragment);
}
}
}
......@@ -84,7 +84,7 @@ public final class BrowserFragment extends RemoteFragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
mBrowser = new Browser(mImpl.getBrowser());
mBrowser = new Browser(mImpl.getBrowser(), this);
} catch (RemoteException e) {
throw new APICallException(e);
}
......
// Copyright 2020 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.weblayer;
import android.content.Context;
import org.chromium.weblayer_private.interfaces.IRemoteFragment;
/**
* The client-side implementation of MediaRouteDialogFragment.
*
* This class hosts dialog fragments for casting, such as a {@link MediaRouteChooserDialogFragment}
* or a {@link MediaRouteControllerDialogFragment}.
*
* @since 87
*/
class MediaRouteDialogFragment extends RemoteFragment {
private static final String FRAGMENT_TAG = "WebLayerMediaRouteDialogFragment";
static IRemoteFragment create(BrowserFragment browserFragment) {
MediaRouteDialogFragment fragment = new MediaRouteDialogFragment();
browserFragment.getParentFragmentManager()
.beginTransaction()
.add(0, fragment, FRAGMENT_TAG)
.commitNow();
return fragment.getRemoteFragment();
}
@Override
protected IRemoteFragment createRemoteFragment(Context appContext) {
try {
return WebLayer.loadSync(appContext)
.connectMediaRouteDialogFragment(getRemoteFragmentClient())
.asRemoteFragment();
} catch (Exception e) {
throw new RuntimeException("Failed to initialize WebLayer", e);
}
}
}
......@@ -150,6 +150,14 @@ abstract class RemoteFragment extends Fragment {
StrictModeWorkaround.apply();
RemoteFragment.this.requestPermissions(permissions, requestCode);
}
@Override
public void removeFragmentFromFragmentManager() {
StrictModeWorkaround.apply();
Fragment fragment = RemoteFragment.this;
if (fragment.getParentFragmentManager() == null) return;
fragment.getParentFragmentManager().beginTransaction().remove(fragment).commit();
}
};
// Nonnull after first onAttach().
......@@ -336,4 +344,8 @@ abstract class RemoteFragment extends Fragment {
throw new APICallException(e);
}
}
protected IRemoteFragment getRemoteFragment() {
return mRemoteFragment;
}
}
......@@ -26,6 +26,7 @@ import androidx.fragment.app.Fragment;
import org.chromium.weblayer_private.interfaces.APICallException;
import org.chromium.weblayer_private.interfaces.BrowserFragmentArgs;
import org.chromium.weblayer_private.interfaces.IBrowserFragment;
import org.chromium.weblayer_private.interfaces.IMediaRouteDialogFragment;
import org.chromium.weblayer_private.interfaces.IProfile;
import org.chromium.weblayer_private.interfaces.IRemoteFragmentClient;
import org.chromium.weblayer_private.interfaces.ISiteSettingsFragment;
......@@ -559,6 +560,21 @@ public class WebLayer {
}
}
/**
* Returns the remote counterpart of MediaRouteDialogFragment.
*/
/* package */ IMediaRouteDialogFragment connectMediaRouteDialogFragment(
IRemoteFragmentClient remoteFragmentClient) {
if (getSupportedMajorVersionInternal() < 87) {
throw new UnsupportedOperationException();
}
try {
return mImpl.createMediaRouteDialogFragmentImpl(remoteFragmentClient);
} catch (RemoteException e) {
throw new APICallException(e);
}
}
/* package */ static IWebLayer getIWebLayer(Context context) {
return getWebLayerLoader(context).getIWebLayer();
}
......
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