1
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
3
const Lang = imports.lang;
4
const Gio = imports.gi.Gio;
5
const St = imports.gi.St;
7
const GnomeSession = imports.misc.gnomeSession;
8
const Main = imports.ui.main;
9
const MessageTray = imports.ui.messageTray;
10
const ShellMountOperation = imports.ui.shellMountOperation;
13
const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
14
const SETTING_DISABLE_AUTORUN = 'autorun-never';
15
const SETTING_START_APP = 'autorun-x-content-start-app';
16
const SETTING_IGNORE = 'autorun-x-content-ignore';
17
const SETTING_OPEN_FOLDER = 'autorun-x-content-open-folder';
19
const AutorunSetting = {
27
function shouldAutorunMount(mount, forTransient) {
28
let root = mount.get_root();
29
let volume = mount.get_volume();
31
if (!volume || (!volume.allowAutorun && forTransient))
34
if (root.is_native() && isMountRootHidden(root))
40
function isMountRootHidden(root) {
41
let path = root.get_path();
43
// skip any mounts in hidden directory hierarchies
44
return (path.indexOf('/.') != -1);
47
function isMountNonLocal(mount) {
48
// If the mount doesn't have an associated volume, that means it's
49
// an uninteresting filesystem. Most devices that we care about will
50
// have a mount, like media players and USB sticks.
51
let volume = mount.get_volume();
55
return (volume.get_identifier("class") == "network");
58
function startAppForMount(app, mount) {
60
let root = mount.get_root();
66
retval = app.launch(files,
67
global.create_app_launch_context())
69
log('Unable to launch the application ' + app.get_name()
70
+ ': ' + e.toString());
76
/******************************************/
78
const HotplugSnifferIface = <interface name="org.gnome.Shell.HotplugSniffer">
79
<method name="SniffURI">
80
<arg type="s" direction="in" />
81
<arg type="as" direction="out" />
85
const HotplugSnifferProxy = Gio.DBusProxy.makeProxyWrapper(HotplugSnifferIface);
86
function HotplugSniffer() {
87
return new HotplugSnifferProxy(Gio.DBus.session,
88
'org.gnome.Shell.HotplugSniffer',
89
'/org/gnome/Shell/HotplugSniffer');
92
const ContentTypeDiscoverer = new Lang.Class({
93
Name: 'ContentTypeDiscoverer',
95
_init: function(callback) {
96
this._callback = callback;
97
this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA });
100
guessContentTypes: function(mount) {
101
let autorunEnabled = !this._settings.get_boolean(SETTING_DISABLE_AUTORUN);
102
let shouldScan = autorunEnabled && !isMountNonLocal(mount);
105
// guess mount's content types using GIO
106
mount.guess_content_type(false, null,
108
this._onContentTypeGuessed));
110
this._emitCallback(mount, []);
114
_onContentTypeGuessed: function(mount, res) {
115
let contentTypes = [];
118
contentTypes = mount.guess_content_type_finish(res);
120
log('Unable to guess content types on added mount ' + mount.get_name()
121
+ ': ' + e.toString());
124
if (contentTypes.length) {
125
this._emitCallback(mount, contentTypes);
127
let root = mount.get_root();
129
let hotplugSniffer = new HotplugSniffer();
130
hotplugSniffer.SniffURIRemote(root.get_uri(),
131
Lang.bind(this, function([contentTypes]) {
132
this._emitCallback(mount, contentTypes);
137
_emitCallback: function(mount, contentTypes) {
141
// we're not interested in win32 software content types here
142
contentTypes = contentTypes.filter(function(type) {
143
return (type != 'x-content/win32-software');
147
contentTypes.forEach(function(type) {
148
let app = Gio.app_info_get_default_for_type(type, false);
154
if (apps.length == 0)
155
apps.push(Gio.app_info_get_default_for_type('inode/directory', false));
157
this._callback(mount, apps, contentTypes);
161
const AutorunManager = new Lang.Class({
162
Name: 'AutorunManager',
165
this._session = new GnomeSession.SessionManager();
166
this._volumeMonitor = Gio.VolumeMonitor.get();
168
this._transDispatcher = new AutorunTransientDispatcher(this);
171
_ensureResidentSource: function() {
172
if (this._residentSource)
175
this._residentSource = new AutorunResidentSource(this);
176
let destroyId = this._residentSource.connect('destroy', Lang.bind(this, function() {
177
this._residentSource.disconnect(destroyId);
178
this._residentSource = null;
185
this._mountAddedId = this._volumeMonitor.connect('mount-added', Lang.bind(this, this._onMountAdded));
186
this._mountRemovedId = this._volumeMonitor.connect('mount-removed', Lang.bind(this, this._onMountRemoved));
189
disable: function() {
190
if (this._residentSource)
191
this._residentSource.destroy();
192
this._volumeMonitor.disconnect(this._mountAddedId);
193
this._volumeMonitor.disconnect(this._mountRemovedId);
196
_processMount: function(mount, hotplug) {
197
let discoverer = new ContentTypeDiscoverer(Lang.bind(this, function(mount, apps, contentTypes) {
198
this._ensureResidentSource();
199
this._residentSource.addMount(mount, apps);
202
this._transDispatcher.addMount(mount, apps, contentTypes);
204
discoverer.guessContentTypes(mount);
207
_scanMounts: function() {
208
let mounts = this._volumeMonitor.get_mounts();
209
mounts.forEach(Lang.bind(this, function(mount) {
210
this._processMount(mount, false);
214
_onMountAdded: function(monitor, mount) {
215
// don't do anything if our session is not the currently
217
if (!this._session.SessionIsActive)
220
this._processMount(mount, true);
223
_onMountRemoved: function(monitor, mount) {
224
this._transDispatcher.removeMount(mount);
225
if (this._residentSource)
226
this._residentSource.removeMount(mount);
229
ejectMount: function(mount) {
230
let mountOp = new ShellMountOperation.ShellMountOperation(mount);
232
// first, see if we have a drive
233
let drive = mount.get_drive();
234
let volume = mount.get_volume();
237
drive.get_start_stop_type() == Gio.DriveStartStopType.SHUTDOWN &&
239
drive.stop(0, mountOp.mountOp, null,
240
Lang.bind(this, this._onStop));
242
if (mount.can_eject()) {
243
mount.eject_with_operation(0, mountOp.mountOp, null,
244
Lang.bind(this, this._onEject));
245
} else if (volume && volume.can_eject()) {
246
volume.eject_with_operation(0, mountOp.mountOp, null,
247
Lang.bind(this, this._onEject));
248
} else if (drive && drive.can_eject()) {
249
drive.eject_with_operation(0, mountOp.mountOp, null,
250
Lang.bind(this, this._onEject));
251
} else if (mount.can_unmount()) {
252
mount.unmount_with_operation(0, mountOp.mountOp, null,
253
Lang.bind(this, this._onUnmount));
258
_onUnmount: function(mount, res) {
260
mount.unmount_with_operation_finish(res);
262
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
263
log('Unable to eject the mount ' + mount.get_name()
264
+ ': ' + e.toString());
268
_onEject: function(source, res) {
270
source.eject_with_operation_finish(res);
272
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
273
log('Unable to eject the drive ' + source.get_name()
274
+ ': ' + e.toString());
278
_onStop: function(drive, res) {
280
drive.stop_finish(res);
282
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
283
log('Unable to stop the drive ' + drive.get_name()
284
+ ': ' + e.toString());
289
const AutorunResidentSource = new Lang.Class({
290
Name: 'AutorunResidentSource',
291
Extends: MessageTray.Source,
293
_init: function(manager) {
294
this.parent(_("Removable Devices"), 'media-removable');
295
this.resident = true;
299
this._manager = manager;
300
this._notification = new AutorunResidentNotification(this._manager, this);
303
_createPolicy: function() {
304
return new MessageTray.NotificationPolicy({ showInLockScreen: false });
307
buildRightClickMenu: function() {
311
addMount: function(mount, apps) {
312
if (!shouldAutorunMount(mount, false))
315
let filtered = this._mounts.filter(function (element) {
316
return (element.mount == mount);
319
if (filtered.length != 0)
322
let element = { mount: mount, apps: apps };
323
this._mounts.push(element);
327
removeMount: function(mount) {
329
this._mounts.filter(function (element) {
330
return (element.mount != mount);
336
_redisplay: function() {
337
if (this._mounts.length == 0) {
338
this._notification.destroy();
344
this._notification.updateForMounts(this._mounts);
346
// add ourselves as a source, and push the notification
347
if (!Main.messageTray.contains(this)) {
348
Main.messageTray.add(this);
349
this.pushNotification(this._notification);
354
const AutorunResidentNotification = new Lang.Class({
355
Name: 'AutorunResidentNotification',
356
Extends: MessageTray.Notification,
358
_init: function(manager, source) {
359
this.parent(source, source.title, null, { customContent: true });
361
// set the notification as resident
362
this.setResident(true);
364
this._layout = new St.BoxLayout ({ style_class: 'hotplug-resident-box',
366
this._manager = manager;
368
this.addActor(this._layout,
373
updateForMounts: function(mounts) {
374
// remove all the layout content
375
this._layout.destroy_all_children();
377
for (let idx = 0; idx < mounts.length; idx++) {
378
let element = mounts[idx];
380
let actor = this._itemForMount(element.mount, element.apps);
381
this._layout.add(actor, { x_fill: true,
386
_itemForMount: function(mount, apps) {
387
let item = new St.BoxLayout();
389
// prepare the mount button content
390
let mountLayout = new St.BoxLayout();
392
let mountIcon = new St.Icon({ gicon: mount.get_icon(),
393
style_class: 'hotplug-resident-mount-icon' });
394
mountLayout.add_actor(mountIcon);
396
let labelBin = new St.Bin({ y_align: St.Align.MIDDLE });
398
new St.Label({ text: mount.get_name(),
399
style_class: 'hotplug-resident-mount-label',
402
labelBin.add_actor(mountLabel);
403
mountLayout.add_actor(labelBin);
405
let mountButton = new St.Button({ child: mountLayout,
406
x_align: St.Align.START,
408
style_class: 'hotplug-resident-mount',
409
button_mask: St.ButtonMask.ONE });
410
item.add(mountButton, { x_align: St.Align.START,
414
new St.Icon({ icon_name: 'media-eject-symbolic',
415
style_class: 'hotplug-resident-eject-icon' });
418
new St.Button({ style_class: 'hotplug-resident-eject-button',
419
button_mask: St.ButtonMask.ONE,
421
item.add(ejectButton, { x_align: St.Align.END });
423
// now connect signals
424
mountButton.connect('clicked', Lang.bind(this, function(actor, event) {
425
startAppForMount(apps[0], mount);
428
ejectButton.connect('clicked', Lang.bind(this, function() {
429
this._manager.ejectMount(mount);
436
const AutorunTransientDispatcher = new Lang.Class({
437
Name: 'AutorunTransientDispatcher',
439
_init: function(manager) {
440
this._manager = manager;
442
this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA });
445
_getAutorunSettingForType: function(contentType) {
446
let runApp = this._settings.get_strv(SETTING_START_APP);
447
if (runApp.indexOf(contentType) != -1)
448
return AutorunSetting.RUN;
450
let ignore = this._settings.get_strv(SETTING_IGNORE);
451
if (ignore.indexOf(contentType) != -1)
452
return AutorunSetting.IGNORE;
454
let openFiles = this._settings.get_strv(SETTING_OPEN_FOLDER);
455
if (openFiles.indexOf(contentType) != -1)
456
return AutorunSetting.FILES;
458
return AutorunSetting.ASK;
461
_getSourceForMount: function(mount) {
463
this._sources.filter(function (source) {
464
return (source.mount == mount);
467
// we always make sure not to add two sources for the same
468
// mount in addMount(), so it's safe to assume filtered.length
469
// is always either 1 or 0.
470
if (filtered.length == 1)
476
_addSource: function(mount, apps) {
477
// if we already have a source showing for this
479
if (this._getSourceForMount(mount))
483
this._sources.push(new AutorunTransientSource(this._manager, mount, apps));
486
addMount: function(mount, apps, contentTypes) {
487
// if autorun is disabled globally, return
488
if (this._settings.get_boolean(SETTING_DISABLE_AUTORUN))
491
// if the mount doesn't want to be autorun, return
492
if (!shouldAutorunMount(mount, true))
495
let setting = this._getAutorunSettingForType(contentTypes[0]);
497
// check at the settings for the first content type
498
// to see whether we should ask
499
if (setting == AutorunSetting.IGNORE)
500
return; // return right away
505
if (setting == AutorunSetting.RUN) {
506
app = Gio.app_info_get_default_for_type(contentTypes[0], false);
507
} else if (setting == AutorunSetting.FILES) {
508
app = Gio.app_info_get_default_for_type('inode/directory', false);
512
success = startAppForMount(app, mount);
514
// we fallback here also in case the settings did not specify 'ask',
515
// but we failed launching the default app or the default file manager
517
this._addSource(mount, apps);
520
removeMount: function(mount) {
521
let source = this._getSourceForMount(mount);
523
// if we aren't tracking this mount, don't do anything
527
// destroy the notification source
532
const AutorunTransientSource = new Lang.Class({
533
Name: 'AutorunTransientSource',
534
Extends: MessageTray.Source,
536
_init: function(manager, mount, apps) {
537
this._manager = manager;
541
this.parent(mount.get_name());
543
this._notification = new AutorunTransientNotification(this._manager, this);
545
// add ourselves as a source, and popup the notification
546
Main.messageTray.add(this);
547
this.notify(this._notification);
550
getIcon: function() {
551
return this.mount.get_icon();
555
const AutorunTransientNotification = new Lang.Class({
556
Name: 'AutorunTransientNotification',
557
Extends: MessageTray.Notification,
559
_init: function(manager, source) {
560
this.parent(source, source.title, null, { customContent: true });
562
this._manager = manager;
563
this._box = new St.BoxLayout({ style_class: 'hotplug-transient-box',
565
this.addActor(this._box);
567
this._mount = source.mount;
569
source.apps.forEach(Lang.bind(this, function (app) {
570
let actor = this._buttonForApp(app);
573
this._box.add(actor, { x_fill: true,
574
x_align: St.Align.START });
577
this._box.add(this._buttonForEject(), { x_fill: true,
578
x_align: St.Align.START });
580
// set the notification to transient and urgent, so that it
582
this.setTransient(true);
583
this.setUrgency(MessageTray.Urgency.CRITICAL);
586
_buttonForApp: function(app) {
587
let box = new St.BoxLayout();
588
let icon = new St.Icon({ gicon: app.get_icon(),
589
style_class: 'hotplug-notification-item-icon' });
592
let label = new St.Bin({ y_align: St.Align.MIDDLE,
594
({ text: _("Open with %s").format(app.get_name()) })
598
let button = new St.Button({ child: box,
600
x_align: St.Align.START,
601
button_mask: St.ButtonMask.ONE,
602
style_class: 'hotplug-notification-item' });
604
button.connect('clicked', Lang.bind(this, function() {
605
startAppForMount(app, this._mount);
612
_buttonForEject: function() {
613
let box = new St.BoxLayout();
614
let icon = new St.Icon({ icon_name: 'media-eject-symbolic',
615
style_class: 'hotplug-notification-item-icon' });
618
let label = new St.Bin({ y_align: St.Align.MIDDLE,
620
({ text: _("Eject") })
624
let button = new St.Button({ child: box,
626
x_align: St.Align.START,
627
button_mask: St.ButtonMask.ONE,
628
style_class: 'hotplug-notification-item' });
630
button.connect('clicked', Lang.bind(this, function() {
631
this._manager.ejectMount(this._mount);
638
const Component = AutorunManager;