// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/cros_components/switch/switch.js';
import '../../background/js/file_manager_base.js';
import '../../background/js/test_util.js';
import '../../widgets/xf_jellybean.js';
import type {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {ColorChangeUpdater} from 'chrome://resources/cr_components/color_change_listener/colors_css_updater.js';
import {assert, assertInstanceof} from 'chrome://resources/js/assert.js';
import type {Crostini} from '../../background/js/crostini.js';
import type {FileManagerBase} from '../../background/js/file_manager_base.js';
import type {ProgressCenter} from '../../background/js/progress_center.js';
import {getBulkPinProgress, getDialogCaller, getDlpBlockedComponents, getDriveConnectionState, getMaterializedViews, getPreferences} from '../../common/js/api.js';
import type {ArrayDataModel} from '../../common/js/array_data_model.js';
import {crInjectTypeAndInit} from '../../common/js/cr_ui.js';
import {isFolderDialogType} from '../../common/js/dialog_type.js';
import {getKeyModifiers, queryDecoratedElement, queryRequiredElement} from '../../common/js/dom_utils.js';
import type {FakeEntry, FilesAppDirEntry, FilesAppEntry} from '../../common/js/files_app_entry_types.js';
import {EntryList, FakeEntryImpl} from '../../common/js/files_app_entry_types.js';
import type {FilesAppState} from '../../common/js/files_app_state.js';
import {FilteredVolumeManager} from '../../common/js/filtered_volume_manager.js';
import {isDlpEnabled, isGuestOsEnabled, isMaterializedViewsEnabled, isSkyvaultV2Enabled} from '../../common/js/flags.js';
import {recordEnum, recordInterval, startInterval} from '../../common/js/metrics.js';
import {ProgressItemState} from '../../common/js/progress_center_common.js';
import {str} from '../../common/js/translations.js';
import {TrashRootEntry} from '../../common/js/trash.js';
import {getLastVisitedURL, isInGuestMode, runningInBrowser} from '../../common/js/util.js';
import type {VolumeType} from '../../common/js/volume_manager_types.js';
import {AllowedPaths, ARCHIVE_OPENED_EVENT_TYPE, RootType} from '../../common/js/volume_manager_types.js';
import {DirectoryTreeContainer} from '../../containers/directory_tree_container.js';
import {NudgeType} from '../../containers/nudge_container.js';
import {getMyFiles} from '../../state/ducks/all_entries.js';
import {updateBulkPinProgress} from '../../state/ducks/bulk_pinning.js';
import {updateDeviceConnectionState} from '../../state/ducks/device.js';
import {updateDriveConnectionStatus} from '../../state/ducks/drive.js';
import {setLaunchParameters} from '../../state/ducks/launch_params.js';
import {updateMaterializedViews} from '../../state/ducks/materialized_views.js';
import {updatePreferences} from '../../state/ducks/preferences.js';
import {getDefaultSearchOptions, updateSearch} from '../../state/ducks/search.js';
import {addUiEntry, removeUiEntry} from '../../state/ducks/ui_entries.js';
import {driveRootEntryListKey, trashRootKey} from '../../state/ducks/volumes.js';
import {DialogType, SearchLocation} from '../../state/state.js';
import {getEmptyState, getEntry, getStore, getVolume} from '../../state/store.js';
import {ActionsController} from './actions_controller.js';
import {AndroidAppListModel} from './android_app_list_model.js';
import {AppStateController} from './app_state_controller.js';
import {BannerController} from './banner_controller.js';
import {CommandHandler} from './command_handler.js';
import {findQueryMatchedDirectoryEntry} from './crossover_search_utils.js';
import {CrostiniController} from './crostini_controller.js';
import {DialogActionController} from './dialog_action_controller.js';
import {FileFilter} from './directory_contents.js';
import {DirectoryModel} from './directory_model.js';
import {DirectoryTreeNamingController} from './directory_tree_naming_controller.js';
import {importElements} from './elements_importer.js';
import {EmptyFolderController} from './empty_folder_controller.js';
import {forceDefaultHandler} from './file_manager_commands_util.js';
import type {FileSelection} from './file_selection.js';
import {FileSelectionHandler} from './file_selection.js';
import {FileTasks} from './file_tasks.js';
import {FileTransferController} from './file_transfer_controller.js';
import {FileTypeFiltersController} from './file_type_filters_controller.js';
import {FolderShortcutsDataModel} from './folder_shortcuts_data_model.js';
import {GearMenuController} from './gear_menu_controller.js';
import {GuestOsController} from './guest_os_controller.js';
import {LastModifiedController} from './last_modified_controller.js';
import {LaunchParam} from './launch_param.js';
import {ListThumbnailLoader} from './list_thumbnail_loader.js';
import {MainWindowComponent} from './main_window_component.js';
import {MetadataModel} from './metadata/metadata_model.js';
import {ThumbnailModel} from './metadata/thumbnail_model.js';
import {MetadataBoxController} from './metadata_box_controller.js';
import {MetadataUpdateController} from './metadata_update_controller.js';
import {NamingController} from './naming_controller.js';
import {NavigationUma} from './navigation_uma.js';
import {OneDriveController} from './one_drive_controller.js';
import {ProvidersModel} from './providers_model.js';
import {QuickViewController} from './quick_view_controller.js';
import {QuickViewModel} from './quick_view_model.js';
import {QuickViewUma} from './quick_view_uma.js';
import {ScanController} from './scan_controller.js';
import {SelectionMenuController} from './selection_menu_controller.js';
import {SortMenuController} from './sort_menu_controller.js';
import {SpinnerController} from './spinner_controller.js';
import {TaskController} from './task_controller.js';
import {ToolbarController} from './toolbar_controller.js';
import {CommandButton} from './ui/commandbutton.js';
import {contextMenuHandler} from './ui/context_menu_handler.js';
import {FileGrid} from './ui/file_grid.js';
import {FileManagerUI} from './ui/file_manager_ui.js';
import {FileMetadataFormatter} from './ui/file_metadata_formatter.js';
import {FileTable} from './ui/file_table.js';
import type {List} from './ui/list.js';
import {Menu} from './ui/menu.js';
/**
* FileManager constructor.
*
* FileManager objects encapsulate the functionality of the file selector
* dialogs, as well as the full screen file manager application.
*
*/
export class FileManager {
// ------------------------------------------------------------------------
// Services FileManager depends on.
/**
* Volume manager.
*/
private volumeManager_: FilteredVolumeManager|null = null;
private crostini_: Crostini|null = null;
private crostiniController_: CrostiniController|null = null;
private guestOsController_: GuestOsController|null = null;
private metadataModel_: MetadataModel|null = null;
private fileMetadataFormatter_ = new FileMetadataFormatter();
private thumbnailModel_: ThumbnailModel|null = null;
/**
* File filter.
*/
private fileFilter_: null|FileFilter = null;
/**
* Model of current directory.
*/
private directoryModel_: null|DirectoryModel = null;
/**
* Model of folder shortcuts.
*/
private folderShortcutsModel_: null|FolderShortcutsDataModel = null;
/**
* Model of Android apps.
*/
private androidAppListModel_: null|AndroidAppListModel = null;
/**
* Model for providers (providing extensions).
*/
private providersModel_: null|ProvidersModel = null;
/**
* Model for quick view.
*/
private quickViewModel_: null|QuickViewModel = null;
/**
* Controller for actions for current selection.
*/
private actionsController_: ActionsController|null = null;
/**
* Handler for command events.
*/
private commandHandler_: CommandHandler|null = null;
/**
* Handler for the change of file selection.
*/
private selectionHandler_: null|FileSelectionHandler = null;
/**
* UI management class of file manager.
*/
private ui_: null|FileManagerUI = null;
// ------------------------------------------------------------------------
// Parameters determining the type of file manager.
/**
* Dialog type of this window.
*/
dialogType: DialogType = DialogType.FULL_PAGE;
/**
* Startup parameters for this application.
*/
private launchParams_: null|LaunchParam = null;
// ------------------------------------------------------------------------
// Controllers.
/**
* File transfer controller.
*/
private fileTransferController_: null|FileTransferController = null;
/**
* Naming controller.
*/
private namingController_: null|NamingController = null;
/**
* Directory tree naming controller.
*/
private directoryTreeNamingController_: DirectoryTreeNamingController|null =
null;
/**
* Controller for directory scan.
*/
protected scanController_: null|ScanController = null;
/**
* Controller for spinner.
*/
private spinnerController_: null|SpinnerController = null;
/**
* Sort menu controller.
*/
protected sortMenuController_: null|SortMenuController = null;
/**
* Gear menu controller.
*/
protected gearMenuController_: null|GearMenuController = null;
/**
* Controller for the context menu opened by the action bar button in the
* check-select mode.
*/
protected selectionMenuController_: null|SelectionMenuController = null;
/**
* Toolbar controller.
*/
private toolbarController_: null|ToolbarController = null;
/**
* App state controller.
*/
private appStateController_: null|AppStateController = null;
/**
* Dialog action controller.
*/
protected dialogActionController_: null|DialogActionController = null;
/**
* List update controller.
*/
private metadataUpdateController_: null|MetadataUpdateController = null;
/**
* Last modified controller.
*/
protected lastModifiedController_: LastModifiedController|null = null;
/**
* OneDrive controller.
*/
protected oneDriveController_: OneDriveController|null = null;
/**
* Component for main window and its misc UI parts.
*/
protected mainWindowComponent_: null|MainWindowComponent = null;
private taskController_: TaskController|null = null;
private quickViewUma_: QuickViewUma|null = null;
protected quickViewController_: QuickViewController|null = null;
protected fileTypeFiltersController_: FileTypeFiltersController|null = null;
/**
* Empty folder controller.
*/
protected emptyFolderController_: null|EmptyFolderController = null;
/**
* Records histograms of directory-changed event.
*/
private navigationUma_: null|NavigationUma = null;
// ------------------------------------------------------------------------
// DOM elements.
/**
*/
private fileBrowserBackground_: null|FileManagerBase = null;
/**
* The root DOM element of this app.
*/
private dialogDom_: null|HTMLElement = null;
/**
* The document object of this app.
*/
private document_: null|Document = null;
// ------------------------------------------------------------------------
// Miscellaneous FileManager's states.
/**
* Promise object which is fulfilled when initialization for app state
* controller is done.
*/
private initSettingsPromise_: null|Promise<void> = null;
/**
* Promise object which is fulfilled when initialization related to the
* background page is done.
*/
private initBackgroundPagePromise_: null|Promise<void> = null;
/**
* Whether Drive is enabled. Retrieved from user preferences.
*/
private driveEnabled_: boolean = false;
/**
* Whether Drive bulk-pinning is available on this device. Retrieved from
* user preferences.
*/
private bulkPinningAvailable_: boolean = false;
/**
* Whether Drive bulk-pinning has been initialized in Files App.
*/
private bulkPinningInitialized_: boolean = false;
/**
* Whether Trash is enabled or not, retrieved from user preferences.
*/
trashEnabled: boolean = false;
/**
* A fake entry for Recents.
*/
private recentEntry_: null|FakeEntry = null;
/**
* Whether or not we are running in guest mode.
*/
private guestMode_: boolean = false;
private store_ = getStore();
/**
* Whether local user files (e.g. My Files, Downloads, Play files...) are
* enabled or not, retrieved from user preferences.
*/
localUserFilesAllowed: boolean = true;
constructor() {
(function() {
ColorChangeUpdater.forDocument().start();
})();
}
get progressCenter(): ProgressCenter {
assert(this.fileBrowserBackground_);
assert(this.fileBrowserBackground_.progressCenter);
return this.fileBrowserBackground_.progressCenter;
}
get directoryModel(): DirectoryModel {
return this.directoryModel_!;
}
get directoryTreeNamingController(): DirectoryTreeNamingController {
assert(this.directoryTreeNamingController_);
return this.directoryTreeNamingController_;
}
get fileFilter(): FileFilter {
assert(this.fileFilter_);
return this.fileFilter_;
}
get folderShortcutsModel(): FolderShortcutsDataModel {
assert(this.folderShortcutsModel_);
return this.folderShortcutsModel_;
}
get actionsController(): ActionsController {
assert(this.actionsController_);
return this.actionsController_;
}
get commandHandler(): CommandHandler {
assert(this.commandHandler_);
return this.commandHandler_;
}
get providersModel(): ProvidersModel {
assert(this.providersModel_);
return this.providersModel_;
}
get metadataModel(): MetadataModel {
assert(this.metadataModel_);
return this.metadataModel_;
}
get selectionHandler(): FileSelectionHandler {
assert(this.selectionHandler_);
return this.selectionHandler_;
}
get document(): Document {
assert(this.document_);
return this.document_;
}
get fileTransferController(): FileTransferController|null {
return this.fileTransferController_;
}
get namingController(): NamingController {
assert(this.namingController_);
return this.namingController_;
}
get taskController(): TaskController {
assert(this.taskController_);
return this.taskController_;
}
get spinnerController(): SpinnerController {
assert(this.spinnerController_);
return this.spinnerController_;
}
get volumeManager(): FilteredVolumeManager {
assert(this.volumeManager_);
return this.volumeManager_;
}
get crostini(): Crostini {
assert(this.crostini_);
return this.crostini_;
}
get ui(): FileManagerUI {
assert(this.ui_);
return this.ui_;
}
/**
* @return If the app is running in the guest mode.
*/
get guestMode(): boolean {
return this.guestMode_;
}
/**
* Launch a new File Manager app.
* @param appState App state.
*/
launchFileManager(appState?: FilesAppState) {
assert(this.fileBrowserBackground_);
this.fileBrowserBackground_.launchFileManager(appState);
}
/**
* Returns the last URL visited with visitURL() (e.g. for "Manage in Drive").
* Used by the integration tests.
*/
getLastVisitedUrl(): string {
return getLastVisitedURL();
}
/**
* Returns a string translation from its translation ID.
* @param id The id of the translated string.
*/
getTranslatedString(id: string): string {
return str(id);
}
/**
* One time initialization for app state controller to load view option from
* local storage.
*/
private async startInitSettings_(): Promise<void> {
startInterval('Load.InitSettings');
this.appStateController_ = new AppStateController(this.dialogType);
await this.appStateController_.loadInitialViewOptions();
recordInterval('Load.InitSettings');
}
/**
* Updates guestMode_ field based on what the result of the isInGuestMode
* helper function. It errs on the side of not-in-guestmode, if the util
* function fails. The worse this causes are extra notifications.
*/
private async setGuestMode_() {
try {
const guest = await isInGuestMode();
if (guest !== null) {
this.guestMode_ = guest;
}
} catch (error) {
console.warn(error);
// Leave this.guestMode_ as its initial value.
}
}
/**
* One time initialization for the file system and related things.
*/
private async initFileSystemUi_(): Promise<void> {
this.ui.listContainer.startBatchUpdates();
const fileListPromise = this.initFileList_();
const currentDirectoryPromise = this.setupCurrentDirectory_();
let listBeingUpdated: List|null = null;
this.directoryModel.addEventListener('begin-update-files', () => {
this.ui.listContainer.currentList.startBatchUpdates();
// Remember the list which was used when updating files started, so
// endBatchUpdates() is called on the same list.
listBeingUpdated = this.ui.listContainer.currentList;
});
this.directoryModel.addEventListener('end-update-files', () => {
this.namingController.restoreItemBeingRenamed();
listBeingUpdated?.endBatchUpdates();
listBeingUpdated = null;
});
this.volumeManager.addEventListener(ARCHIVE_OPENED_EVENT_TYPE, event => {
assert(event.detail.mountPoint);
if (window.isFocused?.()) {
this.directoryModel.changeDirectoryEntry(event.detail.mountPoint);
}
});
this.directoryModel.addEventListener('directory-changed', event => {
this.navigationUma_!.onDirectoryChanged(event.detail.newDirEntry);
});
this.initCommands_();
assert(this.directoryModel_);
assert(this.spinnerController_);
assert(this.commandHandler_);
assert(this.selectionHandler_);
assert(this.launchParams_);
assert(this.volumeManager_);
assert(this.dialogDom_);
assert(this.fileBrowserBackground_);
assert(this.metadataModel_);
assert(this.providersModel_);
assert(this.folderShortcutsModel_);
assert(this.ui_);
assert(this.taskController_);
this.fileBrowserBackground_.driveSyncHandler.metadataModel =
this.metadataModel_;
this.scanController_ = new ScanController(
this.directoryModel_, this.ui.listContainer, this.spinnerController_,
this.selectionHandler_);
this.sortMenuController_ = new SortMenuController(
this.ui.sortButton, this.directoryModel_.getFileList());
this.gearMenuController_ = new GearMenuController(
this.ui.gearButton, this.ui.gearMenu, this.ui.providersMenu,
this.directoryModel_, this.providersModel_);
this.selectionMenuController_ = new SelectionMenuController(
this.ui.selectionMenuButton,
queryDecoratedElement('#file-context-menu', Menu));
this.toolbarController_ = new ToolbarController(
this.ui.toolbar, this.ui.dialogNavigationList, this.ui.listContainer,
this.selectionHandler_, this.directoryModel_, this.volumeManager_,
this.ui_);
this.actionsController_ = new ActionsController(
this.volumeManager_, this.metadataModel_, this.folderShortcutsModel_,
this.selectionHandler_, this.ui_);
this.lastModifiedController_ = new LastModifiedController(
this.ui_.listContainer.table, this.directoryModel_);
this.quickViewModel_ = new QuickViewModel();
const fileListSelectionModel = this.directoryModel_.getFileListSelection();
this.quickViewUma_ = new QuickViewUma(this.volumeManager_, this.dialogType);
const metadataBoxController = new MetadataBoxController(
this.metadataModel_, this.quickViewModel_, this.fileMetadataFormatter_,
this.volumeManager_);
this.quickViewController_ = new QuickViewController(
this, this.metadataModel_, this.selectionHandler_,
this.ui_.listContainer, this.ui_.selectionMenuButton,
this.quickViewModel_, this.taskController_, fileListSelectionModel,
this.quickViewUma_, metadataBoxController, this.dialogType,
this.volumeManager_, this.dialogDom_);
assert(this.fileFilter_);
assert(this.namingController_);
assert(this.appStateController_);
assert(this.taskController_);
this.mainWindowComponent_ = new MainWindowComponent(
this.dialogType, this.ui_, this.volumeManager_, this.directoryModel_,
this.selectionHandler_, this.namingController_,
this.appStateController_, this.taskController_);
this.initDataTransferOperations_();
fileListPromise.then(() => {
this.taskController_!.setFileTransferController(
this.fileTransferController_!);
});
this.selectionHandler_.onFileSelectionChanged();
this.ui_.listContainer.endBatchUpdates();
const bannerController = new BannerController(
this.directoryModel_, this.volumeManager_, this.crostini,
this.dialogType);
this.ui_.initBanners(bannerController);
bannerController.initialize();
this.ui_.attachFilesTooltip();
this.ui_.decorateFilesMenuItems();
this.ui_.selectionMenuButton.hidden = false;
await Promise.all([
fileListPromise,
currentDirectoryPromise,
this.setGuestMode_(),
]);
}
/**
* Subscribes to bulk-pinning events to ensure the store is kept up to date.
* Also tries to retrieve a first bulk pinning progress to populate the store.
*/
private async initBulkPinning_() {
try {
const promise = getBulkPinProgress();
if (!this.bulkPinningInitialized_) {
chrome.fileManagerPrivate.onBulkPinProgress.addListener(
(progress: chrome.fileManagerPrivate.BulkPinProgress) => {
console.debug('Got bulk-pinning event:', progress);
this.store_.dispatch(updateBulkPinProgress(progress));
});
this.bulkPinningInitialized_ = true;
}
const progress = await promise;
if (progress) {
console.debug('Got initial bulk-pinning state:', progress);
this.store_.dispatch(updateBulkPinProgress(progress));
}
} catch (e) {
console.warn('Cannot get initial bulk-pinning state:', e);
}
}
private initDataTransferOperations_() {
// CopyManager are required for 'Delete' operation in
// Open and Save dialogs. But drag-n-drop and copy-paste are not needed.
if (this.dialogType !== DialogType.FULL_PAGE) {
return;
}
this.fileTransferController_ = new FileTransferController(
this.document, this.ui.listContainer, this.ui.directoryTree!,
this.ui.showConfirmationDialog.bind(this.ui), this.progressCenter,
this.metadataModel, this.directoryModel, this.volumeManager,
this.selectionHandler, this.ui.toast);
}
/**
* One-time initialization of commands.
*/
private initCommands_() {
assert(this.ui_);
assert(this.ui_.textContextMenu);
assert(this.dialogDom_);
assert(this.directoryTreeNamingController_);
this.commandHandler_ = new CommandHandler(this);
// TODO(hirono): Move the following block to the UI part.
// Hook up the cr-button commands.
for (const crButton of this.dialogDom_.querySelectorAll<CrButtonElement>(
'cr-button[command]')) {
crInjectTypeAndInit(crButton, CommandButton);
}
for (const input of this.getDomInputs_()) {
this.setContextMenuForInput_(input);
}
this.setContextMenuForInput_(this.ui_.listContainer.renameInput);
this.setContextMenuForInput_(
this.directoryTreeNamingController_.getInputElement());
}
/**
* Get input elements from root DOM element of this app.
*/
private getDomInputs_() {
return this.dialogDom_!.querySelectorAll<HTMLInputElement>(
'input[type=text], input[type=search], textarea, cr-input');
}
/**
* Set context menu and handlers for an input element.
*/
private setContextMenuForInput_(input: HTMLInputElement) {
let touchInduced = false;
// stop contextmenu propagation for touch-induced events.
input.addEventListener('touchstart', (_e) => {
touchInduced = true;
});
input.addEventListener('contextmenu', (e) => {
if (touchInduced) {
e.stopImmediatePropagation();
}
touchInduced = false;
});
input.addEventListener('click', (_e) => {
touchInduced = false;
});
contextMenuHandler.setContextMenu(input, this.ui.textContextMenu);
this.registerInputCommands_(input);
}
/**
* Registers cut, copy, paste and delete commands on input element.
*
* @param node Text input element to register on.
*/
private registerInputCommands_(node: HTMLElement) {
forceDefaultHandler(node, 'cut');
forceDefaultHandler(node, 'copy');
forceDefaultHandler(node, 'paste');
forceDefaultHandler(node, 'delete');
node.addEventListener('keydown', (e: KeyboardEvent) => {
const key = getKeyModifiers(e) + e.keyCode;
if (key === '190' /* '/' */ || key === '191' /* '.' */) {
// If this key event is propagated, this is handled search command,
// which calls 'preventDefault' method.
e.stopPropagation();
}
});
}
/**
* Entry point of the initialization.
* This method is called from main.js.
*/
initializeCore() {
this.initGeneral_();
this.initSettingsPromise_ = this.startInitSettings_();
this.initBackgroundPagePromise_ =
this.startInitBackgroundPage_().then(() => this.initVolumeManager_());
window.addEventListener('pagehide', this.onUnload_.bind(this));
}
async initializeUi(dialogDom: HTMLElement): Promise<void> {
console.warn(`Files app starting up: ${this.dialogType}`);
this.dialogDom_ = dialogDom;
this.document_ = this.dialogDom_.ownerDocument;
startInterval('Load.InitDocuments');
// importElements depend on loadTimeData which is initialized in the
// initBackgroundPagePromise_.
await this.initBackgroundPagePromise_;
await importElements();
recordInterval('Load.InitDocuments');
startInterval('Load.InitUI');
this.document_.documentElement.classList.add('files-ng');
this.dialogDom_.classList.add('files-ng');
chrome.fileManagerPrivate.isTabletModeEnabled(
this.onTabletModeChanged_.bind(this));
chrome.fileManagerPrivate.onTabletModeChanged.addListener(
this.onTabletModeChanged_.bind(this));
this.initEssentialUi_();
// Initialize the Store for the whole app.
this.store_.init(getEmptyState());
this.initAdditionalUi_();
await this.initPrefs_();
await this.initSettingsPromise_;
const fileSystemUIPromise = this.initFileSystemUi_();
this.initUiFocus_();
recordInterval('Load.InitUI');
chrome.fileManagerPrivate.onDeviceConnectionStatusChanged.addListener(
this.updateDeviceConnectionState_.bind(this));
chrome.fileManagerPrivate.getDeviceConnectionState(
this.updateDeviceConnectionState_.bind(this));
return fileSystemUIPromise;
}
/**
* Initializes general purpose basic things, which are used by other
* initializing methods.
*/
private initGeneral_() {
// Initialize the application state, from the GET params.
let json = {};
if (location.search) {
const query = location.search.substr(1);
try {
json = JSON.parse(decodeURIComponent(query));
} catch (e) {
console.debug(`Error parsing location.search "${query}" due to ${e}`);
}
}
this.launchParams_ = new LaunchParam(json);
this.store_.dispatch(
setLaunchParameters({dialogType: this.launchParams_.type}));
// Initialize the member variables that depend this.launchParams_.
this.dialogType = this.launchParams_.type;
}
/**
* Initializes the background page.
*/
private async startInitBackgroundPage_(): Promise<void> {
startInterval('Load.InitBackgroundPage');
this.fileBrowserBackground_ = window.background;
await this.fileBrowserBackground_.ready();
// For the SWA, we load background and foreground in the same Window, avoid
// loading the `data` twice.
if (!loadTimeData.isInitialized()) {
loadTimeData.data = this.fileBrowserBackground_.stringData;
}
if (runningInBrowser()) {
this.fileBrowserBackground_.registerDialog(window);
}
this.crostini_ = this.fileBrowserBackground_.crostini;
recordInterval('Load.InitBackgroundPage');
}
/**
* Initializes the VolumeManager instance.
*/
private async initVolumeManager_() {
const allowedPaths = this.getAllowedPaths_();
assert(this.launchParams_);
assert(this.fileBrowserBackground_);
const writableOnly =
this.launchParams_.type === DialogType.SELECT_SAVEAS_FILE;
const disabledVolumes = await this.getDisabledVolumes_();
// FilteredVolumeManager hides virtual file system related event and data
// even depends on the value of |supportVirtualPath|. If it is
// VirtualPathSupport.NO_VIRTUAL_PATH, it hides Drive even if Drive is
// enabled on preference.
// In other words, even if Drive is disabled on preference but the Files app
// should show Drive when it is re-enabled, then the value should be set to
// true.
// Note that the Drive enabling preference change is listened by
// DriveIntegrationService, so here we don't need to take care about it.
this.volumeManager_ = new FilteredVolumeManager(
allowedPaths, writableOnly,
this.fileBrowserBackground_.getVolumeManager(),
this.launchParams_.volumeFilter, disabledVolumes);
await this.fileBrowserBackground_.getVolumeManager();
}
/**
* One time initialization of the essential UI elements in the Files app.
* These elements will be shown to the user. Only visible elements should be
* initialized here. Any heavy operation should be avoided. The Files app's
* window is shown at the end of this routine.
*/
private initEssentialUi_() {
// Record stats of dialog types. New values must NOT be inserted into the
// array enumerating the types. It must be in sync with
// FileDialogType enum in tools/metrics/histograms/histogram.xml.
const metricName = 'SWA.Create';
recordEnum(metricName, this.dialogType, [
DialogType.SELECT_FOLDER,
DialogType.SELECT_UPLOAD_FOLDER,
DialogType.SELECT_SAVEAS_FILE,
DialogType.SELECT_OPEN_FILE,
DialogType.SELECT_OPEN_MULTI_FILE,
DialogType.FULL_PAGE,
]);
// Create the metadata cache.
assert(this.volumeManager_);
this.metadataModel_ = MetadataModel.create(this.volumeManager_);
this.thumbnailModel_ = new ThumbnailModel(this.metadataModel_);
this.providersModel_ = new ProvidersModel(this.volumeManager_);
this.fileFilter_ = new FileFilter(this.volumeManager_);
// Set the files-ng class for dialog header styling.
const dialogHeader = queryRequiredElement('.dialog-header');
dialogHeader.classList.add('files-ng');
// Create the root view of FileManager.
assert(this.dialogDom_);
assert(this.launchParams_);
assert(this.providersModel_);
this.ui_ = new FileManagerUI(
this.providersModel_, this.dialogDom_, this.launchParams_);
}
/**
* One-time initialization of various DOM nodes. Loads the additional DOM
* elements visible to the user. Initialize here elements, which are expensive
* or hidden in the beginning.
*/
private initAdditionalUi_() {
assert(this.metadataModel_);
assert(this.volumeManager_);
assert(this.dialogDom_);
assert(this.ui_);
// Cache nodes we'll be manipulating.
const dom = this.dialogDom_;
assert(dom);
const table = queryRequiredElement('.detail-table', dom);
FileTable.decorate(
table, this.metadataModel_, this.volumeManager_, this.ui,
this.dialogType === DialogType.FULL_PAGE);
const grid = queryRequiredElement('.thumbnail-grid', dom);
FileGrid.decorate(grid, this.metadataModel_, this.volumeManager_, this.ui);
assertInstanceof(table, FileTable);
assertInstanceof(grid, FileGrid);
this.ui_.initAdditionalUI(table, grid, this.volumeManager_);
// Handle UI events.
this.progressCenter.addPanel(this.ui_.progressCenterPanel);
// Arrange the file list.
this.ui_.listContainer.table.normalizeColumns();
this.ui_.listContainer.table.redraw();
}
/**
* Initializes the prefs in the store.
*/
private async initPrefs_():
Promise<chrome.fileManagerPrivate.Preferences|null> {
let prefs = null;
try {
prefs = await getPreferences();
} catch (e) {
console.error('Cannot get preferences:', e);
return null;
}
this.store_.dispatch(updatePreferences(prefs));
return prefs;
}
/**
* One-time initialization of focus. This should run at the last of UI
* initialization.
*/
private initUiFocus_() {
this.ui_?.initUIFocus();
}
/**
* Constructs table and grid (heavy operation).
*/
private async initFileList_(): Promise<void> {
const singleSelection = this.dialogType === DialogType.SELECT_OPEN_FILE ||
this.dialogType === DialogType.SELECT_FOLDER ||
this.dialogType === DialogType.SELECT_UPLOAD_FOLDER ||
this.dialogType === DialogType.SELECT_SAVEAS_FILE;
assert(this.volumeManager_);
assert(this.metadataModel_);
assert(this.fileFilter_);
assert(this.launchParams_);
assert(this.ui_);
assert(this.thumbnailModel_);
assert(this.appStateController_);
assert(this.crostini_);
assert(this.providersModel_);
this.directoryModel_ = new DirectoryModel(
singleSelection, this.fileFilter_, this.metadataModel_,
this.volumeManager_);
assert(this.directoryModel_);
this.folderShortcutsModel_ =
new FolderShortcutsDataModel(this.volumeManager_);
this.androidAppListModel_ = new AndroidAppListModel(
this.launchParams_.showAndroidPickerApps,
this.launchParams_.includeAllFiles, this.launchParams_.typeList);
this.recentEntry_ = new FakeEntryImpl(
str('RECENT_ROOT_LABEL'), RootType.RECENT, this.getSourceRestriction_(),
chrome.fileManagerPrivate.FileCategory.ALL);
this.store_.dispatch(addUiEntry(this.recentEntry_));
assert(this.launchParams_);
this.selectionHandler_ = new FileSelectionHandler(
this.directoryModel_, this.ui_.listContainer, this.metadataModel_,
this.volumeManager_, this.launchParams_.allowedPaths);
// TODO(mtomasz, yoshiki): Create navigation list earlier, and here just
// attach the directory model.
const directoryTreePromise = this.initDirectoryTree_();
this.ui_.listContainer.listThumbnailLoader = new ListThumbnailLoader(
this.directoryModel_, this.thumbnailModel_, this.volumeManager_);
this.ui_.listContainer.dataModel = this.directoryModel_.getFileList();
this.ui_.listContainer.emptyDataModel =
this.directoryModel_.getEmptyFileList();
this.ui_.listContainer.selectionModel =
this.directoryModel_.getFileListSelection();
this.appStateController_.initialize(this.ui_, this.directoryModel_);
// Create metadata update controller.
this.metadataUpdateController_ = new MetadataUpdateController(
this.ui_.listContainer, this.directoryModel_, this.metadataModel_,
this.fileMetadataFormatter_);
// Create naming controller.
this.namingController_ = new NamingController(
this.ui_.listContainer, this.ui_.alertDialog, this.ui_.confirmDialog,
this.directoryModel_, this.fileFilter_, this.selectionHandler_);
// Create task controller.
this.taskController_ = new TaskController(
this.volumeManager_, this.ui_, this.metadataModel_,
this.directoryModel_, this.selectionHandler_,
this.metadataUpdateController_, this.crostini_, this.progressCenter);
// Create directory tree naming controller.
this.directoryTreeNamingController_ = new DirectoryTreeNamingController(
this.directoryModel_, this.ui_.directoryTree,
this.ui_.directoryTreeContainer, this.ui_.alertDialog);
// Create spinner controller.
this.spinnerController_ =
new SpinnerController(this.ui_.listContainer.spinner);
this.spinnerController_.blink();
// Create dialog action controller.
this.dialogActionController_ = new DialogActionController(
this.dialogType, this.ui_.dialogFooter, this.directoryModel_,
this.volumeManager_, this.fileFilter_, this.namingController_,
this.selectionHandler_, this.launchParams_);
// Create file-type filter controller.
this.fileTypeFiltersController_ = new FileTypeFiltersController(
this.ui_.fileTypeFilterContainer, this.directoryModel_,
this.recentEntry_, this.ui);
this.emptyFolderController_ = new EmptyFolderController(
this.ui_.emptyFolder, this.directoryModel_, this.providersModel_,
this.recentEntry_);
return directoryTreePromise;
}
/**
* Based on the dialog type and dialog caller, sets the list of volumes
* that should be disabled according to Data Leak Prevention rules.
*/
private async getDisabledVolumes_(): Promise<VolumeType[]> {
if (this.dialogType !== DialogType.SELECT_SAVEAS_FILE || !isDlpEnabled()) {
return [];
}
const caller = await getDialogCaller();
if (!caller.url) {
return [];
}
const dlpBlockedComponents = await getDlpBlockedComponents(caller.url);
const disabledVolumes = [];
for (const c of dlpBlockedComponents) {
disabledVolumes.push(c);
}
return disabledVolumes as VolumeType[];
}
private async initDirectoryTree_(): Promise<void> {
this.navigationUma_ = new NavigationUma(this.volumeManager);
assert(this.dialogDom_);
assert(this.directoryModel_);
assert(this.ui_);
assert(this.volumeManager_);
assert(this.directoryModel_);
assert(this.metadataModel_);
assert(this.folderShortcutsModel_);
assert(this.launchParams_);
assert(this.recentEntry_);
assert(this.androidAppListModel_);
assert(this.crostini_);
const treeContainerDiv = this.dialogDom_.querySelector<HTMLDivElement>(
'.dialog-navigation-list-contents');
assert(treeContainerDiv);
const directoryTreeContainer =
new DirectoryTreeContainer(treeContainerDiv, this.directoryModel_);
this.ui_.initDirectoryTree(directoryTreeContainer);
// If 'media-store-files-only' volume filter is enabled, then Android ARC
// SelectFile opened files app to pick files from volumes that are indexed
// by the Android MediaStore. Never add Drive, Crostini, GuestOS, to the
// directory tree in that case: their volume content is not indexed by the
// Android MediaStore, and being indexed there is needed for this Android
// ARC SelectFile MediaStore filter mode to work: crbug.com/1333385
if (this.volumeManager_.getMediaStoreFilesOnlyFilterEnabled()) {
return;
}
// Drive add/removes itself from directory tree in onPreferencesChanged_.
// Setup a prefs change listener then call onPreferencesChanged_() to add
// Drive to the directory tree if Drive is enabled by prefs.
chrome.fileManagerPrivate.onPreferencesChanged.addListener(() => {
this.onPreferencesChanged_();
});
this.onPreferencesChanged_();
chrome.fileManagerPrivate.onDriveConnectionStatusChanged.addListener(() => {
this.onDriveConnectionStatusChanged_();
});
this.onDriveConnectionStatusChanged_();
// The fmp.onCrostiniChanged receives enabled/disabled events via a pref
// watcher and share/unshare events. The enabled/disabled prefs are
// handled in fmp.onCrostiniChanged rather than fmp.onPreferencesChanged
// to keep crostini logic colocated, and to have an API that best supports
// multiple VMs.
chrome.fileManagerPrivate.onCrostiniChanged.addListener(
this.onCrostiniChanged_.bind(this));
this.crostiniController_ = new CrostiniController(this.crostini_);
await this.crostiniController_.redraw();
// Never show toast in an open-file dialog.
const maybeShowToast = this.dialogType === DialogType.FULL_PAGE;
await this.crostiniController_.loadSharedPaths(
maybeShowToast, this.ui_.toast);
if (isGuestOsEnabled()) {
this.guestOsController_ = new GuestOsController();
await this.guestOsController_.refresh();
}
}
/**
* Listens for the enable and disable events in order to add or remove the
* directory tree 'Linux files' root item.
*
*/
private async onCrostiniChanged_(
event: chrome.fileManagerPrivate.CrostiniEvent): Promise<void> {
assert(this.crostini_);
assert(this.crostiniController_);
assert(this.ui_);
assert(this.volumeManager_);
assert(this.metadataModel_);
assert(this.directoryModel_);
// The background |this.crostini_| object also listens to all crostini
// events including enable/disable, and share/unshare.
// But to ensure we don't have any race conditions between bg and fg, we
// set enabled status on it before calling |setupCrostini_| which reads
// enabled status from it to determine whether 'Linux files' is shown.
switch (event.eventType) {
case chrome.fileManagerPrivate.CrostiniEventType.ENABLE:
this.crostini_.setEnabled(event.vmName, event.containerName, true);
return this.crostiniController_.redraw();
case chrome.fileManagerPrivate.CrostiniEventType.DISABLE:
this.crostini_.setEnabled(event.vmName, event.containerName, false);
return this.crostiniController_.redraw();
// Event is sent when a user drops an unshared file on Plugin VM.
// We show the move dialog so the user can move the file or share the
// directory.
case chrome.fileManagerPrivate.CrostiniEventType
.DROP_FAILED_PLUGIN_VM_DIRECTORY_NOT_SHARED:
if (this.ui_.dragInProcess) {
const moveMessage =
str('UNABLE_TO_DROP_IN_PLUGIN_VM_DIRECTORY_NOT_SHARED_MESSAGE');
const copyMessage =
str('UNABLE_TO_DROP_IN_PLUGIN_VM_EXTERNAL_DRIVE_MESSAGE');
FileTasks.showPluginVmNotSharedDialog(
this.selectionHandler.selection.entries, this.volumeManager_,
this.metadataModel_, this.ui_, moveMessage, copyMessage,
this.fileTransferController_, this.directoryModel_);
}
break;
}
}
private async initMaterializedViews_() {
const views = await getMaterializedViews();
this.store_.dispatch(updateMaterializedViews({materializedViews: views}));
}
/**
* Sets up the current directory during initialization.
*/
private async setupCurrentDirectory_(): Promise<void> {
assert(this.launchParams_);
assert(this.recentEntry_);
assert(this.ui_);
assert(this.volumeManager_);
assert(this.metadataModel_);
assert(this.directoryModel_);
if (isSkyvaultV2Enabled()) {
this.oneDriveController_ = new OneDriveController();
}
const initMaterializedViewsPromise = isMaterializedViewsEnabled() ?
this.initMaterializedViews_() :
Promise.resolve();
const tracker = this.directoryModel_.createDirectoryChangeTracker();
tracker.start();
// Wait until the volume manager is initialized.
await new Promise<void>(
resolve => this.volumeManager.ensureInitialized(resolve));
let nextCurrentDirEntry: Entry|FilesAppEntry|null = null;
let selectionEntry: Entry|null = null;
// Resolve the selectionURL to selectionEntry or to currentDirectoryEntry in
// case of being a display root or a default directory to open files.
if (this.launchParams_.selectionURL) {
if (this.launchParams_.selectionURL === this.recentEntry_.toURL()) {
nextCurrentDirEntry = this.recentEntry_;
} else {
try {
const inEntry = await new Promise<Entry>((resolve, reject) => {
window.webkitResolveLocalFileSystemURL(
this.launchParams_!.selectionURL, resolve, reject);
});
const locationInfo = this.volumeManager_.getLocationInfo(inEntry);
// If location information is not available, then the volume is no
// longer (or never) available.
if (locationInfo) {
// If the selection is root, use it as a current directory instead.
// This is because selecting a root is the same as opening it.
if (locationInfo.isRootEntry) {
nextCurrentDirEntry = inEntry;
}
// If the |selectionURL| is a directory make it the current
// directory.
if (inEntry.isDirectory) {
nextCurrentDirEntry = inEntry;
}
// By default, the selection should be selected entry and the parent
// directory of it should be the current directory.
if (!nextCurrentDirEntry) {
selectionEntry = inEntry;
}
}
} catch (error: any) {
// If `selectionURL` doesn't exist we just don't select it, thus we
// don't need to log the failure.
if (error.name !== 'NotFoundError') {
console.warn(error.stack || error);
}
}
}
}
// If searchQuery param is set, find the first directory that matches the
// query, and select it if exists.
const searchQuery = this.launchParams_.searchQuery;
if (searchQuery) {
startInterval('Load.ProcessInitialSearchQuery');
assert(this.spinnerController_);
// Show a spinner, as the crossover search function call could be slow.
const hideSpinnerCallback = this.spinnerController_.show();
const queryMatchedDirEntry = await findQueryMatchedDirectoryEntry(
this.volumeManager_, this.directoryModel_, searchQuery);
if (queryMatchedDirEntry) {
nextCurrentDirEntry = queryMatchedDirEntry;
}
hideSpinnerCallback();
recordInterval('Load.ProcessInitialSearchQuery');
}
// Resolve the currentDirectoryURL to currentDirectoryEntry (if not done by
// the previous step).
if (!nextCurrentDirEntry && this.launchParams_.currentDirectoryURL) {
try {
const inEntry = await new Promise<Entry>((resolve, reject) => {
window.webkitResolveLocalFileSystemURL(
this.launchParams_!.currentDirectoryURL, resolve, reject);
});
const locationInfo = this.volumeManager_.getLocationInfo(inEntry);
if (locationInfo) {
nextCurrentDirEntry = inEntry;
}
} catch (error: any) {
console.warn(error.stack || error);
}
}
// If the directory to be changed to is not available, then first fallback
// to the parent of the selection entry.
if (!nextCurrentDirEntry && selectionEntry) {
nextCurrentDirEntry = await new Promise(resolve => {
selectionEntry!.getParent(resolve);
});
}
// Check if the next current directory is not a virtual directory which is
// not available in UI. This may happen to shared on Drive.
if (nextCurrentDirEntry) {
const locationInfo =
this.volumeManager_.getLocationInfo(nextCurrentDirEntry);
// If we can't check, assume that the directory is illegal.
if (!locationInfo) {
nextCurrentDirEntry = null;
} else {
// Having root directory of DRIVE_SHARED_WITH_ME here should be only for
// shared with me files. Fallback to Drive root in such case.
if (locationInfo.isRootEntry &&
locationInfo.rootType === RootType.DRIVE_SHARED_WITH_ME) {
const volumeInfo =
this.volumeManager_.getVolumeInfo(nextCurrentDirEntry);
if (!volumeInfo) {
nextCurrentDirEntry = null;
} else {
try {
nextCurrentDirEntry = await volumeInfo.resolveDisplayRoot();
} catch (error: any) {
console.warn(error.stack || error);
nextCurrentDirEntry = null;
}
}
}
}
}
// If the resolved directory to be changed is blocked by DLP, we should
// fallback to the default display root.
if (nextCurrentDirEntry && isDlpEnabled()) {
const volumeInfo = this.volumeManager_.getVolumeInfo(nextCurrentDirEntry);
if (volumeInfo && this.volumeManager_.isDisabled(volumeInfo.volumeType)) {
console.warn('Target directory is DLP blocked, redirecting to MyFiles');
nextCurrentDirEntry = null;
}
}
// If the directory to be changed to is still not resolved, then fallback to
// the default display root.
if (!nextCurrentDirEntry) {
nextCurrentDirEntry = await this.volumeManager_.getDefaultDisplayRoot();
}
// If selection failed to be resolved (eg. didn't exist, in case of saving a
// file, or in case of a fallback of the current directory, then try to
// resolve again using the target name.
if (!selectionEntry && nextCurrentDirEntry &&
this.launchParams_.targetName) {
// Try to resolve as a file first. If it fails, then as a directory.
try {
selectionEntry = await new Promise((resolve, reject) => {
(nextCurrentDirEntry as DirectoryEntry)
.getFile(this.launchParams_!.targetName, {}, resolve, reject);
});
} catch (error1: any) {
// Failed to resolve as a file. Try to resolve as a directory.
try {
selectionEntry = await new Promise((resolve, reject) => {
(nextCurrentDirEntry as DirectoryEntry)
.getDirectory(
this.launchParams_!.targetName, {}, resolve, reject);
});
} catch (error2: any) {
// If `targetName` doesn't exist we just don't select it, thus we
// don't need to log the failure.
if (error1.name !== 'NotFoundError') {
console.warn(error1.stack || error1);
console.log(error1);
}
if (error2.name !== 'NotFoundError') {
console.warn(error2.stack || error2);
}
}
}
}
// If there is no target select MyFiles by default, but only if local files
// are enabled.
if (!nextCurrentDirEntry && this.localUserFilesAllowed) {
assert(this.ui.directoryTree);
const myFiles = getMyFiles(this.store_.getState());
// When MyFiles volume is mounted, we rely on the current directory
// change to make it as selected (controlled by DirectoryModel),
// that's why we can't set MyFiles entry list as the current directory
// here.
// TODO(b/308504417): MyFiles entry list should be selected before
// MyFiles volume mounts.
if (myFiles && myFiles.myFilesVolume) {
nextCurrentDirEntry = myFiles.myFilesEntry;
}
}
// The next directory might be a materialized view.
await initMaterializedViewsPromise;
// TODO(b/328031885): Handle !nextCurrentDirEntry case here - it means some
// error occurred and we should show the appropriate UI.
// Check directory change.
if (!tracker.hasChanged) {
// Finish setup current directory.
await this.finishSetupCurrentDirectory_(
(nextCurrentDirEntry as DirectoryEntry), selectionEntry,
this.launchParams_.targetName);
}
// Only stop the tracker after finishing the directory change.
tracker.stop();
}
/**
* @param directoryEntry Directory to be opened.
* @param selectionEntry Entry to be selected.
* @param suggestedName Suggested name for a non-existing selection.
*/
private async finishSetupCurrentDirectory_(
directoryEntry: null|DirectoryEntry, selectionEntry?: Entry|null,
suggestedName?: string): Promise<void> {
// Open the directory, and select the selection (if passed).
const promise = (async () => {
console.warn('Files app has started');
if (directoryEntry) {
await new Promise(resolve => {
this.directoryModel.changeDirectoryEntry(directoryEntry, resolve);
});
if (selectionEntry) {
this.directoryModel.selectEntry(selectionEntry);
}
if (this.launchParams_?.searchQuery) {
const searchState = this.store_.getState().search;
this.store_.dispatch(updateSearch({
query: this.launchParams_.searchQuery,
status: undefined,
// Make sure the current directory can be highlighted in the
// directory tree.
options: {
...getDefaultSearchOptions(),
...searchState?.options,
location: SearchLocation.THIS_FOLDER,
},
}));
}
} else {
console.warn('No entry for finishSetupCurrentDirectory_');
}
this.ui.addLoadedAttribute();
})();
if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) {
this.ui.dialogFooter.filenameInput.value = suggestedName || '';
this.ui.dialogFooter.selectTargetNameInFilenameInput();
}
return promise;
}
/**
* Returns DirectoryEntry of the current directory. Returns null if the
* directory model is not ready or the current directory is not set.
*/
getCurrentDirectoryEntry(): DirectoryEntry|FakeEntry|FilesAppDirEntry|null
|undefined {
return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry();
}
/** Expose the unload method for integration tests. */
onUnloadForTest() {
this.onUnload_();
}
/**
* Unload handler for the page.
*/
private onUnload_() {
if (this.directoryModel_) {
this.directoryModel_.dispose();
}
if (this.volumeManager_) {
this.volumeManager_.dispose();
}
if (this.fileTransferController_) {
for (const taskId of this.fileTransferController_.pendingTaskIds) {
const item = this.progressCenter.getItemById(taskId)!;
item.message = '';
item.state = ProgressItemState.CANCELED;
this.progressCenter.updateItem(item);
}
}
if (this.ui_ && this.ui_.progressCenterPanel) {
this.progressCenter.removePanel(this.ui_.progressCenterPanel);
}
}
/**
* Returns allowed path for the dialog by considering:
* 1) The launch parameter which specifies generic category of valid files
* paths.
* 2) Files app's unique capabilities and restrictions.
*/
private getAllowedPaths_(): AllowedPaths {
assert(this.launchParams_);
let allowedPaths = this.launchParams_.allowedPaths;
// The native implementation of the Files app creates snapshot files for
// non-native files. But it does not work for folders (e.g., dialog for
// loading unpacked extensions).
if (allowedPaths === AllowedPaths.NATIVE_PATH &&
!isFolderDialogType(this.launchParams_.type)) {
if (this.launchParams_.type === DialogType.SELECT_SAVEAS_FILE) {
allowedPaths = AllowedPaths.NATIVE_PATH;
} else {
allowedPaths = AllowedPaths.ANY_PATH;
}
}
return allowedPaths;
}
/**
* Returns SourceRestriction which is used to communicate restrictions about
* sources to chrome.fileManagerPrivate.getRecentFiles API.
*/
private getSourceRestriction_(): chrome.fileManagerPrivate.SourceRestriction {
const allowedPaths = this.getAllowedPaths_();
if (allowedPaths === AllowedPaths.NATIVE_PATH) {
return chrome.fileManagerPrivate.SourceRestriction.NATIVE_SOURCE;
}
return chrome.fileManagerPrivate.SourceRestriction.ANY_SOURCE;
}
/**
* @return Selection object.
*/
getSelection(): FileSelection {
return this.selectionHandler.selection;
}
/**
* @return File list.
*/
getFileList(): ArrayDataModel {
return this.directoryModel.getFileList();
}
/**
* @return Current list object.
*/
getCurrentList(): List {
return this.ui.listContainer.currentList;
}
/**
* Add or remove the fake Drive and Trash item from the directory tree when
* the prefs change. If Drive or Trash has been enabled by prefs, add the item
* otherwise remove it. This supports dynamic refresh when the pref changes.
*/
private async onPreferencesChanged_() {
const prefs = await this.initPrefs_();
if (!prefs) {
return;
}
if (this.driveEnabled_ !== prefs.driveEnabled) {
this.driveEnabled_ = prefs.driveEnabled;
this.toggleDriveRootOnPreferencesUpdate_();
}
if (this.bulkPinningAvailable_ !== prefs.driveFsBulkPinningAvailable) {
this.bulkPinningAvailable_ = prefs.driveFsBulkPinningAvailable;
console.debug(`Bulk-pinning is now ${
this.bulkPinningAvailable_ ? 'available' : 'unavailable'}`);
if (this.bulkPinningAvailable_) {
await this.initBulkPinning_();
}
}
assert(this.toolbarController_);
if (this.trashEnabled !== prefs.trashEnabled) {
this.trashEnabled = prefs.trashEnabled;
this.toggleTrashRootOnPreferencesUpdate_();
this.toolbarController_.moveToTrashCommand.disabled = !this.trashEnabled;
this.toolbarController_.moveToTrashCommand.canExecuteChange(
this.ui.listContainer.currentList);
}
if (this.localUserFilesAllowed !== prefs.localUserFilesAllowed) {
this.localUserFilesAllowed = prefs.localUserFilesAllowed;
// Trigger the change after prefs are updated, so that if needed, the
// default root can be resolved correctly.
await this.maybeChangeRootOnPreferencesUpdate_();
}
await this.updateOfficePrefs_(prefs);
assert(this.ui.directoryTree);
}
private async onDriveConnectionStatusChanged_() {
let connectionState = null;
try {
connectionState = await getDriveConnectionState();
} catch (e) {
console.error('Failed to retrieve drive connection state:', e);
return;
}
this.store_.dispatch(updateDriveConnectionStatus(connectionState));
}
private async updateOfficePrefs_(prefs:
chrome.fileManagerPrivate.Preferences) {
assert(this.ui_);
// These prefs starts with value 0. We only want to display when they're
// non-zero and show the most recent (larger value).
if (prefs.officeFileMovedOneDrive > prefs.officeFileMovedGoogleDrive) {
this.ui_.nudgeContainer.showNudge(
NudgeType['ONE_DRIVE_MOVED_FILE_NUDGE']);
} else if (
prefs.officeFileMovedOneDrive < prefs.officeFileMovedGoogleDrive) {
this.ui_.nudgeContainer.showNudge(NudgeType['DRIVE_MOVED_FILE_NUDGE']);
}
// Reset the seen state for office nudge. For normal users these 2 prefs
// will never reset to 0, however for manual tests it can be reset in
// chrome://files-internals.
if (prefs.officeFileMovedOneDrive === 0 &&
await this.ui_.nudgeContainer.checkSeen(
NudgeType['ONE_DRIVE_MOVED_FILE_NUDGE'])) {
this.ui_.nudgeContainer.clearSeen(
NudgeType['ONE_DRIVE_MOVED_FILE_NUDGE']);
console.debug('Reset OneDrive move to cloud nudge');
}
if (prefs.officeFileMovedGoogleDrive === 0 &&
await this.ui_.nudgeContainer.checkSeen(
NudgeType['DRIVE_MOVED_FILE_NUDGE'])) {
this.ui_.nudgeContainer.clearSeen(NudgeType['DRIVE_MOVED_FILE_NUDGE']);
console.debug('Reset Google Drive move to cloud nudge');
}
}
/**
* Invoked when the device connection status changes.
*/
private updateDeviceConnectionState_(
state: chrome.fileManagerPrivate.DeviceConnectionState) {
this.store_.dispatch(updateDeviceConnectionState({connection: state}));
}
/**
* Toggles the trash root visibility when the `trashEnabled` preference is
* updated.
*/
private toggleTrashRootOnPreferencesUpdate_() {
assert(this.ui_);
let trashRoot: TrashRootEntry|null =
getEntry(this.store_.getState(), trashRootKey) as TrashRootEntry | null;
if (this.trashEnabled) {
if (!trashRoot) {
trashRoot = new TrashRootEntry();
}
this.store_.dispatch(addUiEntry(trashRoot));
assert(this.ui_.directoryTree);
return;
}
this.store_.dispatch(removeUiEntry(trashRootKey));
assert(this.ui_.directoryTree);
this.navigateAwayFromDisabledRoot_(trashRoot || null);
}
/**
* Toggles the drive root visibility when the `driveEnabled` preference is
* updated.
*/
private toggleDriveRootOnPreferencesUpdate_() {
let driveFakeRoot: EntryList|FakeEntry|null =
getEntry(this.store_.getState(), driveRootEntryListKey) as EntryList |
null;
if (this.driveEnabled_) {
if (!driveFakeRoot) {
driveFakeRoot = new EntryList(
str('DRIVE_DIRECTORY_LABEL'), RootType.DRIVE_FAKE_ROOT);
this.store_.dispatch(addUiEntry(driveFakeRoot));
}
assert(this.ui.directoryTree);
return;
}
this.store_.dispatch(removeUiEntry(driveRootEntryListKey));
assert(this.ui.directoryTree);
this.navigateAwayFromDisabledRoot_(driveFakeRoot);
}
/**
* Navigates to default display root if currently in a local folder and
* `localUserFilesAllowed` preference is updated to False.
*/
private async maybeChangeRootOnPreferencesUpdate_() {
if (this.localUserFilesAllowed) {
return;
}
assert(this.directoryModel_);
assert(this.volumeManager_);
const fileData = this.directoryModel_.getCurrentFileData();
if (!fileData) {
return;
}
const tracker = this.directoryModel_.createDirectoryChangeTracker();
tracker.start();
const state = this.store_.getState();
const volume = getVolume(state, fileData);
// The current directory is pointing to an entry that has a volume,
// but the volume isn't mounted anymore.
if (fileData.volumeId && !volume) {
const displayRoot = await this.volumeManager_.getDefaultDisplayRoot();
if (displayRoot && !tracker.hasChanged) {
this.directoryModel_!.changeDirectoryEntry(displayRoot);
}
}
tracker.stop();
}
/**
* If the root item has been disabled but it is the current visible entry,
* navigate away from it to the default display root.
* @param entry The entry to navigate away from.
*/
private navigateAwayFromDisabledRoot_(entry: null|Entry|FilesAppEntry) {
if (!entry) {
return;
}
assert(this.directoryModel_);
assert(this.volumeManager_);
// The fake root item is being hidden so navigate away if it's the
// current directory.
if (this.directoryModel_.getCurrentDirEntry() === entry) {
this.volumeManager_.getDefaultDisplayRoot().then((displayRoot) => {
if (this.directoryModel_!.getCurrentDirEntry() === entry &&
displayRoot) {
this.directoryModel_!.changeDirectoryEntry(displayRoot);
}
});
}
}
/**
* Updates the DOM to reflect the specified tablet mode `enabled` state.
*/
private onTabletModeChanged_(enabled: boolean) {
this.dialogDom_!.classList.toggle('tablet-mode-enabled', enabled);
}
}