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.
5 cr
.define('ntp', function() {
9 // The histogram buckets (keep in sync with extension_constants.h).
10 NTP_APPS_MAXIMIZED
: 0,
11 NTP_APPS_COLLAPSED
: 1,
14 NTP_RECENTLY_CLOSED
: 4,
15 NTP_APP_RE_ENABLE
: 16,
16 NTP_WEBSTORE_FOOTER
: 18,
17 NTP_WEBSTORE_PLUS_ICON
: 19,
20 // Histogram buckets for UMA tracking of where a DnD drop came from.
28 var DRAG_SOURCE_LIMIT
= DRAG_SOURCE
.OUTSIDE_NTP
+ 1;
31 * App context menu. The class is designed to be used as a singleton with
32 * the app that is currently showing a context menu stored in this.app_.
35 function AppContextMenu() {
36 this.__proto__
= AppContextMenu
.prototype;
39 cr
.addSingletonGetter(AppContextMenu
);
41 AppContextMenu
.prototype = {
42 initialize: function() {
43 var menu
= new cr
.ui
.Menu
;
44 cr
.ui
.decorate(menu
, cr
.ui
.Menu
);
45 menu
.classList
.add('app-context-menu');
48 this.launch_
= this.appendMenuItem_();
49 this.launch_
.addEventListener('activate', this.onLaunch_
.bind(this));
51 menu
.appendChild(cr
.ui
.MenuItem
.createSeparator());
52 this.launchRegularTab_
= this.appendMenuItem_('applaunchtyperegular');
53 this.launchPinnedTab_
= this.appendMenuItem_('applaunchtypepinned');
54 if (loadTimeData
.getBoolean('enableNewBookmarkApps') || !cr
.isMac
)
55 this.launchNewWindow_
= this.appendMenuItem_('applaunchtypewindow');
56 this.launchFullscreen_
= this.appendMenuItem_('applaunchtypefullscreen');
59 this.forAllLaunchTypes_(function(launchTypeButton
, id
) {
60 launchTypeButton
.addEventListener('activate',
61 self
.onLaunchTypeChanged_
.bind(self
));
64 this.launchTypeMenuSeparator_
= cr
.ui
.MenuItem
.createSeparator();
65 menu
.appendChild(this.launchTypeMenuSeparator_
);
66 this.options_
= this.appendMenuItem_('appoptions');
67 this.uninstall_
= this.appendMenuItem_('appuninstall');
69 if (loadTimeData
.getBoolean('canShowAppInfoDialog')) {
70 this.appinfo_
= this.appendMenuItem_('appinfodialog');
71 this.appinfo_
.addEventListener('activate',
72 this.onShowAppInfo_
.bind(this));
74 this.details_
= this.appendMenuItem_('appdetails');
75 this.details_
.addEventListener('activate',
76 this.onShowDetails_
.bind(this));
79 this.options_
.addEventListener('activate',
80 this.onShowOptions_
.bind(this));
81 this.uninstall_
.addEventListener('activate',
82 this.onUninstall_
.bind(this));
85 this.createShortcutSeparator_
=
86 menu
.appendChild(cr
.ui
.MenuItem
.createSeparator());
87 this.createShortcut_
= this.appendMenuItem_('appcreateshortcut');
88 this.createShortcut_
.addEventListener(
89 'activate', this.onCreateShortcut_
.bind(this));
92 document
.body
.appendChild(menu
);
96 * Appends a menu item to |this.menu|.
97 * @param {string=} opt_textId If defined, the ID for the localized string
98 * that acts as the item's label.
100 appendMenuItem_: function(opt_textId
) {
101 var button
= cr
.doc
.createElement('button');
102 this.menu
.appendChild(button
);
103 cr
.ui
.decorate(button
, cr
.ui
.MenuItem
);
105 button
.textContent
= loadTimeData
.getString(opt_textId
);
110 * Iterates over all the launch type menu items.
111 * @param {function(cr.ui.MenuItem, number)} f The function to call for each
112 * menu item. The parameters to the function include the menu item and
113 * the associated launch ID.
115 forAllLaunchTypes_: function(f
) {
116 // Order matters: index matches launchType id.
117 var launchTypes
= [this.launchPinnedTab_
,
118 this.launchRegularTab_
,
119 this.launchFullscreen_
,
120 this.launchNewWindow_
];
122 for (var i
= 0; i
< launchTypes
.length
; ++i
) {
126 f(launchTypes
[i
], i
);
131 * Does all the necessary setup to show the menu for the given app.
132 * @param {App} app The App object that will be showing a context menu.
134 setupForApp: function(app
) {
137 this.launch_
.textContent
= app
.appData
.title
;
139 var launchTypeWindow
= this.launchNewWindow_
;
140 this.forAllLaunchTypes_(function(launchTypeButton
, id
) {
141 launchTypeButton
.disabled
= false;
142 launchTypeButton
.checked
= app
.appData
.launch_type
== id
;
143 // If bookmark apps are enabled, only show the "Open as window" button.
144 launchTypeButton
.hidden
= app
.appData
.packagedApp
||
145 (loadTimeData
.getBoolean('enableNewBookmarkApps') &&
146 launchTypeButton
!= launchTypeWindow
);
149 this.launchTypeMenuSeparator_
.hidden
= app
.appData
.packagedApp
;
151 this.options_
.disabled
= !app
.appData
.optionsUrl
|| !app
.appData
.enabled
;
153 this.details_
.disabled
= !app
.appData
.detailsUrl
;
154 this.uninstall_
.disabled
= !app
.appData
.mayDisable
;
157 // On Windows and Linux, these should always be visible. On ChromeOS,
158 // they are never created. On Mac, shortcuts can only be created for
159 // new-style packaged apps, so hide the menu item.
160 this.createShortcutSeparator_
.hidden
= this.createShortcut_
.hidden
=
161 !app
.appData
.packagedApp
;
166 * Handlers for menu item activation.
167 * @param {Event} e The activation event.
170 onLaunch_: function(e
) {
171 chrome
.send('launchApp', [this.app_
.appId
, APP_LAUNCH
.NTP_APPS_MENU
]);
173 onLaunchTypeChanged_: function(e
) {
174 var pressed
= e
.currentTarget
;
176 var targetLaunchType
= pressed
;
177 // When bookmark apps are enabled, hosted apps can only toggle between
178 // open as window and open as tab.
179 if (loadTimeData
.getBoolean('enableNewBookmarkApps')) {
180 targetLaunchType
= this.launchNewWindow_
.checked
?
181 this.launchRegularTab_
: this.launchNewWindow_
;
183 this.forAllLaunchTypes_(function(launchTypeButton
, id
) {
184 if (launchTypeButton
== targetLaunchType
) {
185 chrome
.send('setLaunchType', [app
.appId
, id
]);
186 // Manually update the launch type. We will only get
187 // appsPrefChangeCallback calls after changes to other NTP instances.
188 app
.appData
.launch_type
= id
;
192 onShowOptions_: function(e
) {
193 window
.location
= this.app_
.appData
.optionsUrl
;
195 onShowDetails_: function(e
) {
196 var url
= this.app_
.appData
.detailsUrl
;
197 url
= appendParam(url
, 'utm_source', 'chrome-ntp-launcher');
198 window
.location
= url
;
200 onUninstall_: function(e
) {
201 chrome
.send('uninstallApp', [this.app_
.appData
.id
]);
203 onCreateShortcut_: function(e
) {
204 chrome
.send('createAppShortcut', [this.app_
.appData
.id
]);
206 onShowAppInfo_: function(e
) {
207 chrome
.send('showAppInfo', [this.app_
.appData
.id
]);
212 * Creates a new App object.
213 * @param {Object} appData The data object that describes the app.
215 * @extends {HTMLDivElement}
217 function App(appData
) {
218 var el
= cr
.doc
.createElement('div');
219 el
.__proto__
= App
.prototype;
220 el
.initialize(appData
);
226 __proto__
: HTMLDivElement
.prototype,
229 * Initialize the app object.
230 * @param {Object} appData The data object that describes the app.
232 initialize: function(appData
) {
233 this.appData
= appData
;
234 assert(this.appData_
.id
, 'Got an app without an ID');
235 this.id
= this.appData_
.id
;
236 this.setAttribute('role', 'menuitem');
238 this.className
= 'app focusable';
240 if (!this.appData_
.icon_big_exists
&& this.appData_
.icon_small_exists
)
241 this.useSmallIcon_
= true;
243 this.appContents_
= this.useSmallIcon_
?
244 $('app-small-icon-template').cloneNode(true) :
245 $('app-large-icon-template').cloneNode(true);
246 this.appContents_
.id
= '';
247 this.appendChild(this.appContents_
);
249 this.appImgContainer_
= /** @type {HTMLElement} */(
250 this.querySelector('.app-img-container'));
251 this.appImg_
= this.appImgContainer_
.querySelector('img');
254 if (this.useSmallIcon_
) {
255 this.imgDiv_
= /** @type {HTMLElement} */(
256 this.querySelector('.app-icon-div'));
257 this.addLaunchClickTarget_(this.imgDiv_
);
258 this.imgDiv_
.title
= this.appData_
.full_name
;
259 chrome
.send('getAppIconDominantColor', [this.id
]);
261 this.addLaunchClickTarget_(this.appImgContainer_
);
262 this.appImgContainer_
.title
= this.appData_
.full_name
;
265 // The app's full name is shown in the tooltip, whereas the short name
266 // is used for the label.
267 var appSpan
= /** @type {HTMLElement} */(
268 this.appContents_
.querySelector('.title'));
269 appSpan
.textContent
= this.appData_
.title
;
270 appSpan
.title
= this.appData_
.full_name
;
271 this.addLaunchClickTarget_(appSpan
);
273 this.addEventListener('keydown', cr
.ui
.contextMenuHandler
);
274 this.addEventListener('keyup', cr
.ui
.contextMenuHandler
);
276 // This hack is here so that appContents.contextMenu will be the same as
279 this.appContents_
.__defineGetter__('contextMenu', function() {
280 return self
.contextMenu
;
282 this.appContents_
.addEventListener('contextmenu',
283 cr
.ui
.contextMenuHandler
);
285 this.addEventListener('mousedown', this.onMousedown_
, true);
286 this.addEventListener('keydown', this.onKeydown_
);
287 this.addEventListener('keyup', this.onKeyup_
);
291 * Sets the color of the favicon dominant color bar.
292 * @param {string} color The css-parsable value for the color.
294 set stripeColor(color
) {
295 this.querySelector('.color-stripe').style
.backgroundColor
= color
;
299 * Removes the app tile from the page. Should be called after the app has
302 remove: function(opt_animate
) {
303 // Unset the ID immediately, because the app is already gone. But leave
304 // the tile on the page as it animates out.
306 this.tile
.doRemove(opt_animate
);
310 * Set the URL of the icon from |appData_|. This won't actually show the
311 * icon until loadIcon() is called (for performance reasons; we don't want
312 * to load icons until we have to).
314 setIcon: function() {
315 var src
= this.useSmallIcon_
? this.appData_
.icon_small
:
316 this.appData_
.icon_big
;
317 if (!this.appData_
.enabled
||
318 (!this.appData_
.offlineEnabled
&& !navigator
.onLine
)) {
319 src
+= '?grayscale=true';
322 this.appImgSrc_
= src
;
323 this.classList
.add('icon-loading');
327 * Shows the icon for the app. That is, it causes chrome to load the app
330 loadIcon: function() {
331 if (this.appImgSrc_
) {
332 this.appImg_
.src
= this.appImgSrc_
;
333 this.appImg_
.classList
.remove('invisible');
334 this.appImgSrc_
= null;
337 this.classList
.remove('icon-loading');
341 * Set the size and position of the app tile.
342 * @param {number} size The total size of |this|.
343 * @param {number} x The x-position.
344 * @param {number} y The y-position.
347 setBounds: function(size
, x
, y
) {
348 var imgSize
= size
* APP_IMG_SIZE_FRACTION
;
349 this.appImgContainer_
.style
.width
= this.appImgContainer_
.style
.height
=
350 toCssPx(this.useSmallIcon_
? 16 : imgSize
);
351 if (this.useSmallIcon_
) {
352 // 3/4 is the ratio of 96px to 128px (the used height and full height
353 // of icons in apps).
354 var iconSize
= imgSize
* 3 / 4;
355 // The -2 is for the div border to improve the visual alignment for the
357 this.imgDiv_
.style
.width
= this.imgDiv_
.style
.height
=
358 toCssPx(iconSize
- 2);
359 // Margins set to get the icon placement right and the text to line up.
360 this.imgDiv_
.style
.marginTop
= this.imgDiv_
.style
.marginBottom
=
361 toCssPx((imgSize
- iconSize
) / 2);
364 this.style
.width
= this.style
.height
= toCssPx(size
);
365 this.style
.left
= toCssPx(x
);
366 this.style
.right
= toCssPx(x
);
367 this.style
.top
= toCssPx(y
);
371 * Invoked when an app is clicked.
372 * @param {Event} e The click event.
375 onClick_: function(e
) {
376 var url
= !this.appData_
.is_webstore
? '' :
377 appendParam(this.appData_
.url
,
381 chrome
.send('launchApp',
382 [this.appId
, APP_LAUNCH
.NTP_APPS_MAXIMIZED
, url
,
383 e
.button
, e
.altKey
, e
.ctrlKey
, e
.metaKey
, e
.shiftKey
]);
385 // Don't allow the click to trigger a link or anything
390 * Invoked when the user presses a key while the app is focused.
391 * @param {Event} e The key event.
394 onKeydown_: function(e
) {
395 if (e
.keyIdentifier
== 'Enter') {
396 chrome
.send('launchApp',
397 [this.appId
, APP_LAUNCH
.NTP_APPS_MAXIMIZED
, '',
398 0, e
.altKey
, e
.ctrlKey
, e
.metaKey
, e
.shiftKey
]);
402 this.onKeyboardUsed_(e
.keyCode
);
406 * Invoked when the user releases a key while the app is focused.
407 * @param {Event} e The key event.
410 onKeyup_: function(e
) {
411 this.onKeyboardUsed_(e
.keyCode
);
415 * Called when the keyboard has been used (key down or up). The .click-focus
416 * hack is removed if the user presses a key that can change focus.
417 * @param {number} keyCode The key code of the keyboard event.
420 onKeyboardUsed_: function(keyCode
) {
423 case 37: // Left arrow.
424 case 38: // Up arrow.
425 case 39: // Right arrow.
426 case 40: // Down arrow.
427 this.classList
.remove('click-focus');
432 * Adds a node to the list of targets that will launch the app. This list
433 * is also used in onMousedown to determine whether the app contents should
434 * be shown as active (if we don't do this, then clicking anywhere in
435 * appContents, even a part that is outside the ideally clickable region,
436 * will cause the app icon to look active).
437 * @param {HTMLElement} node The node that should be clickable.
439 addLaunchClickTarget_: function(node
) {
440 node
.classList
.add('launch-click-target');
441 node
.addEventListener('click', this.onClick_
.bind(this));
445 * Handler for mousedown on the App. Adds a class that allows us to
446 * not display as :active for right clicks (specifically, don't pulse on
447 * these occasions). Also, we don't pulse for clicks that aren't within the
449 * @param {Event} e The mousedown event.
451 onMousedown_: function(e
) {
452 // If the current platform uses middle click to autoscroll and this
453 // mousedown isn't handled, onClick_() will never fire. crbug.com/142939
458 !findAncestorByClass(/** @type {Element} */(e
.target
),
459 'launch-click-target')) {
460 this.appContents_
.classList
.add('suppress-active');
462 this.appContents_
.classList
.remove('suppress-active');
465 // This class is here so we don't show the focus state for apps that
466 // gain keyboard focus via mouse clicking.
467 this.classList
.add('click-focus');
471 * Change the appData and update the appearance of the app.
472 * @param {AppInfo} appData The new data object that describes the app.
474 replaceAppData: function(appData
) {
475 this.appData_
= appData
;
481 * The data and preferences for this app.
485 this.appData_
= data
;
488 return this.appData_
;
492 return this.appData_
.id
;
496 * Returns a pointer to the context menu for this app. All apps share the
497 * singleton AppContextMenu. This function is called by the
498 * ContextMenuHandler in response to the 'contextmenu' event.
502 var menu
= AppContextMenu
.getInstance();
503 menu
.setupForApp(this);
508 * Returns whether this element can be 'removed' from chrome (i.e. whether
509 * the user can drag it onto the trash and expect something to happen).
510 * @return {boolean} True if the app can be uninstalled.
512 canBeRemoved: function() {
513 return this.appData_
.mayDisable
;
517 * Uninstalls the app after it's been dropped on the trash.
519 removeFromChrome: function() {
520 chrome
.send('uninstallApp', [this.appData_
.id
, true]);
521 this.tile
.tilePage
.removeTile(this.tile
, true);
525 * Called when a drag is starting on the tile. Updates dataTransfer with
526 * data for this tile.
528 setDragData: function(dataTransfer
) {
529 dataTransfer
.setData('Text', this.appData_
.title
);
530 dataTransfer
.setData('URL', this.appData_
.url
);
534 var TilePage
= ntp
.TilePage
;
536 // The fraction of the app tile size that the icon uses.
537 var APP_IMG_SIZE_FRACTION
= 4 / 5;
539 var appsPageGridValues
= {
540 // The fewest tiles we will show in a row.
542 // The most tiles we will show in a row.
545 // The smallest a tile can be.
546 minTileWidth
: 64 / APP_IMG_SIZE_FRACTION
,
547 // The biggest a tile can be.
548 maxTileWidth
: 128 / APP_IMG_SIZE_FRACTION
,
550 // The padding between tiles, as a fraction of the tile width.
551 tileSpacingFraction
: 1 / 8,
553 TilePage
.initGridValues(appsPageGridValues
);
556 * Creates a new AppsPage object.
558 * @extends {TilePage}
560 function AppsPage() {
561 var el
= new TilePage(appsPageGridValues
);
562 el
.__proto__
= AppsPage
.prototype;
568 AppsPage
.prototype = {
569 __proto__
: TilePage
.prototype,
571 initialize: function() {
572 this.classList
.add('apps-page');
574 this.addEventListener('cardselected', this.onCardSelected_
);
576 this.addEventListener('tilePage:tile_added', this.onTileAdded_
);
578 this.content_
.addEventListener('scroll', this.onScroll_
.bind(this));
582 * Highlight a newly installed app as it's added to the NTP.
583 * @param {AppInfo} appData The data object that describes the app.
585 insertAndHighlightApp: function(appData
) {
586 ntp
.getCardSlider().selectCardByValue(this);
587 this.content_
.scrollTop
= this.content_
.scrollHeight
;
588 this.insertApp(appData
, true);
592 * Similar to appendApp, but it respects the app_launch_ordinal field of
594 * @param {Object} appData The data that describes the app.
595 * @param {boolean} animate Whether to animate the insertion.
597 insertApp: function(appData
, animate
) {
598 var index
= this.tileElements_
.length
;
599 for (var i
= 0; i
< this.tileElements_
.length
; i
++) {
600 if (appData
.app_launch_ordinal
<
601 this.tileElements_
[i
].firstChild
.appData
.app_launch_ordinal
) {
607 this.addTileAt(new App(appData
), index
, animate
);
611 * Handler for 'cardselected' event, fired when |this| is selected. The
612 * first time this is called, we load all the app icons.
615 onCardSelected_: function(e
) {
616 var apps
= this.querySelectorAll('.app.icon-loading');
617 for (var i
= 0; i
< apps
.length
; i
++) {
623 * Handler for tile additions to this page.
624 * @param {Event} e The tilePage:tile_added event.
626 onTileAdded_: function(e
) {
627 assert(e
.currentTarget
== this);
628 assert(e
.addedTile
.firstChild
instanceof App
);
629 if (this.classList
.contains('selected-card'))
630 e
.addedTile
.firstChild
.loadIcon();
634 * A handler for when the apps page is scrolled (then we need to reposition
638 onScroll_: function(e
) {
641 for (var i
= 0; i
< this.tileElements_
.length
; i
++) {
642 var app
= this.tileElements_
[i
].firstChild
;
643 assert(app
instanceof App
);
648 doDragOver: function(e
) {
649 // Only animatedly re-arrange if the user is currently dragging an app.
650 var tile
= ntp
.getCurrentlyDraggingTile();
651 if (tile
&& tile
.querySelector('.app')) {
652 TilePage
.prototype.doDragOver
.call(this, e
);
655 this.setDropEffect(e
.dataTransfer
);
660 shouldAcceptDrag: function(e
) {
661 if (ntp
.getCurrentlyDraggingTile())
663 if (!e
.dataTransfer
|| !e
.dataTransfer
.types
)
665 return Array
.prototype.indexOf
.call(e
.dataTransfer
.types
,
666 'text/uri-list') != -1;
670 addDragData: function(dataTransfer
, index
) {
672 var currentlyDraggingTile
= ntp
.getCurrentlyDraggingTile();
673 if (currentlyDraggingTile
) {
674 var tileContents
= currentlyDraggingTile
.firstChild
;
675 if (tileContents
.classList
.contains('app')) {
676 var originalPage
= currentlyDraggingTile
.tilePage
;
677 var samePageDrag
= originalPage
== this;
678 sourceId
= samePageDrag
? DRAG_SOURCE
.SAME_APPS_PANE
:
679 DRAG_SOURCE
.OTHER_APPS_PANE
;
680 this.tileGrid_
.insertBefore(currentlyDraggingTile
,
681 this.tileElements_
[index
]);
682 this.tileMoved(currentlyDraggingTile
);
684 originalPage
.fireRemovedEvent(currentlyDraggingTile
, index
, true);
685 this.fireAddedEvent(currentlyDraggingTile
, index
, true);
687 } else if (currentlyDraggingTile
.querySelector('.most-visited')) {
688 this.generateAppForLink(tileContents
.data
);
689 sourceId
= DRAG_SOURCE
.MOST_VISITED_PANE
;
692 this.addOutsideData_(dataTransfer
);
693 sourceId
= DRAG_SOURCE
.OUTSIDE_NTP
;
696 assert(sourceId
!= -1);
697 chrome
.send('metricsHandler:recordInHistogram',
698 ['NewTabPage.AppsPageDragSource', sourceId
, DRAG_SOURCE_LIMIT
]);
702 * Adds drag data that has been dropped from a source that is not a tile.
703 * @param {Object} dataTransfer The data transfer object that holds drop
707 addOutsideData_: function(dataTransfer
) {
708 var url
= dataTransfer
.getData('url');
711 // If the dataTransfer has html data, use that html's text contents as the
712 // title of the new link.
713 var html
= dataTransfer
.getData('text/html');
716 // It's important that we don't attach this node to the document
717 // because it might contain scripts.
718 var node
= this.ownerDocument
.createElement('div');
719 node
.innerHTML
= html
;
720 title
= node
.textContent
;
723 // Make sure title is >=1 and <=45 characters for Chrome app limits.
726 if (title
.length
> 45)
727 title
= title
.substring(0, 45);
728 var data
= {url
: url
, title
: title
};
730 // Synthesize an app.
731 this.generateAppForLink(data
);
735 * Creates a new crx-less app manifest and installs it.
736 * @param {Object} data The data object describing the link. Must have |url|
737 * and |title| members.
739 generateAppForLink: function(data
) {
740 assert(data
.url
!= undefined);
741 assert(data
.title
!= undefined);
742 var pageIndex
= ntp
.getAppsPageIndex(this);
743 chrome
.send('generateAppForLink', [data
.url
, data
.title
, pageIndex
]);
747 tileMoved: function(draggedTile
) {
748 if (!(draggedTile
.firstChild
instanceof App
))
751 var pageIndex
= ntp
.getAppsPageIndex(this);
752 chrome
.send('setPageIndex', [draggedTile
.firstChild
.appId
, pageIndex
]);
755 for (var i
= 0; i
< this.tileElements_
.length
; i
++) {
756 var tileContents
= this.tileElements_
[i
].firstChild
;
757 if (tileContents
instanceof App
)
758 appIds
.push(tileContents
.appId
);
761 chrome
.send('reorderApps', [draggedTile
.firstChild
.appId
, appIds
]);
765 setDropEffect: function(dataTransfer
) {
766 var tile
= ntp
.getCurrentlyDraggingTile();
767 if (tile
&& tile
.querySelector('.app'))
768 ntp
.setCurrentDropEffect(dataTransfer
, 'move');
770 ntp
.setCurrentDropEffect(dataTransfer
, 'copy');
775 * Launches the specified app using the APP_LAUNCH_NTP_APP_RE_ENABLE
776 * histogram. This should only be invoked from the AppLauncherHandler.
777 * @param {string} appId The ID of the app.
779 function launchAppAfterEnable(appId
) {
780 chrome
.send('launchApp', [appId
, APP_LAUNCH
.NTP_APP_RE_ENABLE
]);
784 APP_LAUNCH
: APP_LAUNCH
,
786 launchAppAfterEnable
: launchAppAfterEnable
,