From: csagan5 <32685696+csagan5@users.noreply.github.com> Date: Wed, 1 Aug 2018 09:19:40 +0200 Subject: Add bookmark import/export actions Add bookmark import/export actions in bookmarks activity and page Reduce permissions needed for bookmarks import/export Completely remove contacts picker permission from the file dialog --- base/android/content_uri_utils.cc | 10 + base/android/content_uri_utils.h | 4 + .../org/chromium/base/ContentUriUtils.java | 32 ++ chrome/android/java/AndroidManifest.xml | 1 - .../res/menu/bookmark_action_bar_menu.xml | 14 + .../browser/TabbedModeTabDelegateFactory.java | 5 +- .../browser/bookmarks/BookmarkActionBar.java | 12 + .../browser/bookmarks/BookmarkActivity.java | 30 ++ .../browser/bookmarks/BookmarkBridge.java | 277 +++++++++++++++++ .../browser/bookmarks/BookmarkDelegate.java | 10 + .../browser/bookmarks/BookmarkManager.java | 22 ++ .../browser/bookmarks/BookmarkPage.java | 8 +- .../native_page/NativePageFactory.java | 11 +- chrome/browser/BUILD.gn | 11 +- chrome/browser/about_flags.cc | 6 + .../android/bookmarks/bookmark_bridge.cc | 284 ++++++++++++++++++ .../android/bookmarks/bookmark_bridge.h | 28 +- .../browser/bookmarks/bookmark_html_writer.cc | 8 +- .../dialogs/DownloadLocationCustomView.java | 8 +- .../DownloadLocationDialogCoordinator.java | 8 +- chrome/browser/flag_descriptions.cc | 5 + chrome/browser/flag_descriptions.h | 3 + .../flags/android/chrome_feature_list.cc | 5 + .../flags/android/chrome_feature_list.h | 1 + .../browser/flags/ChromeFeatureList.java | 1 + chrome/browser/importer/profile_writer.cc | 12 + chrome/browser/importer/profile_writer.h | 6 + .../preferences/ChromePreferenceKeys.java | 3 + .../strings/android_chrome_strings.grd | 18 ++ chrome/common/BUILD.gn | 3 + chrome/utility/BUILD.gn | 7 +- .../utility/importer/bookmark_html_reader.cc | 27 +- .../utility/importer/bookmark_html_reader.h | 8 + .../chromium/ui/base/SelectFileDialog.java | 18 +- .../java/strings/android_ui_strings.grd | 3 + ui/shell_dialogs/select_file_dialog.h | 2 + .../select_file_dialog_android.cc | 6 + ui/shell_dialogs/select_file_dialog_android.h | 2 + 38 files changed, 890 insertions(+), 29 deletions(-) diff --git a/base/android/content_uri_utils.cc b/base/android/content_uri_utils.cc --- a/base/android/content_uri_utils.cc +++ b/base/android/content_uri_utils.cc @@ -30,6 +30,16 @@ File OpenContentUriForRead(const FilePath& content_uri) { return File(fd); } +File OpenContentUriForWrite(const FilePath& content_uri) { + JNIEnv* env = base::android::AttachCurrentThread(); + ScopedJavaLocalRef j_uri = + ConvertUTF8ToJavaString(env, content_uri.value()); + jint fd = Java_ContentUriUtils_openContentUriForWrite(env, j_uri); + if (fd < 0) + return File(); + return File(fd); +} + std::string GetContentUriMimeType(const FilePath& content_uri) { JNIEnv* env = base::android::AttachCurrentThread(); ScopedJavaLocalRef j_uri = diff --git a/base/android/content_uri_utils.h b/base/android/content_uri_utils.h --- a/base/android/content_uri_utils.h +++ b/base/android/content_uri_utils.h @@ -17,6 +17,10 @@ namespace base { // Returns -1 if the URI is invalid. BASE_EXPORT File OpenContentUriForRead(const FilePath& content_uri); +// Opens a content URI for write and returns the file descriptor to the caller. +// Returns -1 if the URI is invalid. +BASE_EXPORT File OpenContentUriForWrite(const FilePath& content_uri); + // Check whether a content URI exists. BASE_EXPORT bool ContentUriExists(const FilePath& content_uri); diff --git a/base/android/java/src/org/chromium/base/ContentUriUtils.java b/base/android/java/src/org/chromium/base/ContentUriUtils.java --- a/base/android/java/src/org/chromium/base/ContentUriUtils.java +++ b/base/android/java/src/org/chromium/base/ContentUriUtils.java @@ -23,6 +23,9 @@ import org.chromium.base.annotations.CalledByNative; import java.io.File; import java.io.IOException; +import android.system.Os; +import android.content.ContentProviderClient; + /** * This class provides methods to access content URI schemes. */ @@ -89,6 +92,35 @@ public abstract class ContentUriUtils { return -1; } + @CalledByNative + public static int openContentUriForWrite(String uriString) { + try { + Uri uri = Uri.parse(uriString); + ContentResolver resolver = ContextUtils.getApplicationContext().getContentResolver(); + ContentProviderClient client = resolver.acquireContentProviderClient( + uri.getAuthority()); + ParcelFileDescriptor pfd = client.openFile(uri, "rw"); + int fd = pfd.detachFd(); + client.close(); + return fd; + } catch (Exception e) { + Log.e(TAG, "Cannot open intermediate Uri.", e); + } + return -1; + } + + public static String getFilePathFromContentUri(Uri uri) { + String path = null; + try { + ContentResolver resolver = ContextUtils.getApplicationContext().getContentResolver(); + ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r"); + path = Os.readlink("/proc/self/fd/" + pfd.getFd()); + pfd.close(); + } catch (Throwable t) { + } + return path; + } + /** * Check whether a content URI exists. * diff --git a/chrome/android/java/AndroidManifest.xml b/chrome/android/java/AndroidManifest.xml --- a/chrome/android/java/AndroidManifest.xml +++ b/chrome/android/java/AndroidManifest.xml @@ -59,7 +59,6 @@ by a child template that "extends" this file. - diff --git a/chrome/android/java/res/menu/bookmark_action_bar_menu.xml b/chrome/android/java/res/menu/bookmark_action_bar_menu.xml --- a/chrome/android/java/res/menu/bookmark_action_bar_menu.xml +++ b/chrome/android/java/res/menu/bookmark_action_bar_menu.xml @@ -21,6 +21,20 @@ android:visible="false" app:showAsAction="ifRoom" app:iconTint="@color/default_icon_color_tint_list" /> + + mShareDelegateSupplier; private final Supplier mEphemeralTabCoordinatorSupplier; @@ -72,7 +73,7 @@ public class TabbedModeTabDelegateFactory implements TabDelegateFactory { private NativePageFactory mNativePageFactory; - public TabbedModeTabDelegateFactory(Activity activity, + public TabbedModeTabDelegateFactory(ChromeActivity activity, BrowserControlsVisibilityDelegate appBrowserControlsVisibilityDelegate, Supplier shareDelegateSupplier, Supplier ephemeralTabCoordinatorSupplier, diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkActionBar.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkActionBar.java --- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkActionBar.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkActionBar.java @@ -83,6 +83,12 @@ public class BookmarkActionBar extends SelectableListToolbar } else if (menuItem.getItemId() == R.id.search_menu_id) { mDelegate.openSearchUI(); return true; + } else if (menuItem.getItemId() == R.id.import_menu_id) { + mDelegate.importBookmarks(); + return true; + } else if (menuItem.getItemId() == R.id.export_menu_id) { + mDelegate.exportBookmarks(); + return true; } SelectionDelegate selectionDelegate = mDelegate.getSelectionDelegate(); @@ -134,6 +140,8 @@ public class BookmarkActionBar extends SelectableListToolbar void showLoadingUi() { setTitle(null); setNavigationButton(NAVIGATION_BUTTON_NONE); + getMenu().findItem(R.id.import_menu_id).setVisible(false); + getMenu().findItem(R.id.export_menu_id).setVisible(false); getMenu().findItem(R.id.search_menu_id).setVisible(false); getMenu().findItem(R.id.edit_menu_id).setVisible(false); } @@ -143,6 +151,8 @@ public class BookmarkActionBar extends SelectableListToolbar super.showNormalView(); if (mDelegate == null) { + getMenu().findItem(R.id.import_menu_id).setVisible(false); + getMenu().findItem(R.id.export_menu_id).setVisible(false); getMenu().findItem(R.id.search_menu_id).setVisible(false); getMenu().findItem(R.id.edit_menu_id).setVisible(false); } @@ -173,6 +183,8 @@ public class BookmarkActionBar extends SelectableListToolbar public void onFolderStateSet(BookmarkId folder) { mCurrentFolder = mDelegate.getModel().getBookmarkById(folder); + getMenu().findItem(R.id.import_menu_id).setVisible(true); + getMenu().findItem(R.id.export_menu_id).setVisible(true); getMenu().findItem(R.id.search_menu_id).setVisible(true); getMenu().findItem(R.id.edit_menu_id).setVisible(mCurrentFolder.isEditable()); diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkActivity.java --- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkActivity.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkActivity.java @@ -15,6 +15,11 @@ import org.chromium.chrome.browser.IntentHandler; import org.chromium.chrome.browser.SnackbarActivity; import org.chromium.components.bookmarks.BookmarkId; import org.chromium.components.embedder_support.util.UrlConstants; +import org.chromium.ui.base.ActivityWindowAndroid; +import org.chromium.ui.base.IntentRequestTracker; + +import org.chromium.ui.modaldialog.ModalDialogManager; +import org.chromium.components.browser_ui.modaldialog.AppModalPresenter; /** * The activity that displays the bookmark UI on the phone. It keeps a {@link BookmarkManager} @@ -24,6 +29,7 @@ import org.chromium.components.embedder_support.util.UrlConstants; public class BookmarkActivity extends SnackbarActivity { private BookmarkManager mBookmarkManager; + private ActivityWindowAndroid mWindowAndroid; static final int EDIT_BOOKMARK_REQUEST_CODE = 14; public static final String INTENT_VISIT_BOOKMARK_ID = "BookmarkEditActivity.VisitBookmarkId"; @@ -40,8 +46,23 @@ public class BookmarkActivity extends SnackbarActivity { if (TextUtils.isEmpty(url)) url = UrlConstants.BOOKMARKS_URL; mBookmarkManager.updateForUrl(url); setContentView(mBookmarkManager.getView()); + + final boolean listenToActivityState = true; + IntentRequestTracker intentRequestTracker = IntentRequestTracker.createFromActivity(this); + mWindowAndroid = new ActivityWindowAndroid(this, listenToActivityState, intentRequestTracker); + mWindowAndroid.getIntentRequestTracker().restoreInstanceState(savedInstanceState); + mBookmarkManager.setWindow(mWindowAndroid, + new ModalDialogManager( + new AppModalPresenter(this), ModalDialogManager.ModalDialogType.APP)); } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + mWindowAndroid.getIntentRequestTracker().saveInstanceState(outState); + } + @Override protected void onDestroy() { super.onDestroy(); @@ -56,6 +77,7 @@ public class BookmarkActivity extends SnackbarActivity { @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); + mWindowAndroid.getIntentRequestTracker().onActivityResult(requestCode, resultCode, data, mWindowAndroid); if (requestCode == EDIT_BOOKMARK_REQUEST_CODE && resultCode == RESULT_OK) { BookmarkId bookmarkId = BookmarkId.getBookmarkIdFromString(data.getStringExtra( INTENT_VISIT_BOOKMARK_ID)); @@ -63,6 +85,14 @@ public class BookmarkActivity extends SnackbarActivity { } } + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + if (mWindowAndroid.handlePermissionResult(requestCode, permissions, grantResults)) + return; + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + /** * @return The {@link BookmarkManager} for testing purposes. */ diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkBridge.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkBridge.java --- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkBridge.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkBridge.java @@ -4,7 +4,19 @@ package org.chromium.chrome.browser.bookmarks; +import android.content.Intent; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.content.ContentResolver; +import android.provider.Browser; +import android.provider.DocumentsContract; +import android.Manifest.permission; +import androidx.appcompat.app.AlertDialog; import android.os.SystemClock; +import android.os.Build; import android.text.TextUtils; import android.util.Pair; @@ -27,8 +39,33 @@ import org.chromium.components.url_formatter.UrlFormatter; import org.chromium.content_public.browser.WebContents; import org.chromium.url.GURL; +import org.chromium.base.ContentUriUtils; +import org.chromium.chrome.R; +import org.chromium.chrome.browser.document.ChromeLauncherActivity; +import org.chromium.chrome.browser.IntentHandler; +import org.chromium.chrome.browser.preferences.ChromePreferenceKeys; +import org.chromium.chrome.browser.preferences.SharedPreferencesManager; +import org.chromium.chrome.browser.flags.ChromeFeatureList; +import org.chromium.ui.base.PageTransition; +import org.chromium.ui.base.WindowAndroid; +import org.chromium.ui.modaldialog.ModalDialogManager; + +import android.view.View; +import android.view.LayoutInflater; +import org.chromium.ui.modelutil.PropertyModel; +import org.chromium.ui.modaldialog.ModalDialogProperties; +import org.chromium.ui.modaldialog.DialogDismissalCause; +import org.chromium.chrome.browser.download.DownloadLocationDialogType; +import org.chromium.chrome.browser.download.dialogs.DownloadLocationDialogController; +import org.chromium.chrome.browser.download.dialogs.DownloadLocationDialogCoordinator; +import org.chromium.chrome.browser.download.dialogs.DownloadLocationCustomView; +import org.chromium.chrome.browser.download.DirectoryOption; +import android.content.res.Resources; +import org.chromium.base.task.AsyncTask; + import java.util.ArrayList; import java.util.List; +import java.io.File; /** * Provides the communication channel for Android to fetch and manipulate the @@ -585,6 +622,210 @@ public class BookmarkBridge { mNativeBookmarkBridge, BookmarkBridge.this, id.getId(), id.getType()); } + /** + * Import bookmarks from a selected file. + * @param window The current window of the bookmarks activity or page. + */ + public void importBookmarks(WindowAndroid window) { + assert mIsNativeBookmarkModelLoaded; + BookmarkBridgeJni.get().importBookmarks(mNativeBookmarkBridge, BookmarkBridge.this, window); + } + + /** + * Export bookmarks to a path selected by the user. + * @param window The current window of the bookmarks activity or page. + */ + public void exportBookmarks(WindowAndroid window, ModalDialogManager modalDialogManager) { + assert mIsNativeBookmarkModelLoaded; + if (ChromeFeatureList.isEnabled(ChromeFeatureList.BOOKMARKS_EXPORT_USESAF) || + Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) + exportBookmarksImplUseSaf(window); + else + exportBookmarksImplUseFile(window, modalDialogManager); + } + + private void exportBookmarksImplUseSaf(WindowAndroid window) { + Context context = window.getContext().get(); + + // standard name for boorkmark file + final String standardBoorkmarkName = "bookmarks.html"; + + // use the fileSelector and saf asking user for the file + Intent fileSelector = new Intent(Intent.ACTION_CREATE_DOCUMENT); + fileSelector.addCategory(Intent.CATEGORY_OPENABLE); + fileSelector.setType("text/html"); + fileSelector.putExtra(Intent.EXTRA_TITLE, standardBoorkmarkName); + fileSelector.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | + Intent.FLAG_GRANT_READ_URI_PERMISSION | + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + + // get last exported uri path, if any + SharedPreferencesManager sharedPrefs = SharedPreferencesManager.getInstance(); + String bookmarksPath = sharedPrefs.readString(ChromePreferenceKeys.BOOKMARKS_LAST_EXPORT_URI, standardBoorkmarkName); + Uri lastSelectedUri = Uri.parse(bookmarksPath); + + // prepare delegate for file selector + DialogInterface.OnClickListener onClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int button) { + if (button == AlertDialog.BUTTON_NEGATIVE) { + window.showIntent(fileSelector, + new WindowAndroid.IntentCallback() { + @Override + public void onIntentCompleted(WindowAndroid window, int resultCode, Intent data) { + if (data == null) return; + Uri filePath = data.getData(); + doExportBookmarksImpl(window, filePath); + } + }, + null); + } else { + if (dialog!=null) dialog.dismiss(); + doExportBookmarksImpl(window, lastSelectedUri); + } + } + }; + + // as a workaround for https://issuetracker.google.com/issues/37136466 + // ask to overwrite if is a valid uri and the file is present + if (DocumentsContract.isDocumentUri(context, lastSelectedUri)) { + AsyncTask checkUriTask = new AsyncTask() { + boolean uriExists = false; + String actualFilePath = null; + + @Override + protected Void doInBackground() { + uriExists = ContentUriUtils.contentUriExists(lastSelectedUri.toString()); + if (uriExists) { + actualFilePath = ContentUriUtils.getFilePathFromContentUri(lastSelectedUri); + // get real actual file name on disk + if (actualFilePath==null) actualFilePath = lastSelectedUri.toString(); + // set file name to last exported file name + fileSelector.putExtra(Intent.EXTRA_TITLE, + ContentUriUtils.getDisplayName(lastSelectedUri, context, + DocumentsContract.Document.COLUMN_DISPLAY_NAME)); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + // check for permissions + if (uriExists) { + AlertDialog.Builder alert = + new AlertDialog.Builder(context, R.style.Theme_Chromium_AlertDialog); + AlertDialog alertDialog = + alert.setTitle(R.string.export_bookmarks_alert_title) + .setMessage(context.getString(R.string.export_bookmarks_alert_message, actualFilePath)) + .setPositiveButton( + R.string.export_bookmarks_alert_message_yes, onClickListener) + .setNegativeButton(R.string.export_bookmarks_alert_message_no, onClickListener) + .create(); + alertDialog.getDelegate().setHandleNativeActionModesEnabled(false); + + // show dialog asking for overwrite + alertDialog.show(); + return; + } else { + onClickListener.onClick(null, AlertDialog.BUTTON_NEGATIVE); + } + } + }; + checkUriTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + return; + } + + // actually open the file selector + onClickListener.onClick(null, AlertDialog.BUTTON_NEGATIVE); + } + + private void doExportBookmarksImpl(WindowAndroid window, Uri filePath) { + ContentResolver resolver = ContextUtils.getApplicationContext().getContentResolver(); + // since we want to persist the uri in settings, ask for persistable permissions + resolver.takePersistableUriPermission(filePath, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | + Intent.FLAG_GRANT_READ_URI_PERMISSION); + + BookmarkBridgeJni.get().exportBookmarks(mNativeBookmarkBridge, BookmarkBridge.this, + window, filePath.toString()); + } + + private void exportBookmarksImplUseFile(WindowAndroid window, ModalDialogManager modalDialogManager) { + Context context = window.getContext().get(); + + // standard name for boorkmark file + final String standardBoorkmarkName = "bookmarks.html"; + + // use the download ui and standard file saving + DownloadLocationDialogController controller = new DownloadLocationDialogController() { + @Override + public void onDownloadLocationDialogComplete(String returnedPath) {} + + @Override + public void onDownloadLocationDialogCanceled() {} + }; + + DownloadLocationDialogCoordinator dialog = new DownloadLocationDialogCoordinator() { + @Override + protected void onDirectoryOptionsRetrieved(ArrayList dirs) { + if (mDialogModel != null) return; + + // Actually show the dialog. + mCustomView = (DownloadLocationCustomView) LayoutInflater.from(mContext).inflate( + R.layout.download_location_dialog, null); + mCustomView.initialize( + DownloadLocationDialogType.DEFAULT, new File(standardBoorkmarkName), + /*totalBytes*/ 0, context.getString(R.string.export_bookmarks_alert_title)); + mCustomView.mDontShowAgain.setVisibility(View.GONE); + + Resources resources = mContext.getResources(); + mDialogModel = new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS) + .with(ModalDialogProperties.CONTROLLER, this) + .with(ModalDialogProperties.CUSTOM_VIEW, mCustomView) + .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, resources, + R.string.export_bookmarks) + .with(ModalDialogProperties.PRIMARY_BUTTON_FILLED, true) + .with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, resources, + R.string.cancel) + .build(); + + mModalDialogManager.showDialog(mDialogModel, ModalDialogManager.ModalDialogType.APP); + } + + @Override + public void onDismiss(PropertyModel model, int dismissalCause) { + switch (dismissalCause) { + case DialogDismissalCause.POSITIVE_BUTTON_CLICKED: + { + String fileName = mCustomView.getFileName(); + String directory = mCustomView.getDirectoryOption().location; + if (fileName != null && directory != null) { + File file = new File(directory, fileName); + + if (window.hasPermission(permission.WRITE_EXTERNAL_STORAGE)) { + BookmarkBridgeJni.get().exportBookmarks(mNativeBookmarkBridge, + BookmarkBridge.this, window, file.getPath()); + } else { + String[] requestPermissions = new String[] {permission.WRITE_EXTERNAL_STORAGE}; + window.requestPermissions(requestPermissions, (permissions, grantResults) -> { + if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + BookmarkBridgeJni.get().exportBookmarks(mNativeBookmarkBridge, + BookmarkBridge.this, window, file.getPath()); + } + }); + }; + } + } + break; + } + mDialogModel = null; + mCustomView = null; + } + }; + dialog.initialize(controller); + dialog.showDialog(context, modalDialogManager, /*totalBytes*/ 0, + DownloadLocationDialogType.DEFAULT, /*suggestedPath*/ ""); + } + /** * Synchronously gets a list of bookmarks that match the specified search query. * @param query Keyword used for searching bookmarks. @@ -1021,6 +1262,39 @@ public class BookmarkBridge { depthList.add(depth); } + @CalledByNative + public void bookmarksExported(WindowAndroid window, String bookmarksPath, boolean success) { + Uri uri = Uri.parse(bookmarksPath); + + if (success == false) { + ((Activity)window.getContext().get()).runOnUiThread(new Runnable() { + public void run() { + window.showError(R.string.saving_file_error); + } + }); + } else { + SharedPreferencesManager sharedPrefs = SharedPreferencesManager.getInstance(); + sharedPrefs.writeString(ChromePreferenceKeys.BOOKMARKS_LAST_EXPORT_URI, bookmarksPath); + + Context context = ContextUtils.getApplicationContext(); + + Intent intent = new Intent(Intent.ACTION_VIEW, + ContentUriUtils.isContentUri(bookmarksPath) ? + Uri.parse(bookmarksPath) : Uri.parse("file://" + bookmarksPath)); + intent.putExtra(Browser.EXTRA_APPLICATION_ID, + context.getPackageName()); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(IntentHandler.EXTRA_PAGE_TRANSITION_TYPE, PageTransition.AUTO_BOOKMARK); + + // If the bookmark manager is shown in a tab on a phone (rather than in a separate + // activity) the component name may be null. Send the intent through + // ChromeLauncherActivity instead to avoid crashing. See crbug.com/615012. + intent.setClass(context, ChromeLauncherActivity.class); + + IntentHandler.startActivityForTrustedIntent(intent); + } + } + private static List> createPairsList(int[] left, int[] right) { List> pairList = new ArrayList>(); for (int i = 0; i < left.length; i++) { @@ -1088,6 +1362,9 @@ public class BookmarkBridge { int getChildCount(long nativeBookmarkBridge, BookmarkBridge caller, long id, int type); void getChildIDs(long nativeBookmarkBridge, BookmarkBridge caller, long id, int type, List bookmarksList); + void importBookmarks(long nativeBookmarkBridge, BookmarkBridge caller, WindowAndroid window); + void exportBookmarks(long nativeBookmarkBridge, BookmarkBridge caller, WindowAndroid window, + String export_path); BookmarkId getChildAt( long nativeBookmarkBridge, BookmarkBridge caller, long id, int type, int index); int getTotalBookmarkCount( diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkDelegate.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkDelegate.java --- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkDelegate.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkDelegate.java @@ -67,6 +67,16 @@ interface BookmarkDelegate { */ void openSearchUI(); + /** + * Imports bookmarks from user-selected file. + */ + void importBookmarks(); + + /** + * Exports bookmarks to downloads directory. + */ + void exportBookmarks(); + /** * Dismisses the search UI. */ diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManager.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManager.java --- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManager.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManager.java @@ -33,6 +33,8 @@ import org.chromium.components.browser_ui.util.ConversionUtils; import org.chromium.components.browser_ui.widget.dragreorder.DragStateDelegate; import org.chromium.components.browser_ui.widget.selectable_list.SelectableListLayout; import org.chromium.components.browser_ui.widget.selectable_list.SelectableListToolbar.SearchDelegate; +import org.chromium.ui.base.ActivityWindowAndroid; +import org.chromium.ui.modaldialog.ModalDialogManager; import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate; import org.chromium.components.favicon.LargeIconBridge; import org.chromium.url.GURL; @@ -55,6 +57,8 @@ public class BookmarkManager private ComponentName mOpenBookmarkComponentName; private ViewGroup mMainView; private BookmarkModel mBookmarkModel; + private ActivityWindowAndroid mWindowAndroid; + private ModalDialogManager mModalDialogManager; private BookmarkUndoController mUndoController; private final ObserverList mUIObservers = new ObserverList<>(); private BasicNativePage mNativePage; @@ -331,6 +335,14 @@ public class BookmarkManager mNativePage = nativePage; } + /** + * Sets the Android window that is used by further intents created by the bookmark activity. + */ + public void setWindow(ActivityWindowAndroid window, ModalDialogManager modalDialogManager) { + mWindowAndroid = window; + mModalDialogManager = modalDialogManager; + } + /** * @return Current URL representing the UI state of bookmark manager. If no state has been shown * yet in this session, on phone return last used state stored in preference; on tablet @@ -509,6 +521,16 @@ public class BookmarkManager } } + @Override + public void importBookmarks() { + mBookmarkModel.importBookmarks(mWindowAndroid); + } + + @Override + public void exportBookmarks() { + mBookmarkModel.exportBookmarks(mWindowAndroid, mModalDialogManager); + } + @Override public void openSearchUI() { setState(BookmarkUIState.createSearchState()); diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkPage.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkPage.java --- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkPage.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkPage.java @@ -13,6 +13,9 @@ import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager; import org.chromium.chrome.browser.ui.native_page.BasicNativePage; import org.chromium.chrome.browser.ui.native_page.NativePageHost; import org.chromium.components.embedder_support.util.UrlConstants; +import org.chromium.chrome.browser.app.ChromeActivity; +import org.chromium.ui.modaldialog.ModalDialogManager; +import org.chromium.components.browser_ui.modaldialog.AppModalPresenter; /** * A native page holding a {@link BookmarkManager} on _tablet_. @@ -29,12 +32,15 @@ public class BookmarkPage extends BasicNativePage { * @param host A NativePageHost to load urls. */ public BookmarkPage(ComponentName componentName, SnackbarManager snackbarManager, - boolean isIncognito, NativePageHost host) { + boolean isIncognito, NativePageHost host, ChromeActivity activity) { super(host); mManager = new BookmarkManager( host.getContext(), componentName, false, isIncognito, snackbarManager); mManager.setBasicNativePage(this); + mManager.setWindow(activity.getWindowAndroid(), + new ModalDialogManager( + new AppModalPresenter(activity), ModalDialogManager.ModalDialogType.APP)); mTitle = host.getContext().getResources().getString(R.string.bookmarks); initWithView(mManager.getView()); diff --git a/chrome/android/java/src/org/chromium/chrome/browser/native_page/NativePageFactory.java b/chrome/android/java/src/org/chromium/chrome/browser/native_page/NativePageFactory.java --- a/chrome/android/java/src/org/chromium/chrome/browser/native_page/NativePageFactory.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/native_page/NativePageFactory.java @@ -15,6 +15,7 @@ import org.chromium.base.jank_tracker.JankTracker; import org.chromium.base.supplier.BooleanSupplier; import org.chromium.base.supplier.DestroyableObservableSupplier; import org.chromium.base.supplier.Supplier; +import org.chromium.chrome.browser.app.ChromeActivity; import org.chromium.chrome.browser.bookmarks.BookmarkPage; import org.chromium.chrome.browser.browser_controls.BrowserControlsMarginSupplier; import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider; @@ -54,7 +55,7 @@ import org.chromium.ui.util.ColorUtils; * Creates NativePage objects to show chrome-native:// URLs using the native Android view system. */ public class NativePageFactory { - private final Activity mActivity; + private final ChromeActivity mActivity; private final BottomSheetController mBottomSheetController; private final BrowserControlsManager mBrowserControlsManager; private final Supplier mCurrentTabSupplier; @@ -72,7 +73,7 @@ public class NativePageFactory { private NativePageBuilder mNativePageBuilder; - public NativePageFactory(@NonNull Activity activity, + public NativePageFactory(@NonNull ChromeActivity activity, @NonNull BottomSheetController sheetController, @NonNull BrowserControlsManager browserControlsManager, @NonNull Supplier currentTabSupplier, @@ -123,7 +124,7 @@ public class NativePageFactory { @VisibleForTesting static class NativePageBuilder { - private final Activity mActivity; + private final ChromeActivity mActivity; private final BottomSheetController mBottomSheetController; private final Supplier mUma; private final BrowserControlsManager mBrowserControlsManager; @@ -137,7 +138,7 @@ public class NativePageFactory { private final JankTracker mJankTracker; private final Supplier mToolbarSupplier; - public NativePageBuilder(Activity activity, Supplier uma, + public NativePageBuilder(ChromeActivity activity, Supplier uma, BottomSheetController sheetController, BrowserControlsManager browserControlsManager, Supplier currentTabSupplier, Supplier snackbarManagerSupplier, @@ -176,7 +177,7 @@ public class NativePageFactory { protected NativePage buildBookmarksPage(Tab tab) { return new BookmarkPage(mActivity.getComponentName(), mSnackbarManagerSupplier.get(), mTabModelSelector.isIncognitoSelected(), - new TabShim(tab, mBrowserControlsManager, mTabModelSelector)); + new TabShim(tab, mBrowserControlsManager, mTabModelSelector), mActivity); } protected NativePage buildDownloadsPage(Tab tab) { diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn --- a/chrome/browser/BUILD.gn +++ b/chrome/browser/BUILD.gn @@ -1817,6 +1817,13 @@ static_library("browser") { "window_placement/window_placement_permission_context.h", ] + if (is_android) { + sources += [ + "importer/profile_writer.cc", + "importer/profile_writer.h", + ] + } + configs += [ "//build/config/compiler:wexit_time_destructors", "//build/config:precompiled_headers", @@ -2938,6 +2945,8 @@ static_library("browser") { "autofill/manual_filling_view_interface.h", "banners/android/chrome_app_banner_manager_android.cc", "banners/android/chrome_app_banner_manager_android.h", + "bookmarks/bookmark_html_writer.cc", + "bookmarks/bookmark_html_writer.h", "browser_process_platform_part_android.cc", "browser_process_platform_part_android.h", "chrome_browser_field_trials_mobile.cc", @@ -3483,8 +3492,6 @@ static_library("browser") { "badging/badge_manager_factory.h", "banners/app_banner_manager_desktop.cc", "banners/app_banner_manager_desktop.h", - "bookmarks/bookmark_html_writer.cc", - "bookmarks/bookmark_html_writer.h", "browsing_data/chrome_browsing_data_lifetime_manager.cc", "browsing_data/chrome_browsing_data_lifetime_manager.h", "browsing_data/chrome_browsing_data_lifetime_manager_factory.cc", diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc --- a/chrome/browser/about_flags.cc +++ b/chrome/browser/about_flags.cc @@ -7567,6 +7567,12 @@ const FeatureEntry kFeatureEntries[] = { FEATURE_VALUE_TYPE( password_manager::features::kPasswordsAccountStorageRevisedOptInFlow)}, + {"export-bookmarks-use-saf", + flag_descriptions::kBookmarksExportUseSafName, + flag_descriptions::kBookmarksExportUseSafDescription, kOsAndroid, + FEATURE_VALUE_TYPE( + chrome::android::kBookmarksExportUseSaf)}, + // NOTE: Adding a new flag requires adding a corresponding entry to enum // "LoginCustomFlags" in tools/metrics/histograms/enums.xml. See "Flag // Histograms" in tools/metrics/histograms/README.md (run the diff --git a/chrome/browser/android/bookmarks/bookmark_bridge.cc b/chrome/browser/android/bookmarks/bookmark_bridge.cc --- a/chrome/browser/android/bookmarks/bookmark_bridge.cc +++ b/chrome/browser/android/bookmarks/bookmark_bridge.cc @@ -39,6 +39,7 @@ #include "components/bookmarks/common/android/bookmark_type.h" #include "components/bookmarks/common/bookmark_pref_names.h" #include "components/bookmarks/managed/managed_bookmark_service.h" +#include "components/favicon_base/favicon_usage_data.h" #include "components/dom_distiller/core/url_utils.h" #include "components/prefs/pref_service.h" #include "components/query_parser/query_parser.h" @@ -48,6 +49,24 @@ #include "content/public/browser/browser_thread.h" #include "content/public/browser/web_contents.h" +#include "base/android/content_uri_utils.h" +#include "base/android/path_utils.h" +#include "base/strings/utf_string_conversions.h" +#include "chrome/utility/importer/bookmark_html_reader.h" +#include "chrome/browser/bookmarks/bookmark_html_writer.h" +#include "chrome/browser/importer/profile_writer.h" +#include "chrome/browser/platform_util.h" +#include "chrome/browser/ui/chrome_select_file_policy.h" +#include "chrome/common/importer/imported_bookmark_entry.h" +#include "chrome/common/importer/importer_data_types.h" +#include "chrome/common/url_constants.h" +#include "components/search_engines/template_url.h" +#include "components/url_formatter/url_fixer.h" +#include "ui/android/window_android.h" +#include "base/task/task_traits.h" +#include "base/task/thread_pool.h" +#include "content/public/browser/browser_task_traits.h" + using base::android::AttachCurrentThread; using base::android::ConvertUTF8ToJavaString; using base::android::ConvertUTF16ToJavaString; @@ -64,8 +83,93 @@ using bookmarks::BookmarkNode; using bookmarks::BookmarkType; using content::BrowserThread; +namespace internal { + +// Returns true if |url| has a valid scheme that we allow to import. We +// filter out the URL with a unsupported scheme. +bool CanImportURL(const GURL& url) { + // The URL is not valid. + if (!url.is_valid()) + return false; + + // Filter out the URLs with unsupported schemes. + const char* const kInvalidSchemes[] = {"wyciwyg", "place"}; + for (size_t i = 0; i < base::size(kInvalidSchemes); ++i) { + if (url.SchemeIs(kInvalidSchemes[i])) + return false; + } + + // Check if |url| is about:blank. + if (url == url::kAboutBlankURL) + return true; + + // If |url| starts with chrome:// or about:, check if it's one of the URLs + // that we support. + if (url.SchemeIs(content::kChromeUIScheme) || + url.SchemeIs(url::kAboutScheme)) { + if (url.host_piece() == chrome::kChromeUIAboutHost) + return true; + + GURL fixed_url(url_formatter::FixupURL(url.spec(), std::string())); + for (size_t i = 0; i < chrome::kNumberOfChromeHostURLs; ++i) { + if (fixed_url.DomainIs(chrome::kChromeHostURLs[i])) + return true; + } + + for (size_t i = 0; i < chrome::kNumberOfChromeDebugURLs; ++i) { + if (fixed_url == chrome::kChromeDebugURLs[i]) + return true; + } + + // If url has either chrome:// or about: schemes but wasn't found in the + // above lists, it means we don't support it, so we don't allow the user + // to import it. + return false; + } + + // Otherwise, we assume the url has a valid (importable) scheme. + return true; +} + +} // internal + namespace { +class FileBookmarksExportObserver: public BookmarksExportObserver { + public: + FileBookmarksExportObserver( + const JavaParamRef& obj, + ui::WindowAndroid* window, + const std::string& export_path) : + obj_(ScopedJavaGlobalRef(obj)), + window_(window), + export_path_(export_path) {} + + void OnExportFinished(Result result) override { + if (result == Result::kSuccess) { + LOG(INFO) << "Bookmarks exported successfully to " << export_path_; + } else if (result == Result::kCouldNotCreateFile) { + LOG(ERROR) << "Bookmarks export: could not create file " << export_path_; + } else if (result == Result::kCouldNotWriteHeader) { + LOG(ERROR) << "Bookmarks export: could not write header"; + } else if (result == Result::kCouldNotWriteNodes) { + LOG(ERROR) << "Bookmarks export: could not write nodes"; + } + + JNIEnv* env = AttachCurrentThread(); + Java_BookmarkBridge_bookmarksExported(env, obj_, window_->GetJavaObject(), + ConvertUTF8ToJavaString(env, export_path_), + result == Result::kSuccess); + delete this; + } + + private: + const ScopedJavaGlobalRef obj_; + ui::WindowAndroid* window_; + const std::string export_path_; +}; + + const int kInvalidId = -1; class BookmarkTitleComparer { @@ -152,6 +256,10 @@ BookmarkBridge::~BookmarkBridge() { if (partner_bookmarks_shim_) partner_bookmarks_shim_->RemoveObserver(this); reading_list_manager_->RemoveObserver(this); + // 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 BookmarkBridge::Destroy(JNIEnv*, const JavaParamRef&) { @@ -543,6 +651,182 @@ jint BookmarkBridge::GetTotalBookmarkCount( return count; } +void BookmarkBridge::ImportBookmarks(JNIEnv* env, + const JavaParamRef& obj, + const JavaParamRef& java_window) { + DCHECK(IsLoaded()); + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + ui::WindowAndroid* window = + ui::WindowAndroid::FromJavaWindowAndroid(java_window); + CHECK(window); + + select_file_dialog_ = ui::SelectFileDialog::Create( + this, std::make_unique(nullptr)); + + //NOTE: extension and description are not used on Android, thus not set + ui::SelectFileDialog::FileTypeInfo file_type_info; + + const std::vector v_accept_types = { u"text/html" }; + + // Android needs the original MIME types and an additional capture value. + std::pair, bool> accept_types = + std::make_pair(v_accept_types, /* use_media_capture */ false); + + select_file_dialog_->SelectFile( + ui::SelectFileDialog::SELECT_OPEN_FILE, + std::u16string(), + export_path_, + &file_type_info, + 0, + base::FilePath::StringType(), + window, + &accept_types + ); +} + +void BookmarkBridge::ExportBookmarks(JNIEnv* env, + const JavaParamRef& obj, + const JavaParamRef& java_window, + const JavaParamRef& j_export_path) { + DCHECK(IsLoaded()); + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + ui::WindowAndroid* window = + ui::WindowAndroid::FromJavaWindowAndroid(java_window); + CHECK(window); + + std::u16string export_path = + base::android::ConvertJavaStringToUTF16(env, j_export_path); + + export_path_ = base::FilePath::FromUTF16Unsafe(export_path); + + if (export_path_.empty()) { + if (!base::android::GetDownloadsDirectory(&export_path_)) { + LOG(ERROR) << "Could not retrieve downloads directory for bookmarks export"; + return; + } + export_path_ = export_path_.Append(FILE_PATH_LITERAL("bookmarks.html")); + } + + observer_ = new FileBookmarksExportObserver(obj, window, export_path_.MaybeAsASCII()); + bookmark_html_writer::WriteBookmarks(profile_, export_path_, observer_); +} + +// Attempts to create a TemplateURL from the provided data. |title| is optional. +// If TemplateURL creation fails, returns null. +std::unique_ptr CreateTemplateURL(const std::u16string& url, + const std::u16string& keyword, + const std::u16string& title) { + if (url.empty() || keyword.empty()) + return nullptr; + TemplateURLData data; + data.SetKeyword(keyword); + // We set short name by using the title if it exists. + // Otherwise, we use the shortcut. + data.SetShortName(title.empty() ? keyword : title); + data.SetURL(TemplateURLRef::DisplayURLToURLRef(url)); + return std::make_unique(data); +} + +void BookmarkBridge::FileSelected(const base::FilePath& path, int index, + void* params) { + base::ThreadPool::PostTaskAndReplyWithResult( + FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()}, + base::BindOnce(&BookmarkBridge::FileSelectedImpl, + base::Unretained(this), + path), + base::BindOnce(&BookmarkBridge::FileSelectedImplOnUIThread, + base::Unretained(this), + path)); +} + +const std::string BookmarkBridge::FileSelectedImpl(const base::FilePath& path) { + base::File file; + if (path.IsContentUri()) { + file = base::OpenContentUriForRead(path); + } else { + file.Initialize(path, base::File::FLAG_OPEN | base::File::FLAG_READ); + } + if (!file.IsValid()) { + select_file_dialog_->ShowToast("Cannot open bookmarks file for import"); + return ""; + } + + auto fileLength = file.GetLength(); + if (-1 == fileLength) { + select_file_dialog_->ShowToast("Cannot read bookmarks file length"); + return ""; + } + + if (fileLength > 10 * 1024 * 1024) { + select_file_dialog_->ShowToast("Bookmark file is bigger than 10MB"); + return ""; + } + + std::vector buffer(fileLength); + if (-1 == file.ReadAtCurrentPos(buffer.data(), fileLength)) { + select_file_dialog_->ShowToast("Could not read bookmarks file"); + return ""; + } + + if (buffer.empty()) { + select_file_dialog_->ShowToast("Empty bookmarks file"); + return ""; + } + + std::string contents(buffer.begin(), buffer.end()); + return contents; +} + +void BookmarkBridge::FileSelectedImplOnUIThread(const base::FilePath& path, + const std::string& contents) { + if (contents.empty()) + return; + + // the following import logic comes from BookmarksFileImporter class + std::vector bookmarks; + std::vector search_engines; + favicon_base::FaviconUsageDataList favicons; + + bookmark_html_reader::ImportBookmarksFile( + base::RepeatingCallback(), + base::BindRepeating(internal::CanImportURL), + contents, + &bookmarks, + &search_engines, + &favicons); + + auto *writer = new ProfileWriter(profile_); + + if (!bookmarks.empty()) { + // adding bookmarks will begin extensive changes to the model + writer->AddBookmarksWithModel(bookmark_model_, bookmarks, u"Imported"); + } + if (!search_engines.empty()) { + TemplateURLService::OwnedTemplateURLVector owned_template_urls; + for (const auto& search_engine : search_engines) { + std::unique_ptr owned_template_url = CreateTemplateURL( + search_engine.url, search_engine.keyword, search_engine.display_name); + if (owned_template_url) + owned_template_urls.push_back(std::move(owned_template_url)); + } + writer->AddKeywords(std::move(owned_template_urls), false); + } + + std::stringstream message; + message << "Imported " << bookmarks.size() << " bookmarks and " << + search_engines.size() << " search engines from " << path.MaybeAsASCII(); + auto result = message.str(); + + select_file_dialog_->ShowToast(result); + + LOG(INFO) << result; +} + +void BookmarkBridge::FileSelectionCanceled(void* params) { +} + void BookmarkBridge::SetBookmarkTitle(JNIEnv* env, const JavaParamRef& obj, jlong id, diff --git a/chrome/browser/android/bookmarks/bookmark_bridge.h b/chrome/browser/android/bookmarks/bookmark_bridge.h --- a/chrome/browser/android/bookmarks/bookmark_bridge.h +++ b/chrome/browser/android/bookmarks/bookmark_bridge.h @@ -17,6 +17,7 @@ #include "base/scoped_observation.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/android/bookmarks/partner_bookmarks_shim.h" +#include "chrome/browser/bookmarks/bookmark_html_writer.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/profiles/profile_observer.h" #include "chrome/browser/reading_list/android/reading_list_manager.h" @@ -25,6 +26,9 @@ #include "components/prefs/pref_change_registrar.h" #include "url/android/gurl_android.h" +#include "components/search_engines/template_url.h" +#include "ui/shell_dialogs/select_file_dialog.h" + namespace bookmarks { class BookmarkModel; class ManagedBookmarkService; @@ -39,7 +43,8 @@ class Profile; class BookmarkBridge : public bookmarks::BaseBookmarkModelObserver, public PartnerBookmarksShim::Observer, public ReadingListManager::Observer, - public ProfileObserver { + public ProfileObserver, + public ui::SelectFileDialog::Listener { public: BookmarkBridge(JNIEnv* env, const base::android::JavaRef& obj, @@ -55,6 +60,12 @@ class BookmarkBridge : public bookmarks::BaseBookmarkModelObserver, bool IsDoingExtensiveChanges(JNIEnv* env, const base::android::JavaParamRef& obj); + // SelectFileDialog::Listener implementation. + void FileSelected(const base::FilePath& path, + int index, + void* params) override; + void FileSelectionCanceled(void* params) override; + jboolean IsEditBookmarksEnabled(JNIEnv* env); void LoadEmptyPartnerBookmarkShimForTesting( @@ -142,6 +153,15 @@ class BookmarkBridge : public bookmarks::BaseBookmarkModelObserver, jlong id, jint type); + void ImportBookmarks(JNIEnv* env, + const base::android::JavaParamRef& obj, + const base::android::JavaParamRef& java_window); + + void ExportBookmarks(JNIEnv* env, + const base::android::JavaParamRef& obj, + const base::android::JavaParamRef& java_window, + const base::android::JavaParamRef& j_export_path); + void SetBookmarkTitle(JNIEnv* env, const base::android::JavaParamRef& obj, jlong id, @@ -315,12 +335,15 @@ class BookmarkBridge : public bookmarks::BaseBookmarkModelObserver, void DestroyJavaObject(); Profile* profile_; + base::FilePath export_path_; + BookmarksExportObserver* observer_; // weak JavaObjectWeakGlobalRef weak_java_ref_; bookmarks::BookmarkModel* bookmark_model_; // weak bookmarks::ManagedBookmarkService* managed_bookmark_service_; // weak std::unique_ptr grouped_bookmark_actions_; PrefChangeRegistrar pref_change_registrar_; + scoped_refptr select_file_dialog_; // Information about the Partner bookmarks (must check for IsLoaded()). // This is owned by profile. @@ -332,6 +355,9 @@ class BookmarkBridge : public bookmarks::BaseBookmarkModelObserver, // Observes the profile destruction and creation. base::ScopedObservation profile_observation_{this}; + const std::string FileSelectedImpl(const base::FilePath& path); + void FileSelectedImplOnUIThread(const base::FilePath& path, + const std::string& contents); DISALLOW_COPY_AND_ASSIGN(BookmarkBridge); }; diff --git a/chrome/browser/bookmarks/bookmark_html_writer.cc b/chrome/browser/bookmarks/bookmark_html_writer.cc --- a/chrome/browser/bookmarks/bookmark_html_writer.cc +++ b/chrome/browser/bookmarks/bookmark_html_writer.cc @@ -27,6 +27,7 @@ #include "base/task/thread_pool.h" #include "base/time/time.h" #include "base/values.h" +#include "base/android/content_uri_utils.h" #include "chrome/browser/bookmarks/bookmark_model_factory.h" #include "chrome/browser/favicon/favicon_service_factory.h" #include "chrome/browser/profiles/profile.h" @@ -230,7 +231,12 @@ class Writer : public base::RefCountedThreadSafe { // Opens the file, returning true on success. bool OpenFile() { int flags = base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE; - file_ = std::make_unique(path_, flags); + if (path_.IsContentUri()) { + file_ = std::make_unique(base::OpenContentUriForWrite(path_)); + } else { + file_ = std::make_unique(path_, flags); + } + if (!file_->IsValid()) { PLOG(ERROR) << "Could not create " << path_; return false; diff --git a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationCustomView.java b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationCustomView.java --- a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationCustomView.java +++ b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationCustomView.java @@ -46,7 +46,7 @@ public class DownloadLocationCustomView private TextView mFileSize; private Spinner mFileLocation; private TextView mLocationAvailableSpace; - private CheckBox mDontShowAgain; + public CheckBox mDontShowAgain; private @DownloadLocationDialogType int mDialogType; private long mTotalBytes; @@ -68,7 +68,7 @@ public class DownloadLocationCustomView mDontShowAgain = findViewById(R.id.show_again_checkbox); } - void initialize(@DownloadLocationDialogType int dialogType, long totalBytes) { + public void initialize(@DownloadLocationDialogType int dialogType, long totalBytes) { // TODO(xingliu): Remove this function, currently used by smart suggestion. mDialogType = dialogType; mTotalBytes = totalBytes; @@ -117,7 +117,7 @@ public class DownloadLocationCustomView * @return The text that the user inputted as the name of the file. */ @Nullable - String getFileName() { + public String getFileName() { if (mFileName == null || mFileName.getText() == null) return null; return mFileName.getText().toString(); } @@ -126,7 +126,7 @@ public class DownloadLocationCustomView * @return The file path based on what the user selected as the location of the file. */ @Nullable - DirectoryOption getDirectoryOption() { + public DirectoryOption getDirectoryOption() { if (mFileLocation == null) return null; DirectoryOption selected = (DirectoryOption) mFileLocation.getSelectedItem(); return selected; diff --git a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogCoordinator.java b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogCoordinator.java --- a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogCoordinator.java +++ b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogCoordinator.java @@ -36,12 +36,12 @@ import java.util.ArrayList; public class DownloadLocationDialogCoordinator implements ModalDialogProperties.Controller { @NonNull private DownloadLocationDialogController mController; - private PropertyModel mDialogModel; + protected PropertyModel mDialogModel; private PropertyModel mDownloadLocationDialogModel; private PropertyModelChangeProcessor mPropertyModelChangeProcessor; - private DownloadLocationCustomView mCustomView; - private ModalDialogManager mModalDialogManager; + protected DownloadLocationCustomView mCustomView; + protected ModalDialogManager mModalDialogManager; private long mTotalBytes; private @DownloadLocationDialogType int mDialogType; private String mSuggestedPath; @@ -127,7 +127,7 @@ public class DownloadLocationDialogCoordinator implements ModalDialogProperties. * Called after retrieved the download directory options. * @param dirs An list of available download directories. */ - private void onDirectoryOptionsRetrieved(ArrayList dirs) { + protected void onDirectoryOptionsRetrieved(ArrayList dirs) { // Already showing the dialog. if (mDialogModel != null) return; diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc --- a/chrome/browser/flag_descriptions.cc +++ b/chrome/browser/flag_descriptions.cc @@ -5306,6 +5306,11 @@ const char kWebKioskEnableLacrosDescription[] = "Chrome OS. When disabled, the Ash-chrome will be used"; #endif // BUILDFLAG(IS_CHROMEOS_ASH) +const char kBookmarksExportUseSafName[] = "Use saf for bookmarks export"; +const char kBookmarksExportUseSafDescription[] = + "When enabled user can choose where save the exported bookmarks " + "file."; + // ============================================================================ // Don't just add flags to the end, put them in the right section in // alphabetical order just like the header file. diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h --- a/chrome/browser/flag_descriptions.h +++ b/chrome/browser/flag_descriptions.h @@ -3091,6 +3091,9 @@ extern const char kWebKioskEnableLacrosName[]; extern const char kWebKioskEnableLacrosDescription[]; #endif // BUILDFLAG(IS_CHROMEOS_ASH) +extern const char kBookmarksExportUseSafName[]; +extern const char kBookmarksExportUseSafDescription[]; + // ============================================================================ // Don't just add flags to the end, put them in the right section in // alphabetical order. See top instructions for more. diff --git a/chrome/browser/flags/android/chrome_feature_list.cc b/chrome/browser/flags/android/chrome_feature_list.cc --- a/chrome/browser/flags/android/chrome_feature_list.cc +++ b/chrome/browser/flags/android/chrome_feature_list.cc @@ -164,6 +164,7 @@ const base::Feature* const kFeaturesExposedToJava[] = { &kBackgroundThreadPool, &kBentoOffline, &kBookmarkBottomSheet, + &kBookmarksExportUseSaf, &kCastDeviceFilter, &kCloseTabSuggestions, &kCriticalPersistedTabData, @@ -810,6 +811,10 @@ const base::Feature kWebApkInstallCompleteNotification{ const base::Feature kWebApkTrampolineOnInitialIntent{ "WebApkTrampolineOnInitialIntent", base::FEATURE_ENABLED_BY_DEFAULT}; +// disabled by default because of an issue on Android 6.0 +const base::Feature kBookmarksExportUseSaf{"BookmarksExportUseSaf", + base::FEATURE_DISABLED_BY_DEFAULT}; + static jboolean JNI_ChromeFeatureList_IsEnabled( JNIEnv* env, const JavaParamRef& jfeature_name) { diff --git a/chrome/browser/flags/android/chrome_feature_list.h b/chrome/browser/flags/android/chrome_feature_list.h --- a/chrome/browser/flags/android/chrome_feature_list.h +++ b/chrome/browser/flags/android/chrome_feature_list.h @@ -146,6 +146,7 @@ extern const base::Feature kToolbarMicIphAndroid; extern const base::Feature kToolbarUseHardwareBitmapDraw; extern const base::Feature kTrustedWebActivityLocationDelegation; extern const base::Feature kTrustedWebActivityNewDisclosure; +extern const base::Feature kBookmarksExportUseSaf; extern const base::Feature kTrustedWebActivityPostMessage; extern const base::Feature kTrustedWebActivityQualityEnforcement; extern const base::Feature kTrustedWebActivityQualityEnforcementForced; diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java --- a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java +++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java @@ -560,6 +560,7 @@ public abstract class ChromeFeatureList { "WebApkTrampolineOnInitialIntent"; public static final String XSURFACE_METRICS_REPORTING = "XsurfaceMetricsReporting"; public static final String WEB_OTP_CROSS_DEVICE_SIMPLE_STRING = "WebOtpCrossDeviceSimpleString"; + public static final String BOOKMARKS_EXPORT_USESAF = "BookmarksExportUseSaf"; @NativeMethods interface Natives { diff --git a/chrome/browser/importer/profile_writer.cc b/chrome/browser/importer/profile_writer.cc --- a/chrome/browser/importer/profile_writer.cc +++ b/chrome/browser/importer/profile_writer.cc @@ -105,12 +105,14 @@ void ProfileWriter::AddHistoryPage(const history::URLRows& page, HistoryServiceFactory::GetForProfile(profile_, ServiceAccessType::EXPLICIT_ACCESS) ->AddPagesWithDetails(page, visit_source); +#if !defined(OS_ANDROID) // Measure the size of the history page after Auto Import on first run. if (first_run::IsChromeFirstRun() && visit_source == history::SOURCE_IE_IMPORTED) { UMA_HISTOGRAM_COUNTS_1M("Import.ImportedHistorySize.AutoImportFromIE", page.size()); } +#endif } void ProfileWriter::AddHomepage(const GURL& home_page) { @@ -131,6 +133,16 @@ void ProfileWriter::AddBookmarks( return; BookmarkModel* model = BookmarkModelFactory::GetForBrowserContext(profile_); + AddBookmarksWithModel(model, bookmarks, top_level_folder_name); +} + +void ProfileWriter::AddBookmarksWithModel( + BookmarkModel* model, + const std::vector& bookmarks, + const std::u16string& top_level_folder_name) { + if (bookmarks.empty()) + return; + DCHECK(model->loaded()); // If the bookmark bar is currently empty, we should import directly to it. diff --git a/chrome/browser/importer/profile_writer.h b/chrome/browser/importer/profile_writer.h --- a/chrome/browser/importer/profile_writer.h +++ b/chrome/browser/importer/profile_writer.h @@ -11,6 +11,7 @@ #include "base/macros.h" #include "base/memory/ref_counted.h" #include "build/build_config.h" +#include "components/bookmarks/browser/bookmark_model.h" #include "components/favicon_base/favicon_usage_data.h" #include "components/history/core/browser/history_types.h" #include "components/search_engines/template_url_service.h" @@ -68,6 +69,11 @@ class ProfileWriter : public base::RefCountedThreadSafe { virtual void AddBookmarks(const std::vector& bookmarks, const std::u16string& top_level_folder_name); + virtual void AddBookmarksWithModel( + bookmarks::BookmarkModel* model, + const std::vector& bookmarks, + const std::u16string& top_level_folder_name); + virtual void AddFavicons(const favicon_base::FaviconUsageDataList& favicons); // Adds the TemplateURLs in |template_urls| to the local store. diff --git a/chrome/browser/preferences/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceKeys.java b/chrome/browser/preferences/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceKeys.java --- a/chrome/browser/preferences/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceKeys.java +++ b/chrome/browser/preferences/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceKeys.java @@ -124,6 +124,8 @@ public final class ChromePreferenceKeys { public static final String BOOKMARKS_LAST_USED_URL = "enhanced_bookmark_last_used_url"; public static final String BOOKMARKS_LAST_USED_PARENT = "enhanced_bookmark_last_used_parent_folder"; + public static final String BOOKMARKS_LAST_EXPORT_URI = + "Chrome.Bookmarks.Last_Export_Uri"; /** * Whether Chrome is set as the default browser. @@ -1042,6 +1044,7 @@ public final class ChromePreferenceKeys { AUTOFILL_ASSISTANT_PROACTIVE_HELP, APP_LAUNCH_LAST_KNOWN_ACTIVE_TAB_STATE, APP_LAUNCH_SEARCH_ENGINE_HAD_LOGO, + BOOKMARKS_LAST_EXPORT_URI, APPLICATION_OVERRIDE_LANGUAGE, CHROME_SURVEY_DOWNLOAD_ATTEMPTS.pattern(), CHROME_SURVEY_PROMPT_DISPLAYED_TIMESTAMP.pattern(), diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd --- a/chrome/browser/ui/android/strings/android_chrome_strings.grd +++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd @@ -242,6 +242,24 @@ CHAR_LIMIT guidelines: Sites + + Import + + + Export + + + Export bookmarks to file + + + Do you want to overwrite %s? + + + Yes + + + Choose another file + Virtual Reality diff --git a/chrome/common/BUILD.gn b/chrome/common/BUILD.gn --- a/chrome/common/BUILD.gn +++ b/chrome/common/BUILD.gn @@ -416,6 +416,9 @@ static_library("common") { sources += [ "media/chrome_media_drm_bridge_client.cc", "media/chrome_media_drm_bridge_client.h", + ## Bromite dependencies for bookmark import functionality + "importer/imported_bookmark_entry.cc", + "importer/imported_bookmark_entry.h", ] } else { # Non-Android. diff --git a/chrome/utility/BUILD.gn b/chrome/utility/BUILD.gn --- a/chrome/utility/BUILD.gn +++ b/chrome/utility/BUILD.gn @@ -78,8 +78,6 @@ static_library("utility") { if (!is_android) { sources += [ - "importer/bookmark_html_reader.cc", - "importer/bookmark_html_reader.h", "importer/bookmarks_file_importer.cc", "importer/bookmarks_file_importer.h", "importer/external_process_importer_bridge.cc", @@ -184,6 +182,11 @@ static_library("utility") { } } + sources += [ + "importer/bookmark_html_reader.cc", + "importer/bookmark_html_reader.h", + ] + if (use_nss_certs) { sources += [ "importer/nss_decryptor_system_nss.cc", diff --git a/chrome/utility/importer/bookmark_html_reader.cc b/chrome/utility/importer/bookmark_html_reader.cc --- a/chrome/utility/importer/bookmark_html_reader.cc +++ b/chrome/utility/importer/bookmark_html_reader.cc @@ -17,7 +17,9 @@ #include "base/strings/utf_string_conversions.h" #include "base/time/time.h" #include "chrome/common/importer/imported_bookmark_entry.h" +#if !defined(OS_ANDROID) #include "chrome/utility/importer/favicon_reencode.h" +#endif #include "components/search_engines/search_terms_data.h" #include "components/search_engines/template_url.h" #include "net/base/data_url.h" @@ -56,6 +58,7 @@ bool GetAttribute(const std::string& attribute_list, return true; } +#if !defined(OS_ANDROID) // Given the URL of a page and a favicon data URL, adds an appropriate record // to the given favicon usage vector. void DataURLToFaviconUsage(const GURL& link_url, @@ -86,6 +89,7 @@ void DataURLToFaviconUsage(const GURL& link_url, favicons->push_back(usage); } +#endif } // namespace @@ -106,14 +110,28 @@ static std::string stripDt(const std::string& lineDt) { } void ImportBookmarksFile( - base::RepeatingCallback cancellation_callback, - base::RepeatingCallback valid_url_callback, + const base::RepeatingCallback cancellation_callback, + const base::RepeatingCallback valid_url_callback, const base::FilePath& file_path, std::vector* bookmarks, std::vector* search_engines, favicon_base::FaviconUsageDataList* favicons) { std::string content; - base::ReadFileToString(file_path, &content); + if (!base::ReadFileToString(file_path, &content)) { + LOG(ERROR) << "Could not directly read bookmarks import file"; + return; + } + + ImportBookmarksFile(cancellation_callback, valid_url_callback, content, bookmarks, search_engines, favicons); +} + +void ImportBookmarksFile( + base::RepeatingCallback cancellation_callback, + base::RepeatingCallback valid_url_callback, + const std::string& content, + std::vector* bookmarks, + std::vector* search_engines, + favicon_base::FaviconUsageDataList* favicons) { std::vector lines = base::SplitString( content, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); @@ -126,6 +144,7 @@ void ImportBookmarksFile( std::vector path; size_t toolbar_folder_index = 0; std::string charset = "UTF-8"; // If no charset is specified, assume utf-8. + for (size_t i = 0; i < lines.size() && (cancellation_callback.is_null() || !cancellation_callback.Run()); @@ -218,10 +237,12 @@ void ImportBookmarksFile( } bookmarks->push_back(entry); +#if !defined(OS_ANDROID) // Save the favicon. DataURLToFaviconUsage will handle the case where // there is no favicon. if (favicons) DataURLToFaviconUsage(url, favicon, favicons); +#endif continue; } diff --git a/chrome/utility/importer/bookmark_html_reader.h b/chrome/utility/importer/bookmark_html_reader.h --- a/chrome/utility/importer/bookmark_html_reader.h +++ b/chrome/utility/importer/bookmark_html_reader.h @@ -50,6 +50,14 @@ void ImportBookmarksFile( std::vector* search_engines, favicon_base::FaviconUsageDataList* favicons); +void ImportBookmarksFile( + const base::RepeatingCallback cancellation_callback, + const base::RepeatingCallback valid_url_callback, + const std::string& content, + std::vector* bookmarks, + std::vector* search_engines, + favicon_base::FaviconUsageDataList* favicons); + // Returns true if |url| should be imported as a search engine, i.e. because it // has replacement terms. Chrome treats such bookmarks as search engines rather // than true bookmarks. diff --git a/ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java b/ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java --- a/ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java +++ b/ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java @@ -38,6 +38,7 @@ import org.chromium.base.task.AsyncTask; import org.chromium.base.task.PostTask; import org.chromium.base.task.TaskTraits; import org.chromium.ui.R; +import org.chromium.ui.widget.Toast; import org.chromium.ui.UiUtils; import java.io.File; @@ -56,6 +57,7 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick private static final String TAG = "SelectFileDialog"; private static final String IMAGE_TYPE = "image"; private static final String VIDEO_TYPE = "video"; + private static final String HTML_TYPE = "html"; private static final String AUDIO_TYPE = "audio"; private static final String ALL_TYPES = "*/*"; @@ -239,6 +241,11 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick mFileTypes = fileTypes; } + @CalledByNative + private void showToast(String message) { + Toast.makeText(ContextUtils.getApplicationContext(), message, Toast.LENGTH_LONG).show(); + } + /** * Creates and starts an intent based on the passed fileTypes and capture value. * @param fileTypes MIME types requested (i.e. "image/*") @@ -266,7 +273,7 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick List missingPermissions = new ArrayList<>(); String storagePermission = Manifest.permission.READ_EXTERNAL_STORAGE; boolean shouldUsePhotoPicker = shouldUsePhotoPicker(); - if (shouldUsePhotoPicker) { + if (shouldUsePhotoPicker || shouldShowHtmlTypes()) { if (!window.hasPermission(storagePermission)) missingPermissions.add(storagePermission); } else { if (((mSupportsImageCapture && shouldShowImageTypes()) @@ -294,7 +301,7 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick } // TODO(finnur): Remove once we figure out the cause of crbug.com/950024. - if (shouldUsePhotoPicker) { + if (shouldUsePhotoPicker || shouldShowHtmlTypes()) { if (permissions.length != requestPermissions.length) { throw new RuntimeException( String.format("Permissions arrays misaligned: %d != %d", @@ -308,7 +315,7 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick } } - if (shouldUsePhotoPicker && permissions[i].equals(storagePermission)) { + if ((shouldUsePhotoPicker || shouldShowHtmlTypes()) && permissions[i].equals(storagePermission)) { onFileNotSelected(); return; } @@ -504,6 +511,7 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick } if (!mimeTypes.contains(mimeType)) mimeTypes.add(mimeType); } + if (mimeTypes.size() == 0) return null; return mimeTypes; } @@ -812,6 +820,10 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick return countAcceptTypesFor(superType) == mFileTypes.size(); } + private boolean shouldShowHtmlTypes() { + return countAcceptTypesFor(HTML_TYPE) > 0; + } + /** * Checks whether the list of accepted types effectively describes only a single * type, which might be wildcard. For example: diff --git a/ui/android/java/strings/android_ui_strings.grd b/ui/android/java/strings/android_ui_strings.grd --- a/ui/android/java/strings/android_ui_strings.grd +++ b/ui/android/java/strings/android_ui_strings.grd @@ -174,6 +174,9 @@ Failed to open selected file + + Failed to save selected file + diff --git a/ui/shell_dialogs/select_file_dialog.h b/ui/shell_dialogs/select_file_dialog.h --- a/ui/shell_dialogs/select_file_dialog.h +++ b/ui/shell_dialogs/select_file_dialog.h @@ -202,6 +202,8 @@ class SHELL_DIALOGS_EXPORT SelectFileDialog void* params); bool HasMultipleFileTypeChoices(); + virtual void ShowToast(const std::string& message) = 0; + protected: friend class base::RefCountedThreadSafe; diff --git a/ui/shell_dialogs/select_file_dialog_android.cc b/ui/shell_dialogs/select_file_dialog_android.cc --- a/ui/shell_dialogs/select_file_dialog_android.cc +++ b/ui/shell_dialogs/select_file_dialog_android.cc @@ -140,6 +140,12 @@ void SelectFileDialogImpl::SelectFileImpl( owning_window->GetJavaObject()); } +void SelectFileDialogImpl::ShowToast(const std::string& message) { + JNIEnv* env = base::android::AttachCurrentThread(); + + Java_SelectFileDialog_showToast(env, java_object_, base::android::ConvertUTF8ToJavaString(env, message)); +} + SelectFileDialogImpl::~SelectFileDialogImpl() { } diff --git a/ui/shell_dialogs/select_file_dialog_android.h b/ui/shell_dialogs/select_file_dialog_android.h --- a/ui/shell_dialogs/select_file_dialog_android.h +++ b/ui/shell_dialogs/select_file_dialog_android.h @@ -55,6 +55,8 @@ class SelectFileDialogImpl : public SelectFileDialog { gfx::NativeWindow owning_window, void* params) override; + void ShowToast(const std::string& message) override; + protected: ~SelectFileDialogImpl() override; -- 2.17.1