Commit c0cb2536 authored by Nicholas Little's avatar Nicholas Little Committed by Andrés G. Aragoneses

Dap/Mtp: Remove unmount, add claim (bgo#731916)

We need to unmount MTP devices for banshee to make use of them, this
causes nautilus to produce error messages in some cases, as described in
[1].

This patch adds a parameter to DapSource.DeviceInitialize to signal
whether we should attempt to force the connection to the device (at the
user's request via the ClaimDapAction) or just make a best attempt.

To facilitate this, DapSource subclasses can now throw
InvalidDeviceStateException which will allow them to make a claim on a
device even though they couldn't initialize it. At that point, a new
type, PotentialSource is mapped, enabling the ClaimDapAction.

[1] https://bugzilla.gnome.org/show_bug.cgi?id=731916
parent a3d73d89
......@@ -70,7 +70,7 @@ namespace Banshee.Dap.AppleDevice
#region Device Setup/Dispose
public override void DeviceInitialize (IDevice device)
public override void DeviceInitialize (IDevice device, bool force)
{
Volume = device as IVolume;
......@@ -94,7 +94,7 @@ namespace Banshee.Dap.AppleDevice
throw new InvalidDeviceException ();
}
base.DeviceInitialize (device);
base.DeviceInitialize (device, force);
Name = Volume.Name;
SupportsPlaylists = true;
......
......@@ -50,9 +50,9 @@ namespace Banshee.Dap.Karma
private Dictionary<long, KarmaTrackInfo> track_map =
new Dictionary<long, KarmaTrackInfo>();
public override void DeviceInitialize(IDevice dev)
public override void DeviceInitialize(IDevice dev, bool force)
{
base.DeviceInitialize(dev);
base.DeviceInitialize(dev, force);
if (!IsKarma(dev))
throw new InvalidDeviceException();
......
......@@ -58,9 +58,9 @@ namespace Banshee.Dap.MassStorage
private IVolume volume;
private IUsbDevice usb_device;
public override void DeviceInitialize (IDevice device)
public override void DeviceInitialize (IDevice device, bool force)
{
base.DeviceInitialize (device);
base.DeviceInitialize (device, force);
volume = device as IVolume;
......
......@@ -64,9 +64,9 @@ namespace Banshee.Dap.Mtp
private bool can_sync_albumart = NeverSyncAlbumArtSchema.Get () == false;
private int thumb_width = AlbumArtWidthSchema.Get ();
public override void DeviceInitialize (IDevice device)
public override void DeviceInitialize (IDevice device, bool force)
{
base.DeviceInitialize (device);
base.DeviceInitialize (device, force);
var portInfo = device.ResolveUsbPortInfo ();
if (portInfo == null || portInfo.DeviceNumber == 0) {
......@@ -98,19 +98,20 @@ namespace Banshee.Dap.Mtp
//if (v.BusNumber == busnum && v.DeviceNumber == devnum) {
if (v.DeviceNumber == devnum) {
// If gvfs-gphoto has it mounted, unmount it
if (volume != null && volume.IsMounted) {
if (volume != null && volume.IsMounted && force) {
Log.DebugFormat ("MtpSource: attempting to unmount {0}", volume.Name);
volume.Unmount ();
}
for (int i = 5; i > 0 && mtp_device == null; i--) {
try {
mtp_device = MtpDevice.Connect (v);
} catch (Exception) {}
if (volume != null && volume.IsMounted) {
throw new InvalidDeviceStateException ();
}
if (mtp_device == null) {
Log.DebugFormat ("Failed to connect to mtp device. Trying {0} more times...", i - 1);
Thread.Sleep (2000);
}
mtp_device = MtpDevice.Connect (v);
if (mtp_device == null) {
Log.DebugFormat ("Failed to connect to mtp device {0}", device.Name);
throw new InvalidDeviceStateException ();
}
}
}
......@@ -465,16 +466,9 @@ namespace Banshee.Dap.Mtp
}
}
ServiceManager.SourceManager.RemoveSource (this);
mtp_device = null;
}
protected override void Eject ()
{
base.Eject ();
Dispose ();
}
private static string MakeAlbumKey (string album_artist, string album)
{
return String.Format ("{0}_{1}", album_artist, album);
......
......@@ -48,6 +48,11 @@ namespace Banshee.Dap.Gui
public DapActions () : base ("dap")
{
AddImportant (
new ActionEntry ("ClaimDapAction", null,
Catalog.GetString ("Claim"), null,
String.Empty, OnClaimDap)
);
AddImportant (
new ActionEntry ("SyncDapAction", null,
Catalog.GetString ("Sync"), null,
......@@ -57,6 +62,7 @@ namespace Banshee.Dap.Gui
AddUiFromFile ("GlobalUI.xml");
this["SyncDapAction"].IconName = Stock.Refresh;
this["ClaimDapAction"].IconName = Stock.Connect;
ServiceManager.SourceManager.ActiveSourceChanged += OnActiveSourceChanged;
Actions.SourceActions.Updated += delegate { UpdateActions (); };
OnActiveSourceChanged (null);
......@@ -71,6 +77,7 @@ namespace Banshee.Dap.Gui
}
previous_dap = ActiveSource as DapSource;
UpdateActions ();
if (previous_dap != null) {
previous_dap.Sync.Updated += OnSyncUpdated;
......@@ -87,6 +94,7 @@ namespace Banshee.Dap.Gui
DapSource dap = Dap;
if (dap != null) {
UpdateAction ("SyncDapAction", dap.Sync.Enabled);
UpdateAction ("ClaimDapAction", dap is PotentialSource);
}
}
......@@ -98,5 +106,12 @@ namespace Banshee.Dap.Gui
}
}
private void OnClaimDap (object o, EventArgs args)
{
var dap = Dap as PotentialSource;
if (dap != null) {
dap.TryClaim ();
}
}
}
}
......@@ -139,7 +139,7 @@ namespace Banshee.Dap.Gui
opts.Dispose ();
}
private void BuildActions ()
internal static void BuildActions()
{
if (actions == null) {
actions = new DapActions ();
......
//
// InactiveContent.cs
//
// Author:
// Nicholas Little <arealityfarbetween@googlemail.com>
//
// Copyright 2014 Nicholas Little
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using Gtk;
using Hyena;
using Hyena.Data;
using Hyena.Widgets;
using Mono.Unix;
using Banshee.Dap;
using Banshee.Sources.Gui;
using Banshee.ServiceStack;
using Banshee.Preferences;
using Banshee.Sources;
using Banshee.Preferences.Gui;
using Banshee.Widgets;
namespace Banshee.Dap.Gui
{
public class InactiveDapContent : DapPropertiesDisplay
{
Label title;
// To avoid the GLib.MissingIntPtrCtorException seen by some; BGO #552169
protected InactiveDapContent (IntPtr ptr) : base (ptr)
{
}
public InactiveDapContent (DapSource dapSource) : base(dapSource)
{
dapSource.Properties.PropertyChanged += OnPropertyChanged;
BuildWidgets ();
}
private void BuildWidgets ()
{
var outer = new HBox();
var device = new Image (LargeIcon) { Yalign = 0.0f };
outer.PackStart (device, false, false, 0);
var inner = new VBox { Spacing = 5, BorderWidth = 5 };
title = new Label { UseMarkup = true, Xalign = 0.0f };
SetTitleText (Source.Name);
inner.PackStart (title, false, false, 0);
var box = new HBox { Spacing = 5 };
box.PackStart (new Image { IconName = "dialog-warning" }, false, false, 0);
box.PackStart (new Label { Markup = ErrorString, UseMarkup = true }, false, false, 0);
inner.PackStart (box, false, false, 0);
outer.PackEnd (inner, false, false, 0);
Add (outer);
ShowAll ();
}
private void SetTitleText (string name)
{
title.Markup = String.Format (@"<span size=""x-large"" weight=""bold"">{0}</span>", name);
}
private void OnPropertyChanged (object o, PropertyChangeEventArgs args)
{
if (args.PropertyName == "Name") {
SetTitleText (args.NewValue.ToString ());
}
}
protected virtual string ErrorString {
get { return DefaultErrorString; }
}
static InactiveDapContent ()
{
var generic = Catalog.GetString ("Your device appears to be in use by another program");
var claimit = String.Format (
@"<span weight=""bold"">{0}</span>",
Catalog.GetString ("Claim")
);
var pressit = String.Format (
Catalog.GetString ("Press the {0} button above to use it in Banshee"),
claimit
);
DefaultErrorString = string.Format (
@"<span size=""large"">{0}." + "\n" + "{1}…</span>", generic, pressit
);
DapContent.BuildActions ();
}
private static readonly string DefaultErrorString;
}
}
......@@ -97,6 +97,8 @@
<Compile Include="Banshee.Dap.Gui\LibrarySyncOptions.cs" />
<Compile Include="Banshee.Dap\DapPriorityNode.cs" />
<Compile Include="Banshee.Dap\SyncPlaylist.cs" />
<Compile Include="Banshee.Dap\PotentialSource.cs" />
<Compile Include="Banshee.Dap\Gui\InactiveDapContent.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Banshee.Dap.addin.xml">
......
......@@ -29,6 +29,7 @@
//
using System;
using System.Linq;
using System.Collections.Generic;
using Mono.Unix;
......@@ -75,7 +76,6 @@ namespace Banshee.Dap
ServiceManager.HardwareManager.DeviceChanged += OnHardwareDeviceChanged;
ServiceManager.HardwareManager.DeviceRemoved += OnHardwareDeviceRemoved;
ServiceManager.HardwareManager.DeviceCommand += OnDeviceCommand;
ServiceManager.SourceManager.SourceRemoved += OnSourceRemoved;
initialized = true;
// Now that we've loaded all the enabled DAP providers, load the devices
......@@ -133,7 +133,6 @@ namespace Banshee.Dap
ServiceManager.HardwareManager.DeviceAdded -= OnHardwareDeviceAdded;
ServiceManager.HardwareManager.DeviceRemoved -= OnHardwareDeviceRemoved;
ServiceManager.HardwareManager.DeviceCommand -= OnDeviceCommand;
ServiceManager.SourceManager.SourceRemoved -= OnSourceRemoved;
List<DapSource> dap_sources = new List<DapSource> (sources.Values);
foreach (DapSource source in dap_sources) {
......@@ -153,10 +152,17 @@ namespace Banshee.Dap
foreach (TypeExtensionNode node in supported_dap_types) {
try {
DapSource source = (DapSource)node.CreateInstance ();
source.DeviceInitialize (device);
source.DeviceInitialize (device, false);
source.LoadDeviceContents ();
source.AddinId = node.Addin.Id;
return source;
} catch (InvalidDeviceStateException) {
Log.WarningFormat (
"Dap.DapService: invalid state, mapping potential source for {0}",
device.Name
);
DapSource source = new PotentialSource (this, node, device);
return source;
} catch (InvalidDeviceException) {
} catch (InvalidCastException e) {
Log.Warning ("Extension is not a DapSource as required", e);
......@@ -173,6 +179,25 @@ namespace Banshee.Dap
Scheduler.Schedule (new MapDeviceJob (this, device));
}
internal void SwapSource (DapSource oldSource, DapSource newSource, bool makeActive)
{
if (oldSource.Device.Uuid != newSource.Device.Uuid) {
Log.ErrorFormat (
"Dap.DapService: swap ignored from {0} to {1}.",
oldSource.Device.Uuid, newSource.Device.Uuid
);
return;
}
Log.DebugFormat (
"Dap.DapService: Swapping {0} with UUID {1} for {2}",
oldSource.GetType ().Name, oldSource.Device.Uuid,
newSource.GetType ().Name
);
Unmap (oldSource.Device.Uuid);
MapSource (newSource, makeActive);
}
private class MapDeviceJob : IJob
{
IDevice device;
......@@ -221,18 +246,28 @@ namespace Banshee.Dap
}
if (source != null) {
service.MapSource (source);
service.MapSource (source, false);
}
}
}
private void MapSource (DapSource source)
private void MapSource (DapSource source, bool active)
{
ThreadAssist.ProxyToMain (() => {
lock (sync) {
sources [source.Device.Uuid] = source;
source.RequestUnmap += OnRequestUnmap;
}
ThreadAssist.ProxyToMain (() => {
ServiceManager.SourceManager.AddSource (source);
source.NotifyUser ();
if (active)
{
ServiceManager.SourceManager.SetActiveSource (source);
}
// If there are any queued device commands, see if they are to be
// handled by this new DAP (e.g. --device-activate=file:///media/disk)
try {
......@@ -254,6 +289,15 @@ namespace Banshee.Dap
});
}
private void OnRequestUnmap (object sender, EventArgs e)
{
DapSource source = sender as DapSource;
if (source != null) {
Log.DebugFormat ("DapService: unmap request from {0}", source.Device.Uuid);
UnmapDevice (source.Device.Uuid);
}
}
internal void UnmapDevice (string uuid)
{
ThreadAssist.SpawnFromMain (() => Unmap (uuid));
......@@ -276,6 +320,7 @@ namespace Banshee.Dap
}
if (source != null) {
source.RequestUnmap -= OnRequestUnmap;
source.Dispose ();
ThreadAssist.ProxyToMain (delegate {
try {
......@@ -287,14 +332,6 @@ namespace Banshee.Dap
}
}
private void OnSourceRemoved (SourceEventArgs args)
{
DapSource dap_source = args.Source as DapSource;
if (dap_source != null) {
UnmapDevice (dap_source.Device.Uuid);
}
}
private void OnHardwareDeviceAdded (object o, DeviceAddedArgs args)
{
MapDevice (args.Device);
......@@ -302,7 +339,16 @@ namespace Banshee.Dap
private void OnHardwareDeviceChanged (object o, DeviceChangedEventArgs args)
{
MapDevice (args.Device);
DapSource source;
if (!sources.TryGetValue (args.Device.Uuid, out source)) {
MapDevice (args.Device);
return;
}
PotentialSource potential = source as PotentialSource;
if (potential != null) {
potential.TryInitialize ();
}
}
private void OnHardwareDeviceRemoved (object o, DeviceRemovedArgs args)
......
......@@ -88,7 +88,7 @@ namespace Banshee.Dap
{
}
public virtual void DeviceInitialize (IDevice device)
public virtual void DeviceInitialize (IDevice device, bool force)
{
this.device = device;
TypeUniqueId = device.Serial;
......@@ -176,6 +176,8 @@ namespace Banshee.Dap
protected set { supports_podcasts = value; }
}
internal event EventHandler RequestUnmap;
#region Source
protected override void Initialize ()
......@@ -343,6 +345,11 @@ namespace Banshee.Dap
protected override void Eject ()
{
Flush ();
var h = RequestUnmap;
if (h != null) {
h (this, EventArgs.Empty);
}
}
private void Flush ()
......
......@@ -33,4 +33,8 @@ namespace Banshee.Dap
public class InvalidDeviceException : ApplicationException
{
}
public class InvalidDeviceStateException : InvalidDeviceException
{
}
}
//
// PotentialSource.cs
//
// Author:
// Nicholas Little <arealityfarbetween@googlemail.com>
//
// Copyright (C) 2014 Nicholas Little
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
using System;
using Banshee.Collection.Database;
using Banshee.Dap.Gui;
using Banshee.Hardware;
using Banshee.Sources;
using Banshee.Sources.Gui;
using Hyena;
using Mono.Addins;
namespace Banshee.Dap
{
internal class PotentialSource : DapSource
{
private readonly TypeExtensionNode Claimant;
private readonly DapService Service;
private object lock_object = new object();
private bool initialized;
internal PotentialSource (DapService service, TypeExtensionNode claimant, IDevice device)
{
Claimant = claimant;
Service = service;
IsTemporary = true;
SupportsPlaylists = false;
SupportsPodcasts = false;
SupportsVideo = false;
DeviceInitialize (device, false);
Initialize ();
}
#region overridden members of Source
protected override void Initialize ()
{
base.Initialize ();
ThreadAssist.ProxyToMain (() => {
ClearChildSources ();
Properties.Set<ISourceContents> ("Nereid.SourceContents", new InactiveDapContent (this));
});
}
#endregion
#region implemented abstract members of RemovableSource
public override void Import ()
{
throw new NotSupportedException ();
}
public override bool CanUnmap {
get { return false; }
}
public override bool CanImport {
get { return false; }
}
public override bool IsReadOnly {
get { return true; }
}
public override long BytesUsed {
get { return 0L; }
}
public override long BytesCapacity {
get { return 0L; }
}
#endregion
#region implemented abstract members of DapSource
public override void AddChildSource (Source child)
{
}
public override void RemoveChildSource (Source child)
{
}
protected override void AddTrackToDevice (DatabaseTrackInfo track, SafeUri fromUri)
{
throw new NotSupportedException ();
}
private bool TryDeviceInitialize (bool force, out DapSource source)
{
lock (lock_object) {
source = default (DapSource);
if (initialized) {
return false;
}
SetStatus (
AddinManager.CurrentLocalizer.GetString ("Trying to Claim Your Device\u2026"),
false,
true,
"dialog-information"
);
Log.Debug ("PotentialSource: Creating Instance");
DapSource src = null;
try {
src = (DapSource) Claimant.CreateInstance ();
Log.Debug ("PotentialSource: Initializing Device");
src.DeviceInitialize (Device, force);
Log.Debug ("PotentialSource: Loading Contents");
src.LoadDeviceContents ();
Log.DebugFormat ("PotentialSource: Success, new Source {0}", src.Name);
src.AddinId = Claimant.Addin.Id;
source = src;
initialized = true;
} catch (InvalidDeviceStateException e) {
Log.Warning (e);
} catch (InvalidDeviceException e) {
Log.Warning (e);
} catch (Exception e) {
Log.Error (e);
}
bool success = !object.ReferenceEquals (source, default (DapSource));
SetStatus (
success ? AddinManager.CurrentLocalizer.GetString ("Connection Successful. Please wait\u2026")
: AddinManager.CurrentLocalizer.GetString ("Connection Failed\u2026"),
!success,
success,
success ? "dialog-information"
: "dialog-warning"
);
return success;
}
}
#endregion
internal void TryClaim ()
{
Log.DebugFormat ("PotentialSource: TryClaim {0} as {1}", Device.Name, Claimant.Type);
ThreadAssist.SpawnFromMain (() => {
DapSource source;
if (TryDeviceInitialize (true, out source)) {
Service.SwapSource (this, source, true);
}
});
}
internal void TryInitialize ()
{
Log.DebugFormat ("PotentialSource: TryInitialize {0} as {1}", Device.Name, Claimant.Type);
ThreadAssist.SpawnFromMain (() => {
DapSource source;
if (TryDeviceInitialize (false, out source)) {
Service.SwapSource (this, source, false);
}
});
}
}
}
......@@ -9,6 +9,7 @@ SOURCES = \
Banshee.Dap.Gui/DapInfoBar.cs \
Banshee.Dap.Gui/DapPropertiesDialog.cs \
Banshee.Dap.Gui/DapPropertiesDisplay.cs \
Banshee.Dap.Gui/InactiveDapContent.cs \
Banshee.Dap.Gui/LibrarySyncOptions.cs \
Banshee.Dap.Gui/PurchasedMusicActions.cs \
Banshee.Dap/DapLibrarySync.cs \
......@@ -21,6 +22,7 @@ SOURCES = \
Banshee.Dap/MediaGroupSource.cs \
Banshee.Dap/MusicGroupSource.cs \
Banshee.Dap/PodcastGroupSource.cs \
Banshee.Dap/PotentialSource.cs \
Banshee.Dap/RemovableSource.cs \
Banshee.Dap/SyncPlaylist.cs \
Banshee.Dap/VideoGroupSource.cs
......
<ui>
<toolbar name="HeaderToolbar">
<placeholder name="SourceActions">
<toolitem action="ClaimDapAction"/>
<toolitem action="UnmapSourceAction"/>
<toolitem action="SyncDapAction"/>
</placeholder>
......
<ui>
<popup name="RemovableSourceContextMenu" action="RemovableSourceContextMenuAction">
<placeholder name="AboveImportSource">
<menuitem action="ClaimDapAction"/>
<menuitem action="SyncDapAction"/>
</placeholder>
</popup>
......