Commit 0c63ddaf authored by Clark DuVall's avatar Clark DuVall Committed by Commit Bot

[WebLayer] Implement file inputs

This implements the various types of file inputs, which required a
few parts:

1) Intents sent from weblayer need to go through the fragment, not the
   activity, so we can handle the responses.
2) Copied file_select_helper.* from
   chrome/browser/file_select_helper.*, but greatly simplified them
   since we likely will not need much of that functionality.
3) Add a FileProvider, which is used when uploading image files from the
   camera.

Change-Id: Iaf01199de8fdd11fc89b2178f69e700794861eed
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1873128Reviewed-by: default avatarScott Violet <sky@chromium.org>
Commit-Queue: Clark DuVall <cduvall@chromium.org>
Cr-Commit-Position: refs/heads/master@{#708350}
parent c45749e5
......@@ -69,17 +69,6 @@ namespace {
constexpr char kContactsMimeType[] = "text/json+contacts";
#endif
// Converts a list of FilePaths to a list of ui::SelectedFileInfo.
std::vector<ui::SelectedFileInfo> FilePathListToSelectedFileInfoList(
const std::vector<base::FilePath>& paths) {
std::vector<ui::SelectedFileInfo> selected_files;
for (size_t i = 0; i < paths.size(); ++i) {
selected_files.push_back(
ui::SelectedFileInfo(paths[i], paths[i]));
}
return selected_files;
}
void DeleteFiles(std::vector<base::FilePath> paths) {
for (auto& file_path : paths)
base::DeleteFile(file_path, false);
......@@ -207,7 +196,7 @@ void FileSelectHelper::MultiFilesSelected(
const std::vector<base::FilePath>& files,
void* params) {
std::vector<ui::SelectedFileInfo> selected_files =
FilePathListToSelectedFileInfoList(files);
ui::FilePathListToSelectedFileInfoList(files);
MultiFilesSelectedWithExtraInfo(selected_files, params);
}
......@@ -281,7 +270,7 @@ void FileSelectHelper::OnListDone(int error) {
}
std::vector<ui::SelectedFileInfo> selected_files =
FilePathListToSelectedFileInfoList(entry->results_);
ui::FilePathListToSelectedFileInfoList(entry->results_);
if (dialog_type_ == ui::SelectFileDialog::SELECT_UPLOAD_FOLDER) {
LaunchConfirmationDialog(entry->path_, std::move(selected_files));
......
......@@ -9,6 +9,7 @@ import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import org.chromium.base.ActivityState;
......@@ -76,18 +77,25 @@ public class ActivityWindowAndroid
return (ActivityKeyboardVisibilityDelegate) super.getKeyboardDelegate();
}
/** Uses the provided intent sender to start the intent. */
protected boolean startIntentSenderForResult(IntentSender intentSender, int requestCode) {
Activity activity = getActivity().get();
if (activity == null) return false;
try {
activity.startIntentSenderForResult(intentSender, requestCode, new Intent(), 0, 0, 0);
} catch (SendIntentException e) {
return false;
}
return true;
}
@Override
public int showCancelableIntent(
PendingIntent intent, IntentCallback callback, Integer errorId) {
Activity activity = getActivity().get();
if (activity == null) return START_INTENT_FAILURE;
int requestCode = generateNextRequestCode();
try {
activity.startIntentSenderForResult(
intent.getIntentSender(), requestCode, new Intent(), 0, 0, 0);
} catch (SendIntentException e) {
if (!startIntentSenderForResult(intent.getIntentSender(), requestCode)) {
return START_INTENT_FAILURE;
}
......@@ -95,16 +103,24 @@ public class ActivityWindowAndroid
return requestCode;
}
@Override
public int showCancelableIntent(Intent intent, IntentCallback callback, Integer errorId) {
/** Starts an activity for the provided intent. */
protected boolean startActivityForResult(Intent intent, int requestCode) {
Activity activity = getActivity().get();
if (activity == null) return START_INTENT_FAILURE;
int requestCode = generateNextRequestCode();
if (activity == null) return false;
try {
activity.startActivityForResult(intent, requestCode);
} catch (ActivityNotFoundException e) {
return false;
}
return true;
}
@Override
public int showCancelableIntent(Intent intent, IntentCallback callback, Integer errorId) {
int requestCode = generateNextRequestCode();
if (!startActivityForResult(intent, requestCode)) {
return START_INTENT_FAILURE;
}
......
......@@ -25,4 +25,13 @@ SelectedFileInfo& SelectedFileInfo::operator=(const SelectedFileInfo& other) =
SelectedFileInfo& SelectedFileInfo::operator=(SelectedFileInfo&& other) =
default;
// Converts a list of FilePaths to a list of ui::SelectedFileInfo.
std::vector<SelectedFileInfo> FilePathListToSelectedFileInfoList(
const std::vector<base::FilePath>& paths) {
std::vector<SelectedFileInfo> selected_files;
for (const auto& path : paths)
selected_files.push_back(SelectedFileInfo(path, path));
return selected_files;
}
} // namespace ui
......@@ -49,6 +49,10 @@ struct SHELL_DIALOGS_EXPORT SelectedFileInfo {
SelectedFileInfo& operator=(SelectedFileInfo&& other);
};
// Converts a list of FilePaths to a list of ui::SelectedFileInfo.
SHELL_DIALOGS_EXPORT std::vector<SelectedFileInfo>
FilePathListToSelectedFileInfoList(const std::vector<base::FilePath>& paths);
} // namespace ui
#endif // UI_SHELL_DIALOGS_SELECTED_FILE_INFO_H_
......@@ -44,6 +44,8 @@ jumbo_static_library("weblayer_lib") {
"browser/browser_main_parts_impl.h",
"browser/content_browser_client_impl.cc",
"browser/content_browser_client_impl.h",
"browser/file_select_helper.cc",
"browser/file_select_helper.h",
"browser/isolated_world_ids.h",
"browser/navigation_controller_impl.cc",
"browser/navigation_controller_impl.h",
......@@ -115,6 +117,7 @@ jumbo_static_library("weblayer_lib") {
"//ui/gfx/ipc/skia",
"//ui/gl",
"//ui/platform_window",
"//ui/shell_dialogs",
"//ui/webui",
"//url",
"//v8",
......
......@@ -14,6 +14,7 @@ include_rules = [
"+ui/base",
"+ui/events",
"+ui/gfx",
"+ui/shell_dialogs",
"+ui/views",
"+ui/webui",
"+ui/wm",
......
......@@ -6,10 +6,12 @@
#include "base/auto_reset.h"
#include "base/logging.h"
#include "content/public/browser/file_select_listener.h"
#include "content/public/browser/interstitial_page.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/browser_controls_state.h"
#include "weblayer/browser/file_select_helper.h"
#include "weblayer/browser/navigation_controller_impl.h"
#include "weblayer/browser/profile_impl.h"
#include "weblayer/public/browser_observer.h"
......@@ -192,6 +194,14 @@ void BrowserControllerImpl::DidNavigateMainFramePostCommit(
observer.DisplayedUrlChanged(web_contents->GetVisibleURL());
}
void BrowserControllerImpl::RunFileChooser(
content::RenderFrameHost* render_frame_host,
std::unique_ptr<content::FileSelectListener> listener,
const blink::mojom::FileChooserParams& params) {
FileSelectHelper::RunFileChooser(render_frame_host, std::move(listener),
params);
}
int BrowserControllerImpl::GetTopControlsHeight() {
#if defined(OS_ANDROID)
return top_controls_container_view_->GetTopControlsHeight();
......
......@@ -80,6 +80,9 @@ class BrowserControllerImpl : public BrowserController,
double progress) override;
void DidNavigateMainFramePostCommit(
content::WebContents* web_contents) override;
void RunFileChooser(content::RenderFrameHost* render_frame_host,
std::unique_ptr<content::FileSelectListener> listener,
const blink::mojom::FileChooserParams& params) override;
int GetTopControlsHeight() override;
bool DoBrowserControlsShrinkRendererSize(
const content::WebContents* web_contents) override;
......
// Copyright 2019 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.
#include "weblayer/browser/file_select_helper.h"
#include <string>
#include "build/build_config.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "ui/shell_dialogs/select_file_policy.h"
#include "ui/shell_dialogs/selected_file_info.h"
#if defined(OS_ANDROID)
#include "ui/android/view_android.h"
#else
#include "ui/aura/window.h"
#endif
namespace weblayer {
using blink::mojom::FileChooserFileInfo;
using blink::mojom::FileChooserFileInfoPtr;
using blink::mojom::FileChooserParams;
using blink::mojom::FileChooserParamsPtr;
// static
void FileSelectHelper::RunFileChooser(
content::RenderFrameHost* render_frame_host,
std::unique_ptr<content::FileSelectListener> listener,
const FileChooserParams& params) {
// TODO: Should we handle text/json+contacts accept type?
// FileSelectHelper will keep itself alive until it sends the result
// message.
scoped_refptr<FileSelectHelper> file_select_helper(new FileSelectHelper());
file_select_helper->RunFileChooser(render_frame_host, std::move(listener),
params.Clone());
}
FileSelectHelper::FileSelectHelper() = default;
FileSelectHelper::~FileSelectHelper() {
// There may be pending file dialogs, we need to tell them that we've gone
// away so they don't try and call back to us.
if (select_file_dialog_)
select_file_dialog_->ListenerDestroyed();
}
void FileSelectHelper::RunFileChooser(
content::RenderFrameHost* render_frame_host,
std::unique_ptr<content::FileSelectListener> listener,
FileChooserParamsPtr params) {
DCHECK(!web_contents());
DCHECK(listener);
DCHECK(!listener_);
listener_ = std::move(listener);
Observe(content::WebContents::FromRenderFrameHost(render_frame_host));
select_file_dialog_ = ui::SelectFileDialog::Create(this, nullptr);
dialog_mode_ = params->mode;
switch (params->mode) {
case FileChooserParams::Mode::kOpen:
dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_FILE;
break;
case FileChooserParams::Mode::kOpenMultiple:
dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE;
break;
case FileChooserParams::Mode::kUploadFolder:
// For now we don't support inputs with webkitdirectory in weblayer.
dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE;
break;
case FileChooserParams::Mode::kSave:
dialog_type_ = ui::SelectFileDialog::SELECT_SAVEAS_FILE;
break;
default:
// Prevent warning.
dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_FILE;
NOTREACHED();
}
gfx::NativeWindow owning_window;
#if defined(OS_ANDROID)
owning_window = web_contents()->GetNativeView()->GetWindowAndroid();
#else
owning_window = web_contents()->GetNativeView()->GetToplevelWindow();
#endif
#if defined(OS_ANDROID)
// Android needs the original MIME types and an additional capture value.
std::pair<std::vector<base::string16>, bool> accept_types =
std::make_pair(params->accept_types, params->use_media_capture);
#endif
// Many of these params are not used in the Android SelectFileDialog
// implementation, so we can safely pass empty values.
select_file_dialog_->SelectFile(dialog_type_, base::string16(),
base::FilePath(), nullptr, 0,
base::FilePath::StringType(), owning_window,
#if defined(OS_ANDROID)
&accept_types);
#else
nullptr);
#endif
// Because this class returns notifications to the RenderViewHost, it is
// difficult for callers to know how long to keep a reference to this
// instance. We AddRef() here to keep the instance alive after we return
// to the caller, until the last callback is received from the file dialog.
// At that point, we must call RunFileChooserEnd().
AddRef();
}
void FileSelectHelper::RunFileChooserEnd() {
if (listener_)
listener_->FileSelectionCanceled();
Release();
}
void FileSelectHelper::FileSelected(const base::FilePath& path,
int index,
void* params) {
FileSelectedWithExtraInfo(ui::SelectedFileInfo(path, path), index, params);
}
void FileSelectHelper::FileSelectedWithExtraInfo(
const ui::SelectedFileInfo& file,
int index,
void* params) {
ConvertToFileChooserFileInfoList({file});
}
void FileSelectHelper::MultiFilesSelected(
const std::vector<base::FilePath>& files,
void* params) {
std::vector<ui::SelectedFileInfo> selected_files =
ui::FilePathListToSelectedFileInfoList(files);
MultiFilesSelectedWithExtraInfo(selected_files, params);
}
void FileSelectHelper::MultiFilesSelectedWithExtraInfo(
const std::vector<ui::SelectedFileInfo>& files,
void* params) {
ConvertToFileChooserFileInfoList(files);
}
void FileSelectHelper::FileSelectionCanceled(void* params) {
RunFileChooserEnd();
}
void FileSelectHelper::ConvertToFileChooserFileInfoList(
const std::vector<ui::SelectedFileInfo>& files) {
if (AbortIfWebContentsDestroyed())
return;
std::vector<FileChooserFileInfoPtr> chooser_files;
for (const auto& file : files) {
chooser_files.push_back(
FileChooserFileInfo::NewNativeFile(blink::mojom::NativeFileInfo::New(
file.local_path,
base::FilePath(file.display_name).AsUTF16Unsafe())));
}
listener_->FileSelected(std::move(chooser_files), base::FilePath(),
dialog_mode_);
listener_ = nullptr;
// No members should be accessed from here on.
RunFileChooserEnd();
}
bool FileSelectHelper::AbortIfWebContentsDestroyed() {
if (web_contents() == nullptr) {
RunFileChooserEnd();
return true;
}
return false;
}
} // namespace weblayer
// Copyright 2019 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.
#ifndef WEBLAYER_BROWSER_FILE_SELECT_HELPER_H_
#define WEBLAYER_BROWSER_FILE_SELECT_HELPER_H_
#include <memory>
#include <vector>
#include "base/macros.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/file_select_listener.h"
#include "content/public/browser/web_contents_observer.h"
#include "ui/shell_dialogs/select_file_dialog.h"
namespace content {
class FileSelectListener;
} // namespace content
namespace ui {
struct SelectedFileInfo;
}
namespace weblayer {
// This class handles file-selection requests coming from renderer processes.
// It implements both the initialisation and listener functions for
// file-selection dialogs.
//
// Since FileSelectHelper listens to observations of a widget, it needs to live
// on and be destroyed on the UI thread. References to FileSelectHelper may be
// passed on to other threads.
class FileSelectHelper : public base::RefCountedThreadSafe<
FileSelectHelper,
content::BrowserThread::DeleteOnUIThread>,
public ui::SelectFileDialog::Listener,
public content::WebContentsObserver {
public:
// Show the file chooser dialog.
static void RunFileChooser(
content::RenderFrameHost* render_frame_host,
std::unique_ptr<content::FileSelectListener> listener,
const blink::mojom::FileChooserParams& params);
private:
friend class base::RefCountedThreadSafe<FileSelectHelper>;
friend class base::DeleteHelper<FileSelectHelper>;
friend struct content::BrowserThread::DeleteOnThread<
content::BrowserThread::UI>;
FileSelectHelper();
~FileSelectHelper() override;
void RunFileChooser(content::RenderFrameHost* render_frame_host,
std::unique_ptr<content::FileSelectListener> listener,
blink::mojom::FileChooserParamsPtr params);
// Cleans up and releases this instance. This must be called after the last
// callback is received from the file chooser dialog.
void RunFileChooserEnd();
// SelectFileDialog::Listener overrides.
void FileSelected(const base::FilePath& path,
int index,
void* params) override;
void FileSelectedWithExtraInfo(const ui::SelectedFileInfo& file,
int index,
void* params) override;
void MultiFilesSelected(const std::vector<base::FilePath>& files,
void* params) override;
void MultiFilesSelectedWithExtraInfo(
const std::vector<ui::SelectedFileInfo>& files,
void* params) override;
void FileSelectionCanceled(void* params) override;
// This method is called after the user has chosen the file(s) in the UI in
// order to process and filter the list before returning the final result to
// the caller.
void ConvertToFileChooserFileInfoList(
const std::vector<ui::SelectedFileInfo>& files);
// Calls RunFileChooserEnd() if the webcontents was destroyed. Returns true
// if the file chooser operation shouldn't proceed.
bool AbortIfWebContentsDestroyed();
// |listener_| receives the result of the FileSelectHelper.
std::unique_ptr<content::FileSelectListener> listener_;
// Dialog box used for choosing files to upload from file form fields.
scoped_refptr<ui::SelectFileDialog> select_file_dialog_;
// The type of file dialog last shown.
ui::SelectFileDialog::Type dialog_type_ =
ui::SelectFileDialog::SELECT_OPEN_FILE;
// The mode of file dialog last shown.
blink::mojom::FileChooserParams::Mode dialog_mode_ =
blink::mojom::FileChooserParams::Mode::kOpen;
DISALLOW_COPY_AND_ASSIGN(FileSelectHelper);
};
} // namespace weblayer
#endif // WEBLAYER_BROWSER_FILE_SELECT_HELPER_H_
......@@ -42,6 +42,7 @@ android_library("java") {
"//base:jni_java",
"//components/embedder_support/android:application_java",
"//content/public/android:content_java",
"//third_party/android_deps:com_android_support_support_compat_java",
"//ui/android:ui_java",
]
srcjar_deps = [ ":weblayer_locale_config" ]
......
......@@ -18,8 +18,8 @@ import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.content_public.browser.ViewEventSink;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.weblayer_private.aidl.IBrowserController;
import org.chromium.weblayer_private.aidl.IBrowserControllerClient;
import org.chromium.weblayer_private.aidl.IDownloadDelegateClient;
......@@ -32,9 +32,8 @@ import org.chromium.weblayer_private.aidl.ObjectWrapper;
public final class BrowserControllerImpl extends IBrowserController.Stub {
private long mNativeBrowserController;
// TODO: move mWindowAndroid, mContentViewRenderView, mContentView, mTopControlsContainerView to
// TODO: move mContentViewRenderView, mContentView, mTopControlsContainerView to
// BrowserFragmentControllerImpl.
private ActivityWindowAndroid mWindowAndroid;
// This view is the main view (returned from the fragment's onCreateView()).
private ContentViewRenderView mContentViewRenderView;
// One of these is needed per WebContents.
......@@ -69,17 +68,14 @@ public final class BrowserControllerImpl extends IBrowserController.Stub {
public void onScrollChanged(int lPix, int tPix, int oldlPix, int oldtPix) {}
}
public BrowserControllerImpl(Context context, ProfileImpl profile) {
public BrowserControllerImpl(
Context context, ProfileImpl profile, WindowAndroid windowAndroid) {
mProfile = profile;
// Use false to disable listening to activity state.
// TODO: this should *not* use ActivityWindowAndroid as that relies on Activity, and this
// code should not assume it is supplied an Activity.
mWindowAndroid = new ActivityWindowAndroid(context, false);
mContentViewRenderView = new ContentViewRenderView(context);
mContentViewRenderView.onNativeLibraryLoaded(
mWindowAndroid, ContentViewRenderView.MODE_SURFACE_VIEW);
windowAndroid, ContentViewRenderView.MODE_SURFACE_VIEW);
mNativeBrowserController =
BrowserControllerImplJni.get().createBrowserController(profile.getNativeProfile());
......@@ -97,7 +93,7 @@ public final class BrowserControllerImpl extends IBrowserController.Stub {
}
};
mWebContents.initialize("", viewAndroidDelegate, new InternalAccessDelegateImpl(),
mWindowAndroid, WebContents.createDefaultInternalsHolder());
windowAndroid, WebContents.createDefaultInternalsHolder());
mContentViewRenderView.setCurrentWebContents(mWebContents);
......
......@@ -5,9 +5,11 @@
package org.chromium.weblayer_private;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.weblayer_private.aidl.IBrowserFragmentController;
import org.chromium.weblayer_private.aidl.IObjectWrapper;
import org.chromium.weblayer_private.aidl.IProfile;
......@@ -18,19 +20,28 @@ import org.chromium.weblayer_private.aidl.IProfile;
public class BrowserFragmentControllerImpl extends IBrowserFragmentController.Stub {
private final ProfileImpl mProfile;
private BrowserControllerImpl mTabController;
private ActivityWindowAndroid mWindowAndroid;
public BrowserFragmentControllerImpl(ProfileImpl profile, Bundle savedInstanceState) {
mProfile = profile;
// Restore tabs etc from savedInstanceState here.
}
public void onFragmentAttached(Context context) {
mTabController = new BrowserControllerImpl(context, mProfile);
public void onFragmentAttached(Context context, ActivityWindowAndroid windowAndroid) {
mTabController = new BrowserControllerImpl(context, mProfile, windowAndroid);
mWindowAndroid = windowAndroid;
}
public void onFragmentDetached() {
mTabController.destroy();
mTabController = null;
mWindowAndroid = null;
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (mWindowAndroid != null) {
mWindowAndroid.onActivityResult(requestCode, resultCode, data);
}
}
@Override
......
......@@ -5,9 +5,12 @@
package org.chromium.weblayer_private;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
import android.view.View;
import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.weblayer_private.aidl.BrowserFragmentArgs;
import org.chromium.weblayer_private.aidl.IBrowserFragment;
import org.chromium.weblayer_private.aidl.IBrowserFragmentController;
......@@ -21,6 +24,29 @@ public class BrowserFragmentImpl extends RemoteFragmentImpl {
private BrowserFragmentControllerImpl mController;
private Context mContext;
// TODO(cduvall): Factor out the logic we need from ActivityWindowAndroid so we do not inherit
// directly from it.
private static class FragmentWindowAndroid extends ActivityWindowAndroid {
private BrowserFragmentImpl mFragment;
FragmentWindowAndroid(Context context, BrowserFragmentImpl fragment) {
// Use false to disable listening to activity state.
super(context, false);
mFragment = fragment;
}
@Override
protected boolean startIntentSenderForResult(IntentSender intentSender, int requestCode) {
return mFragment.startIntentSenderForResult(
intentSender, requestCode, new Intent(), 0, 0, 0, null);
}
@Override
protected boolean startActivityForResult(Intent intent, int requestCode) {
return mFragment.startActivityForResult(intent, requestCode, null);
}
}
public BrowserFragmentImpl(ProfileManager profileManager, IRemoteFragmentClient client,
Bundle fragmentArgs) {
super(client);
......@@ -33,7 +59,7 @@ public class BrowserFragmentImpl extends RemoteFragmentImpl {
super.onAttach(context);
mContext = context;
if (mController != null) { // On first creation, onAttach is called before onCreate
mController.onFragmentAttached(context);
mController.onFragmentAttached(context, new FragmentWindowAndroid(context, this));
}
}
......@@ -42,7 +68,7 @@ public class BrowserFragmentImpl extends RemoteFragmentImpl {
super.onCreate(savedInstanceState);
mController = new BrowserFragmentControllerImpl(mProfile, savedInstanceState);
if (mContext != null) {
mController.onFragmentAttached(mContext);
mController.onFragmentAttached(mContext, new FragmentWindowAndroid(mContext, this));
}
}
......@@ -51,6 +77,11 @@ public class BrowserFragmentImpl extends RemoteFragmentImpl {
return mController.getFragmentView();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
mController.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onDestroy() {
super.onDestroy();
......
......@@ -6,6 +6,8 @@ package org.chromium.weblayer_private;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
import android.os.RemoteException;
import android.view.View;
......@@ -65,6 +67,8 @@ public abstract class RemoteFragmentImpl extends IRemoteFragment.Stub {
}
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {}
public void onStart() {
try {
mClient.superOnStart();
......@@ -129,6 +133,26 @@ public abstract class RemoteFragmentImpl extends IRemoteFragment.Stub {
}
}
public boolean startActivityForResult(Intent intent, int requestCode, Bundle options) {
try {
return mClient.startActivityForResult(
ObjectWrapper.wrap(intent), requestCode, ObjectWrapper.wrap(options));
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public boolean startIntentSenderForResult(IntentSender intent, int requestCode,
Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) {
try {
return mClient.startIntentSenderForResult(ObjectWrapper.wrap(intent), requestCode,
ObjectWrapper.wrap(fillInIntent), flagsMask, flagsValues, extraFlags,
ObjectWrapper.wrap(options));
} catch (RemoteException e) {
throw new APICallException(e);
}
}
// IRemoteFragment implementation below.
@Override
......@@ -190,4 +214,9 @@ public abstract class RemoteFragmentImpl extends IRemoteFragment.Stub {
public final void handleOnSaveInstanceState(IObjectWrapper outState) {
onSaveInstaceState(ObjectWrapper.unwrap(outState, Bundle.class));
}
@Override
public final void handleOnActivityResult(int requestCode, int resultCode, IObjectWrapper data) {
onActivityResult(requestCode, resultCode, ObjectWrapper.unwrap(data, Intent.class));
}
}
......@@ -5,11 +5,14 @@
package org.chromium.weblayer_private;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.content.FileProvider;
import android.webkit.ValueCallback;
import org.chromium.base.CommandLine;
import org.chromium.base.ContentUriUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.PathUtils;
import org.chromium.base.annotations.UsedByReflection;
......@@ -27,6 +30,8 @@ import org.chromium.weblayer_private.aidl.IWebLayer;
import org.chromium.weblayer_private.aidl.ObjectWrapper;
import org.chromium.weblayer_private.aidl.WebLayerVersion;
import java.io.File;
@UsedByReflection("WebLayer")
public final class WebLayerImpl extends IWebLayer.Stub {
// TODO: should there be one tag for all this code?
......@@ -37,6 +42,17 @@ public final class WebLayerImpl extends IWebLayer.Stub {
private final ProfileManager mProfileManager = new ProfileManager();
private static class FileProviderHelper implements ContentUriUtils.FileProviderUtil {
// Keep this variable in sync with the value defined in AndroidManifest.xml.
private static final String API_AUTHORITY = "org.chromium.weblayer.client.FileProvider";
@Override
public Uri getContentUriFromFile(File file) {
Context appContext = ContextUtils.getApplicationContext();
return FileProvider.getUriForFile(appContext, API_AUTHORITY, file);
}
}
@UsedByReflection("WebLayer")
public static IBinder create() {
return new WebLayerImpl();
......@@ -78,6 +94,7 @@ public final class WebLayerImpl extends IWebLayer.Stub {
}
DeviceUtils.addDeviceSpecificUserAgentSwitch();
ContentUriUtils.setFileProviderUtil(new FileProviderHelper());
LibraryLoader.getInstance().ensureInitialized(LibraryProcessType.PROCESS_WEBLAYER);
......
......@@ -20,4 +20,8 @@ interface IRemoteFragment {
void handleOnDetach() = 9;
void handleOnDestroy() = 10;
void handleOnSaveInstanceState(in IObjectWrapper outState) = 11;
// |data| is an Intent with the result returned from the activity.
void handleOnActivityResult(int requestCode,
int resultCode,
in IObjectWrapper data) = 12;
}
......@@ -18,4 +18,14 @@ interface IRemoteFragmentClient {
void superOnSaveInstanceState(in IObjectWrapper outState) = 10;
IObjectWrapper getActivity() = 11;
boolean startActivityForResult(in IObjectWrapper intent,
int requestCode,
in IObjectWrapper options) = 12;
boolean startIntentSenderForResult(in IObjectWrapper intent,
int requestCode,
in IObjectWrapper fillInIntent,
int flagsMask,
int flagsValues,
int extraFlags,
in IObjectWrapper options) = 13;
}
......@@ -44,5 +44,13 @@
android:isolatedProcess="false"
android:exported="false" />
{% endfor %}
<provider android:name="android.support.v4.content.FileProvider"
android:authorities="org.chromium.weblayer.client.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
......@@ -14,7 +14,7 @@ jinja_template("weblayer_client_manifest") {
}
android_resources("client_resources") {
resource_dirs = []
resource_dirs = [ "res" ]
android_manifest = weblayer_client_manifest
android_manifest_dep = ":weblayer_client_manifest"
}
......@@ -43,6 +43,7 @@ template("weblayer_java") {
]
deps = [
":weblayer_client_manifest",
"//third_party/android_deps:com_android_support_support_fragment_java",
"//weblayer/browser/java:client_java",
]
......@@ -51,6 +52,8 @@ template("weblayer_java") {
# Needed for android.webkit.WebViewDelegate.
alternative_android_sdk_dep =
"//third_party/android_sdk:public_framework_system_java"
android_manifest_for_lint = weblayer_client_manifest
}
}
......
......@@ -4,7 +4,11 @@
package org.chromium.weblayer;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.os.RemoteException;
import android.support.v4.app.Fragment;
......@@ -96,6 +100,34 @@ public final class BrowserFragment extends Fragment {
public IObjectWrapper getActivity() {
return ObjectWrapper.wrap(BrowserFragment.this.getActivity());
}
@Override
public boolean startActivityForResult(
IObjectWrapper intent, int requestCode, IObjectWrapper options) {
try {
BrowserFragment.this.startActivityForResult(
ObjectWrapper.unwrap(intent, Intent.class), requestCode,
ObjectWrapper.unwrap(options, Bundle.class));
} catch (ActivityNotFoundException e) {
return false;
}
return true;
}
@Override
public boolean startIntentSenderForResult(IObjectWrapper intent, int requestCode,
IObjectWrapper fillInIntent, int flagsMask, int flagsValues, int extraFlags,
IObjectWrapper options) {
try {
BrowserFragment.this.startIntentSenderForResult(
ObjectWrapper.unwrap(intent, IntentSender.class), requestCode,
ObjectWrapper.unwrap(fillInIntent, Intent.class), flagsMask, flagsValues,
extraFlags, ObjectWrapper.unwrap(options, Bundle.class));
} catch (SendIntentException e) {
return false;
}
return true;
}
};
// Nonnull after first onAttach().
......@@ -125,6 +157,16 @@ public final class BrowserFragment extends Fragment {
return mBrowserFragmentController;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
try {
mImpl.asRemoteFragment().handleOnActivityResult(
requestCode, resultCode, ObjectWrapper.wrap(data));
} catch (RemoteException e) {
throw new APICallException(e);
}
}
@SuppressWarnings("MissingSuperCall")
@Override
public void onAttach(Context context) {
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2019 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. -->
<!-- The attributes in this XML file provide configuration information -->
<!-- for the ContentProvider. -->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="images" path="images/"/>
</paths>
......@@ -262,10 +262,12 @@ instrumentation_test_apk("weblayer_instrumentation_test_apk") {
"javatests/src/org/chromium/weblayer/test/FullscreenDelegateTest.java",
"javatests/src/org/chromium/weblayer/test/NavigationTest.java",
"javatests/src/org/chromium/weblayer/test/SmokeTest.java",
"javatests/src/org/chromium/weblayer/test/EventUtils.java",
"javatests/src/org/chromium/weblayer/test/ExecuteScriptTest.java",
"javatests/src/org/chromium/weblayer/test/RenderingTest.java",
"javatests/src/org/chromium/weblayer/test/WebLayerShellActivityTestRule.java",
"javatests/src/org/chromium/weblayer/test/FragmentRestoreTest.java",
"javatests/src/org/chromium/weblayer/test/FileInputTest.java",
"shell_apk/src/org/chromium/weblayer/shell/WebLayerShellActivity.java",
]
additional_apks = [
......
......@@ -16,6 +16,17 @@
<uses-library android:name="android.test.runner" />
<activity android:name="org.chromium.test.broker.OnDeviceInstrumentationBroker"
android:exported="true"/>
<activity android:name="org.chromium.weblayer.shell.WebLayerShellActivity">
<!-- Add these intent filters so tests can resolve these intents. -->
<intent-filter>
<action android:name="android.provider.MediaStore.RECORD_SOUND" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.media.action.IMAGE_CAPTURE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
<instrumentation android:name="org.chromium.base.test.BaseChromiumAndroidJUnitRunner"
......
// Copyright 2019 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.test;
import android.app.Activity;
import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.MediaStore;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.v4.app.Fragment;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.content_public.browser.test.util.CriteriaHelper;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.weblayer.shell.WebLayerShellActivity;
import java.io.File;
/**
* Tests that file inputs work as expected.
*/
@RunWith(BaseJUnit4ClassRunner.class)
public class FileInputTest {
@Rule
public WebLayerShellActivityTestRule mActivityTestRule = new WebLayerShellActivityTestRule();
private EmbeddedTestServer mTestServer;
private File mTempFile;
private class FileIntentInterceptor implements WebLayerShellActivity.IntentInterceptor {
public Intent mLastIntent;
private Intent mResponseIntent;
private int mResultCode = Activity.RESULT_CANCELED;
private CallbackHelper mCallbackHelper = new CallbackHelper();
@Override
public void interceptIntent(
Fragment fragment, Intent intent, int requestCode, Bundle options) {
new Handler().post(() -> {
fragment.onActivityResult(requestCode, mResultCode, mResponseIntent);
mLastIntent = intent;
mCallbackHelper.notifyCalled();
});
}
public void waitForIntent() {
try {
mCallbackHelper.waitForCallback(0);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void setResponse(int resultCode, Intent response) {
mResponseIntent = response;
mResultCode = resultCode;
}
}
private FileIntentInterceptor mIntentInterceptor = new FileIntentInterceptor();
@Before
public void setUp() throws Exception {
mTestServer = new EmbeddedTestServer();
mTestServer.initializeNative(InstrumentationRegistry.getInstrumentation().getContext(),
EmbeddedTestServer.ServerHTTPSSetting.USE_HTTP);
mTestServer.addDefaultHandlers("weblayer/test/data");
Assert.assertTrue(mTestServer.start(0));
WebLayerShellActivity activity =
mActivityTestRule.launchShellWithUrl(mTestServer.getURL("/file_input.html"));
mTempFile = File.createTempFile("file", null);
activity.setIntentInterceptor(mIntentInterceptor);
Intent response = new Intent();
response.setData(Uri.fromFile(mTempFile));
mIntentInterceptor.setResponse(Activity.RESULT_OK, response);
}
@After
public void tearDown() {
mTempFile.delete();
}
@Test
@SmallTest
public void testFileInputBasic() {
String id = "input_file";
openInputWithId(id);
Assert.assertFalse(getContentIntent().hasCategory(Intent.CATEGORY_OPENABLE));
waitForNumFiles(id, 1);
}
@Test
@SmallTest
public void testFileInputCancel() {
String id = "input_file";
// First add a file.
openInputWithId(id);
waitForNumFiles(id, 1);
// Now cancel the intent.
mIntentInterceptor.setResponse(Activity.RESULT_CANCELED, null);
openInputWithId(id);
waitForNumFiles(id, 0);
}
@Test
@SmallTest
public void testFileInputText() {
String id = "input_text";
openInputWithId(id);
Assert.assertTrue(getContentIntent().hasCategory(Intent.CATEGORY_OPENABLE));
waitForNumFiles(id, 1);
}
@Test
@SmallTest
public void testFileInputAny() {
String id = "input_any";
openInputWithId(id);
Assert.assertFalse(getContentIntent().hasCategory(Intent.CATEGORY_OPENABLE));
waitForNumFiles(id, 1);
}
@Test
@SmallTest
public void testFileInputMultiple() throws Exception {
Intent response = new Intent();
ClipData clipData = ClipData.newUri(mActivityTestRule.getActivity().getContentResolver(),
"uris", Uri.fromFile(mTempFile));
File otherTempFile = File.createTempFile("file2", null);
clipData.addItem(new ClipData.Item(Uri.fromFile(otherTempFile)));
response.setClipData(clipData);
mIntentInterceptor.setResponse(Activity.RESULT_OK, response);
String id = "input_file_multiple";
openInputWithId(id);
Intent contentIntent = getContentIntent();
Assert.assertFalse(contentIntent.hasCategory(Intent.CATEGORY_OPENABLE));
Assert.assertTrue(contentIntent.hasExtra(Intent.EXTRA_ALLOW_MULTIPLE));
waitForNumFiles(id, 2);
otherTempFile.delete();
}
@Test
@SmallTest
public void testFileInputImage() {
String id = "input_image";
openInputWithId(id);
Assert.assertEquals(
MediaStore.ACTION_IMAGE_CAPTURE, mIntentInterceptor.mLastIntent.getAction());
waitForNumFiles(id, 1);
}
@Test
@SmallTest
public void testFileInputAudio() {
String id = "input_audio";
openInputWithId(id);
Assert.assertEquals(MediaStore.Audio.Media.RECORD_SOUND_ACTION,
mIntentInterceptor.mLastIntent.getAction());
waitForNumFiles(id, 1);
}
private void openInputWithId(String id) {
// We need to click the input after user input, otherwise it won't open due to security
// policy.
mActivityTestRule.executeScriptSync(
"document.onclick = function() {document.getElementById('" + id + "').click()}");
EventUtils.simulateTouchCenterOfView(
mActivityTestRule.getActivity().getWindow().getDecorView());
mIntentInterceptor.waitForIntent();
}
private void waitForNumFiles(String id, int num) {
CriteriaHelper.pollInstrumentationThread(() -> {
return num
== mActivityTestRule.executeScriptAndExtractInt(
"document.getElementById('" + id + "').files.length");
});
}
private Intent getContentIntent() {
Assert.assertEquals(Intent.ACTION_CHOOSER, mIntentInterceptor.mLastIntent.getAction());
Intent contentIntent =
(Intent) mIntentInterceptor.mLastIntent.getParcelableExtra(Intent.EXTRA_INTENT);
Assert.assertNotNull(contentIntent);
return contentIntent;
}
}
......@@ -14,6 +14,7 @@ import android.net.Uri;
import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Assert;
......@@ -182,4 +183,28 @@ public class WebLayerShellActivityTestRule extends ActivityTestRule<WebLayerShel
}
return callbackHelper.getResult();
}
public int executeScriptAndExtractInt(String script) {
try {
return executeScriptSync(script).getInt(BrowserController.SCRIPT_RESULT_KEY);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public String executeScriptAndExtractString(String script) {
try {
return executeScriptSync(script).getString(BrowserController.SCRIPT_RESULT_KEY);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public boolean executeScriptAndExtractBoolean(String script) {
try {
return executeScriptSync(script).getBoolean(BrowserController.SCRIPT_RESULT_KEY);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
}
......@@ -59,6 +59,7 @@ public class WebLayerShellActivity extends FragmentActivity {
private int mMainViewId;
private ViewGroup mTopContentsContainer;
private BrowserFragment mFragment;
private IntentInterceptor mIntentInterceptor;
public BrowserController getBrowserController() {
return mBrowserController;
......@@ -68,6 +69,25 @@ public class WebLayerShellActivity extends FragmentActivity {
return mBrowserFragmentController;
}
/** Interface used to intercept intents for testing. */
public static interface IntentInterceptor {
void interceptIntent(Fragment fragment, Intent intent, int requestCode, Bundle options);
}
public void setIntentInterceptor(IntentInterceptor interceptor) {
mIntentInterceptor = interceptor;
}
@Override
public void startActivityFromFragment(
Fragment fragment, Intent intent, int requestCode, Bundle options) {
if (mIntentInterceptor != null) {
mIntentInterceptor.interceptIntent(fragment, intent, requestCode, options);
return;
}
super.startActivityFromFragment(fragment, intent, requestCode, options);
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
......
<html>
<body hidden>
<form action="about:blank">
<input id="input_file" type="file" />
<input id="input_text" type="file" accept="text/plain" />
<input id="input_any" type="file" accept="*/*" />
<input id="input_file_multiple" type="file" multiple />
<input id="input_image" type="file" accept="image/*" capture />
<input id="input_audio" type="file" accept="audio/*" capture />
</form>
</body>
</html>
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