1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * Responsible for showing following banners in the file list.
9 * @param {DirectoryModel} directoryModel The model.
10 * @param {VolumeManagerWrapper} volumeManager The manager.
11 * @param {Document} document HTML document.
12 * @param {boolean} showOffers True if we should show offer banners.
14 * @extends {cr.EventTarget}
16 function FileListBannerController(
17 directoryModel, volumeManager, document, showOffers) {
18 this.directoryModel_ = directoryModel;
19 this.volumeManager_ = volumeManager;
20 this.document_ = document;
21 this.showOffers_ = showOffers;
22 this.driveEnabled_ = false;
24 this.initializeWelcomeBanner_();
25 this.privateOnDirectoryChangedBound_ =
26 this.privateOnDirectoryChanged_.bind(this);
28 var handler = this.checkSpaceAndMaybeShowWelcomeBanner_.bind(this);
29 this.directoryModel_.addEventListener('scan-completed', handler);
30 this.directoryModel_.addEventListener('rescan-completed', handler);
31 this.directoryModel_.addEventListener('directory-changed',
32 this.onDirectoryChanged_.bind(this));
34 this.unmountedPanel_ = this.document_.querySelector('#unmounted-panel');
35 this.volumeManager_.volumeInfoList.addEventListener(
36 'splice', this.onVolumeInfoListSplice_.bind(this));
37 this.volumeManager_.addEventListener('drive-connection-changed',
38 this.onDriveConnectionChanged_.bind(this));
40 chrome.storage.onChanged.addListener(this.onStorageChange_.bind(this));
41 this.welcomeHeaderCounter_ = WELCOME_HEADER_COUNTER_LIMIT;
42 this.warningDismissedCounter_ = 0;
43 chrome.storage.local.get(
44 [WELCOME_HEADER_COUNTER_KEY, WARNING_DISMISSED_KEY],
46 this.welcomeHeaderCounter_ =
47 parseInt(values[WELCOME_HEADER_COUNTER_KEY], 10) || 0;
48 this.warningDismissedCounter_ =
49 parseInt(values[WARNING_DISMISSED_KEY], 10) || 0;
52 this.authFailedBanner_ =
53 this.document_.querySelector('#drive-auth-failed-warning');
54 var authFailedText = this.authFailedBanner_.querySelector('.drive-text');
55 authFailedText.innerHTML = util.htmlUnescape(str('DRIVE_NOT_REACHED'));
56 authFailedText.querySelector('a').addEventListener('click', function(e) {
57 chrome.fileManagerPrivate.logoutUserForReauthentication();
60 this.maybeShowAuthFailBanner_();
64 * FileListBannerController extends cr.EventTarget.
66 FileListBannerController.prototype.__proto__ = cr.EventTarget.prototype;
69 * Key in localStorage to keep number of times the Drive Welcome
72 var WELCOME_HEADER_COUNTER_KEY = 'driveWelcomeHeaderCounter';
74 // If the warning was dismissed before, this key stores the quota value
75 // (as of the moment of dismissal).
76 // If the warning was never dismissed or was reset this key stores 0.
77 var WARNING_DISMISSED_KEY = 'driveSpaceWarningDismissed';
80 * Maximum times Drive Welcome banner could have shown.
82 var WELCOME_HEADER_COUNTER_LIMIT = 25;
85 * Initializes the banner to promote DRIVE.
86 * This method must be called before any of showing banner functions, and
87 * also before registering them as callbacks.
90 FileListBannerController.prototype.initializeWelcomeBanner_ = function() {
91 this.usePromoWelcomeBanner_ = !util.boardIs('x86-mario') &&
92 !util.boardIs('x86-zgb') &&
93 !util.boardIs('x86-alex');
97 * @param {number} value How many times the Drive Welcome header banner
101 FileListBannerController.prototype.setWelcomeHeaderCounter_ = function(value) {
103 values[WELCOME_HEADER_COUNTER_KEY] = value;
104 chrome.storage.local.set(values);
108 * @param {number} value How many times the low space warning has dismissed.
111 FileListBannerController.prototype.setWarningDismissedCounter_ =
114 values[WARNING_DISMISSED_KEY] = value;
115 chrome.storage.local.set(values);
119 * chrome.storage.onChanged event handler.
120 * @param {Object.<string, Object>} changes Changes values.
121 * @param {string} areaName "local" or "sync".
124 FileListBannerController.prototype.onStorageChange_ = function(changes,
126 if (areaName == 'local' && WELCOME_HEADER_COUNTER_KEY in changes) {
127 this.welcomeHeaderCounter_ = changes[WELCOME_HEADER_COUNTER_KEY].newValue;
129 if (areaName == 'local' && WARNING_DISMISSED_KEY in changes) {
130 this.warningDismissedCounter_ = changes[WARNING_DISMISSED_KEY].newValue;
135 * Invoked when the drive connection status is change in the volume manager.
138 FileListBannerController.prototype.onDriveConnectionChanged_ = function() {
139 this.maybeShowAuthFailBanner_();
143 * @param {string} type 'none'|'page'|'header'.
144 * @param {string} messageId Resource ID of the message.
147 FileListBannerController.prototype.prepareAndShowWelcomeBanner_ =
148 function(type, messageId) {
149 this.showWelcomeBanner_(type);
151 var container = this.document_.querySelector('.drive-welcome.' + type);
152 if (container.firstElementChild)
153 return; // Do not re-create.
155 if (!this.document_.querySelector('link[drive-welcome-style]')) {
156 var style = this.document_.createElement('link');
157 style.rel = 'stylesheet';
158 style.href = 'foreground/css/drive_welcome.css';
159 style.setAttribute('drive-welcome-style', '');
160 this.document_.head.appendChild(style);
163 var wrapper = util.createChild(container, 'drive-welcome-wrapper');
164 util.createChild(wrapper, 'drive-welcome-icon');
166 var close = util.createChild(wrapper, 'cr-dialog-close');
167 close.addEventListener('click', this.closeWelcomeBanner_.bind(this));
169 var message = util.createChild(wrapper, 'drive-welcome-message');
171 var title = util.createChild(message, 'drive-welcome-title');
173 var text = util.createChild(message, 'drive-welcome-text');
174 text.innerHTML = str(messageId);
176 var links = util.createChild(message, 'drive-welcome-links');
179 if (this.usePromoWelcomeBanner_) {
180 var welcomeTitle = str('DRIVE_WELCOME_TITLE_ALTERNATIVE');
181 if (util.boardIs('link'))
182 welcomeTitle = str('DRIVE_WELCOME_TITLE_ALTERNATIVE_1TB');
183 title.textContent = welcomeTitle;
184 more = util.createChild(links,
185 'drive-welcome-button drive-welcome-start', 'a');
186 more.textContent = str('DRIVE_WELCOME_CHECK_ELIGIBILITY');
187 more.href = str('GOOGLE_DRIVE_REDEEM_URL');
189 title.textContent = str('DRIVE_WELCOME_TITLE');
190 more = util.createChild(links, 'plain-link', 'a');
191 more.textContent = str('DRIVE_LEARN_MORE');
192 more.href = str('GOOGLE_DRIVE_OVERVIEW_URL');
194 more.tabIndex = '16'; // See: go/filesapp-tabindex.
195 more.target = '_blank';
198 if (this.usePromoWelcomeBanner_)
199 dismiss = util.createChild(links, 'drive-welcome-button');
201 dismiss = util.createChild(links, 'plain-link');
203 dismiss.classList.add('drive-welcome-dismiss');
204 dismiss.textContent = str('DRIVE_WELCOME_DISMISS');
205 dismiss.addEventListener('click', this.closeWelcomeBanner_.bind(this));
207 this.previousDirWasOnDrive_ = false;
211 * Show or hide the "Low Google Drive space" warning.
212 * @param {boolean} show True if the box need to be shown.
213 * @param {Object=} opt_sizeStats Size statistics. Should be defined when
214 * showing the warning.
217 FileListBannerController.prototype.showLowDriveSpaceWarning_ =
218 function(show, opt_sizeStats) {
219 var box = this.document_.querySelector('#volume-space-warning');
221 // Avoid showing two banners.
222 // TODO(kaznacheev): Unify the low space warning and the promo header.
224 this.cleanupWelcomeBanner_();
226 if (box.hidden == !show)
229 if (this.warningDismissedCounter_) {
231 // Quota had not changed
232 this.warningDismissedCounter_ == opt_sizeStats.totalSize &&
233 opt_sizeStats.remainingSize / opt_sizeStats.totalSize < 0.15) {
234 // Since the last dismissal decision the quota has not changed AND
235 // the user did not free up significant space. Obey the dismissal.
238 // Forget the dismissal. Warning will be shown again.
239 this.setWarningDismissedCounter_(0);
243 box.textContent = '';
244 if (show && opt_sizeStats) {
245 var icon = this.document_.createElement('div');
246 icon.className = 'drive-icon';
247 box.appendChild(icon);
249 var text = this.document_.createElement('div');
250 text.className = 'drive-text';
251 text.textContent = strf('DRIVE_SPACE_AVAILABLE_LONG',
252 util.bytesToString(opt_sizeStats.remainingSize));
253 box.appendChild(text);
255 var link = this.document_.createElement('a');
256 link.className = 'plain-link';
257 link.textContent = str('DRIVE_BUY_MORE_SPACE_LINK');
258 link.href = str('GOOGLE_DRIVE_BUY_STORAGE_URL');
259 link.target = '_blank';
260 box.appendChild(link);
262 var close = this.document_.createElement('div');
263 close.className = 'cr-dialog-close';
264 box.appendChild(close);
265 close.addEventListener('click', function(total) {
267 values[WARNING_DISMISSED_KEY] = total;
268 chrome.storage.local.set(values);
270 this.requestRelayout_(100);
271 }.bind(this, opt_sizeStats.totalSize));
274 if (box.hidden != !show) {
276 this.requestRelayout_(100);
280 * Closes the Drive Welcome banner.
283 FileListBannerController.prototype.closeWelcomeBanner_ = function() {
284 this.cleanupWelcomeBanner_();
285 // Stop showing the welcome banner.
286 this.setWelcomeHeaderCounter_(WELCOME_HEADER_COUNTER_LIMIT);
290 * Shows or hides the welcome banner for drive.
293 FileListBannerController.prototype.checkSpaceAndMaybeShowWelcomeBanner_ =
295 if (!this.isOnCurrentProfileDrive()) {
296 // We are not on the drive file system. Do not show (close) the welcome
298 this.cleanupWelcomeBanner_();
299 this.previousDirWasOnDrive_ = false;
303 var driveVolume = this.volumeManager_.getCurrentProfileVolumeInfo(
304 VolumeManagerCommon.VolumeType.DRIVE);
305 if (this.welcomeHeaderCounter_ >= WELCOME_HEADER_COUNTER_LIMIT ||
306 !driveVolume || driveVolume.error) {
307 // The banner is already shown enough times or the drive FS is not mounted.
308 // So, do nothing here.
312 if (!this.showOffers_) {
313 // Because it is not necessary to show the offer, set
314 // |usePromoWelcomeBanner_| false here. Note that it probably should be able
315 // to do this in the constructor, but there remains non-trivial path,
316 // which may be causes |usePromoWelcomeBanner_| == true's behavior even
317 // if |showOffers_| is false.
318 // TODO(hidehiko): Make sure if it is expected or not, and simplify
319 // |showOffers_| if possible.
320 this.usePromoWelcomeBanner_ = false;
323 // Perform asynchronous tasks in parallel.
324 var group = new AsyncUtil.Group();
326 // Choose the offer basing on the board name. The default one is 100 GB.
327 var offerSize = 100; // In GB.
328 var offerServiceId = 'drive.cros.echo.1';
330 if (util.boardIs('link')) {
331 offerSize = 1024; // 1 TB.
332 offerServiceId = 'drive.cros.echo.2';
335 // If the offer has been checked, then do not show the promo anymore.
336 group.add(function(onCompleted) {
337 chrome.echoPrivate.getOfferInfo(offerServiceId, function(offerInfo) {
338 // If the offer has not been checked, then an error is raised.
339 if (!chrome.runtime.lastError)
340 this.usePromoWelcomeBanner_ = false;
345 if (this.usePromoWelcomeBanner_) {
346 // getSizeStats for Drive file system accesses to the server, so we should
347 // minimize the invocation.
348 group.add(function(onCompleted) {
349 // Current directory must be set, since this code is called after
350 // scanning is completed. However, the volumeInfo may be gone.
351 chrome.fileManagerPrivate.getSizeStats(
352 driveVolume.volumeId,
354 if (result && result.totalSize >= offerSize * 1024 * 1024 * 1024)
355 this.usePromoWelcomeBanner_ = false;
361 group.run(this.maybeShowWelcomeBanner_.bind(this));
365 * Decides which banner should be shown, and show it. This method is designed
366 * to be called only from checkSpaceAndMaybeShowWelcomeBanner_.
369 FileListBannerController.prototype.maybeShowWelcomeBanner_ = function() {
370 if (this.directoryModel_.getFileList().length == 0 &&
371 this.welcomeHeaderCounter_ == 0) {
372 // Only show the full page banner if the header banner was never shown.
373 // Do not increment the counter.
374 // The timeout below is required because sometimes another
375 // 'rescan-completed' event arrives shortly with non-empty file list.
376 setTimeout(function() {
377 if (this.isOnCurrentProfileDrive() && this.welcomeHeaderCounter_ == 0) {
378 this.prepareAndShowWelcomeBanner_('page', 'DRIVE_WELCOME_TEXT_LONG');
382 // We do not want to increment the counter when the user navigates
383 // between different directories on Drive, but we increment the counter
384 // once anyway to prevent the full page banner from showing.
385 if (!this.previousDirWasOnDrive_ || this.welcomeHeaderCounter_ == 0) {
386 this.setWelcomeHeaderCounter_(this.welcomeHeaderCounter_ + 1);
387 this.prepareAndShowWelcomeBanner_('header', 'DRIVE_WELCOME_TEXT_SHORT');
390 this.previousDirWasOnDrive_ = true;
394 * @return {boolean} True if current directory is on Drive root of current
397 FileListBannerController.prototype.isOnCurrentProfileDrive = function() {
398 var entry = this.directoryModel_.getCurrentDirEntry();
399 if (!entry || util.isFakeEntry(entry))
401 var locationInfo = this.volumeManager_.getLocationInfo(entry);
404 return locationInfo.rootType === VolumeManagerCommon.RootType.DRIVE &&
405 locationInfo.volumeInfo.profile.isCurrentProfile;
409 * Shows the Drive Welcome banner.
410 * @param {string} type 'page'|'head'|'none'.
413 FileListBannerController.prototype.showWelcomeBanner_ = function(type) {
414 var container = this.document_.querySelector('.dialog-container');
415 if (container.getAttribute('drive-welcome') != type) {
416 container.setAttribute('drive-welcome', type);
417 this.requestRelayout_(200); // Resize only after the animation is done.
422 * Update the UI when the current directory changes.
424 * @param {Event} event The directory-changed event.
427 FileListBannerController.prototype.onDirectoryChanged_ = function(event) {
428 var rootVolume = this.volumeManager_.getVolumeInfo(event.newDirEntry);
429 var previousRootVolume = event.previousDirEntry ?
430 this.volumeManager_.getVolumeInfo(event.previousDirEntry) : null;
432 // Show (or hide) the low space warning.
433 this.maybeShowLowSpaceWarning_(rootVolume);
435 // Add or remove listener to show low space warning, if necessary.
436 var isLowSpaceWarningTarget = this.isLowSpaceWarningTarget_(rootVolume);
437 if (isLowSpaceWarningTarget !==
438 this.isLowSpaceWarningTarget_(previousRootVolume)) {
439 if (isLowSpaceWarningTarget) {
440 chrome.fileManagerPrivate.onDirectoryChanged.addListener(
441 this.privateOnDirectoryChangedBound_);
443 chrome.fileManagerPrivate.onDirectoryChanged.removeListener(
444 this.privateOnDirectoryChangedBound_);
448 if (!this.isOnCurrentProfileDrive()) {
449 this.cleanupWelcomeBanner_();
450 this.authFailedBanner_.hidden = true;
453 this.updateDriveUnmountedPanel_();
454 if (this.isOnCurrentProfileDrive()) {
455 this.unmountedPanel_.classList.remove('retry-enabled');
456 this.maybeShowAuthFailBanner_();
461 * @param {VolumeInfo} volumeInfo Volume info to be checked.
462 * @return {boolean} true if the file system specified by |root| is a target
463 * to show low space warning. Otherwise false.
466 FileListBannerController.prototype.isLowSpaceWarningTarget_ =
467 function(volumeInfo) {
470 return volumeInfo.profile.isCurrentProfile &&
471 (volumeInfo.volumeType === VolumeManagerCommon.VolumeType.DOWNLOADS ||
472 volumeInfo.volumeType === VolumeManagerCommon.VolumeType.DRIVE);
476 * Callback which is invoked when the file system has been changed.
477 * @param {Object} event chrome.fileManagerPrivate.onDirectoryChanged event.
480 FileListBannerController.prototype.privateOnDirectoryChanged_ = function(
482 if (!this.directoryModel_.getCurrentDirEntry())
485 var currentDirEntry = this.directoryModel_.getCurrentDirEntry();
486 var currentVolume = currentDirEntry &&
487 this.volumeManager_.getVolumeInfo(currentDirEntry);
488 var eventVolume = this.volumeManager_.getVolumeInfo(event.entry);
489 if (currentVolume === eventVolume) {
490 // The file system we are currently on is changed.
491 // So, check the free space.
492 this.maybeShowLowSpaceWarning_(currentVolume);
497 * Shows or hides the low space warning.
498 * @param {VolumeInfo} volume Type of volume, which we are interested in.
501 FileListBannerController.prototype.maybeShowLowSpaceWarning_ = function(
503 // TODO(kaznacheev): Unify the two low space warning.
505 switch (volume.volumeType) {
506 case VolumeManagerCommon.VolumeType.DOWNLOADS:
507 this.showLowDriveSpaceWarning_(false);
510 case VolumeManagerCommon.VolumeType.DRIVE:
511 this.showLowDownloadsSpaceWarning_(false);
515 // If the current file system is neither the DOWNLOAD nor the DRIVE,
516 // just hide the warning.
517 this.showLowDownloadsSpaceWarning_(false);
518 this.showLowDriveSpaceWarning_(false);
522 // If not mounted correctly, then do not continue.
523 if (!volume.fileSystem)
526 chrome.fileManagerPrivate.getSizeStats(
528 function(sizeStats) {
529 var currentVolume = this.volumeManager_.getVolumeInfo(
530 this.directoryModel_.getCurrentDirEntry());
531 if (volume !== currentVolume) {
532 // This happens when the current directory is moved during requesting
533 // the file system size. Just ignore it.
536 // sizeStats is undefined, if some error occurs.
537 if (!sizeStats || sizeStats.totalSize == 0)
540 var remainingRatio = sizeStats.remainingSize / sizeStats.totalSize;
541 var isLowDiskSpace = remainingRatio < threshold;
542 if (volume.volumeType === VolumeManagerCommon.VolumeType.DOWNLOADS)
543 this.showLowDownloadsSpaceWarning_(isLowDiskSpace);
545 this.showLowDriveSpaceWarning_(isLowDiskSpace, sizeStats);
550 * removes the Drive Welcome banner.
553 FileListBannerController.prototype.cleanupWelcomeBanner_ = function() {
554 this.showWelcomeBanner_('none');
558 * Notifies the file manager what layout must be recalculated.
559 * @param {number} delay In milliseconds.
562 FileListBannerController.prototype.requestRelayout_ = function(delay) {
564 setTimeout(function() {
565 cr.dispatchSimpleEvent(self, 'relayout');
570 * Show or hide the "Low disk space" warning.
571 * @param {boolean} show True if the box need to be shown.
574 FileListBannerController.prototype.showLowDownloadsSpaceWarning_ =
576 var box = this.document_.querySelector('.downloads-warning');
578 if (box.hidden == !show) return;
581 var html = util.htmlUnescape(str('DOWNLOADS_DIRECTORY_WARNING'));
582 box.innerHTML = html;
583 box.querySelector('a').addEventListener('click', function(e) {
584 util.visitURL(str('DOWNLOADS_LOW_SPACE_WARNING_HELP_URL'));
592 this.requestRelayout_(100);
596 * Creates contents for the DRIVE unmounted panel.
599 FileListBannerController.prototype.ensureDriveUnmountedPanelInitialized_ =
601 var panel = this.unmountedPanel_;
602 if (panel.firstElementChild)
606 * Creates an element using given parameters.
607 * @param {!Element} parent Parent element of the new element.
608 * @param {string} tag Tag of the new element.
609 * @param {string} className Class name of the new element.
610 * @param {string=} opt_textContent Text content of the new element.
611 * @return {!Element} The newly created element.
613 var create = function(parent, tag, className, opt_textContent) {
614 var div = panel.ownerDocument.createElement(tag);
615 div.className = className;
616 div.textContent = opt_textContent || '';
617 parent.appendChild(div);
621 var loading = create(panel, 'div', 'loading', str('DRIVE_LOADING'));
622 var spinnerBox = create(loading, 'div', 'spinner-box');
623 create(spinnerBox, 'div', 'spinner');
624 create(panel, 'div', 'error', str('DRIVE_CANNOT_REACH'));
626 var learnMore = create(panel, 'a', 'learn-more plain-link',
627 str('DRIVE_LEARN_MORE'));
628 learnMore.href = str('GOOGLE_DRIVE_ERROR_HELP_URL');
629 learnMore.target = '_blank';
633 * Called when volume info list is updated.
634 * @param {Event} event Splice event data on volume info list.
637 FileListBannerController.prototype.onVolumeInfoListSplice_ = function(event) {
638 var isDriveVolume = function(volumeInfo) {
639 return volumeInfo.volumeType === VolumeManagerCommon.VolumeType.DRIVE;
641 if (event.removed.some(isDriveVolume) || event.added.some(isDriveVolume))
642 this.updateDriveUnmountedPanel_();
646 * Shows the panel when current directory is DRIVE and it's unmounted.
647 * Hides it otherwise. The panel shows spinner if DRIVE is mounting or
648 * an error message if it failed.
651 FileListBannerController.prototype.updateDriveUnmountedPanel_ = function() {
652 var node = this.document_.body;
653 if (this.isOnCurrentProfileDrive()) {
654 var driveVolume = this.volumeManager_.getCurrentProfileVolumeInfo(
655 VolumeManagerCommon.VolumeType.DRIVE);
656 if (driveVolume && driveVolume.error) {
657 this.ensureDriveUnmountedPanelInitialized_();
658 this.unmountedPanel_.classList.add('retry-enabled');
660 this.unmountedPanel_.classList.remove('retry-enabled');
662 node.setAttribute('drive', status);
664 node.removeAttribute('drive');
669 * Updates the visibility of Drive Connection Warning banner, retrieving the
670 * current connection information.
673 FileListBannerController.prototype.maybeShowAuthFailBanner_ = function() {
674 var connection = this.volumeManager_.getDriveConnectionState();
675 var showDriveNotReachedMessage =
676 this.isOnCurrentProfileDrive() &&
677 connection.type == VolumeManagerCommon.DriveConnectionType.OFFLINE &&
678 connection.reason == VolumeManagerCommon.DriveConnectionReason.NOT_READY;
679 this.authFailedBanner_.hidden = !showDriveNotReachedMessage;