2 * Copyright (c) 2006-2007 Maximilian Kossick <maximilian.kossick@googlemail.com>
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 #define DEBUG_PREFIX "MountPointManager"
21 #include "mountpointmanager.h"
24 #include "collectiondb.h"
26 #include "pluginmanager.h"
27 #include "statusbar.h"
30 #include <solid/predicate.h>
31 #include <solid/device.h>
32 #include <solid/deviceinterface.h>
33 #include <solid/devicenotifier.h>
34 #include <solid/storagevolume.h>
38 #include <QStringList>
41 typedef Medium::List MediumList
;
43 MountPointManager::MountPointManager()
46 setObjectName( "MountPointManager" );
48 if ( !Amarok::config( "Collection" ).readEntry( "DynamicCollection", true ) )
50 debug() << "Dynamic Collection deactivated in amarokrc, not loading plugins, not connecting signals";
54 connect( Solid::DeviceNotifier::instance(), SIGNAL( deviceAdded( QString
) ), SLOT( deviceAdded( QString
) ) );
55 connect( Solid::DeviceNotifier::instance(), SIGNAL( deviceRemoved( QString
) ), SLOT( deviceRemoved( QString
) ) );
59 CollectionDB
*collDB
= CollectionDB::instance();
61 if ( collDB
->adminValue( "Database Stats Version" ).toInt() >= 9 && /* make sure that deviceid actually exists*/
62 collDB
->query( "SELECT COUNT(url) FROM statistics WHERE deviceid = -2;" ).first().toInt() != 0 )
64 connect( this, SIGNAL( mediumConnected( int ) ), SLOT( migrateStatistics() ) );
65 QTimer::singleShot( 0, this, SLOT( migrateStatistics() ) );
67 connect( this, SIGNAL( mediumConnected( int ) ), SLOT( updateStatisticsURLs() ) );
68 updateStatisticsURLs();
72 MountPointManager::~MountPointManager()
74 m_handlerMapMutex
.lock();
75 foreach( DeviceHandler
*dh
, m_handlerMap
)
79 while( !m_mediumFactories
.isEmpty() )
80 delete m_mediumFactories
.takeFirst();
81 while( !m_remoteFactories
.isEmpty() )
82 delete m_remoteFactories
.takeFirst();
83 m_handlerMapMutex
.unlock();
86 MountPointManager
* MountPointManager::instance( )
88 static MountPointManager instance
;
93 MountPointManager::init()
96 KService::List plugins
= PluginManager::query( "[X-KDE-Amarok-plugintype] == 'device'" );
97 debug() << "Received [" << QString::number( plugins
.count() ) << "] device plugin offers";
98 oldForeachType( KService::List
, plugins
)
100 Amarok::Plugin
*plugin
= PluginManager::createFromService( *it
);
103 DeviceHandlerFactory
*factory
= static_cast<DeviceHandlerFactory
*>( plugin
);
104 if ( factory
->canCreateFromMedium() )
105 m_mediumFactories
.append( factory
);
106 else if (factory
->canCreateFromConfig() )
107 m_remoteFactories
.append( factory
);
109 //FIXME max: better error message
110 debug() << "Unknown DeviceHandlerFactory";
112 else debug() << "Plugin could not be loaded";
117 MountPointManager::getIdForUrl( const KUrl
&url
)
119 int mountPointLength
= 0;
121 m_handlerMapMutex
.lock();
122 foreach( DeviceHandler
*dh
, m_handlerMap
)
124 if ( url
.path().startsWith( dh
->getDevicePath() ) && mountPointLength
< dh
->getDevicePath().length() )
126 id
= m_handlerMap
.key( dh
);
127 mountPointLength
= dh
->getDevicePath().length();
130 m_handlerMapMutex
.unlock();
131 if ( mountPointLength
> 0 )
137 //default fallback if we could not identify the mount point.
138 //treat -1 as mount point / in all other methods
144 MountPointManager::getIdForUrl( const QString
&url
)
146 return getIdForUrl( KUrl( url
) );
150 MountPointManager::isMounted ( const int deviceId
) const {
151 m_handlerMapMutex
.lock();
152 bool result
= m_handlerMap
.contains( deviceId
);
153 m_handlerMapMutex
.unlock();
158 MountPointManager::getMountPointForId( const int id
) const
161 if ( isMounted( id
) )
163 m_handlerMapMutex
.lock();
164 mountPoint
= m_handlerMap
[id
]->getDevicePath();
165 m_handlerMapMutex
.unlock();
168 //TODO better error handling
174 MountPointManager::getAbsolutePath( const int deviceId
, const KUrl
& relativePath
, KUrl
& absolutePath
) const
176 //debug() << "id is " << deviceId << ", relative path is " << relativePath.path();
177 if ( deviceId
== -1 )
179 absolutePath
.setPath( "/" );
180 absolutePath
.addPath( relativePath
.path() );
181 absolutePath
.cleanPath();
182 //debug() << "Deviceid is -1, using relative Path as absolute Path, returning " << absolutePath.path();
185 m_handlerMapMutex
.lock();
186 if ( m_handlerMap
.contains( deviceId
) )
188 m_handlerMap
[deviceId
]->getURL( absolutePath
, relativePath
);
189 m_handlerMapMutex
.unlock();
193 m_handlerMapMutex
.unlock();
194 QStringList lastMountPoint
= CollectionDB::instance()->query(
195 QString( "SELECT lastmountpoint FROM devices WHERE id = %1" )
197 if ( lastMountPoint
.count() == 0 )
199 //hmm, no device with that id in the DB...serious problem
200 absolutePath
.setPath( "/" );
201 absolutePath
.addPath( relativePath
.path() );
202 absolutePath
.cleanPath();
203 warning() << "Device " << deviceId
<< " not in database, this should never happen! Returning " << absolutePath
.path();
207 absolutePath
.setPath( lastMountPoint
.first() );
208 absolutePath
.addPath( relativePath
.path() );
209 absolutePath
.cleanPath();
210 // debug() << "Device " << deviceId << " not mounted, using last mount point and returning " << absolutePath.path();
216 MountPointManager::getAbsolutePath( const int deviceId
, const QString
& relativePath
) const
219 rpath
.setPath( relativePath
);
221 getAbsolutePath( deviceId
, rpath
, url
);
226 MountPointManager::getRelativePath( const int deviceId
, const KUrl
& absolutePath
, KUrl
& relativePath
) const
228 m_handlerMapMutex
.lock();
229 if ( deviceId
!= -1 && m_handlerMap
.contains( deviceId
) )
231 //FIXME max: returns garbage if the absolute path is actually not under the device's mount point
232 QString rpath
= KUrl::relativePath( m_handlerMap
[deviceId
]->getDevicePath(), absolutePath
.path() );
233 m_handlerMapMutex
.unlock();
234 relativePath
.setPath( rpath
);
238 m_handlerMapMutex
.unlock();
239 //TODO: better error handling
240 QString rpath
= KUrl::relativePath( "/", absolutePath
.path() );
241 relativePath
.setPath( rpath
);
246 MountPointManager::getRelativePath( const int deviceId
, const QString
& absolutePath
) const
249 getRelativePath( deviceId
, KUrl( absolutePath
), url
);
254 MountPointManager::mediumChanged( const Medium
*m
)
258 if ( m
->isMounted() )
260 foreach( DeviceHandlerFactory
*factory
, m_mediumFactories
)
262 if ( factory
->canHandle ( m
) )
264 debug() << "found handler for " << m
->id();
265 DeviceHandler
*handler
= factory
->createHandler( m
);
268 debug() << "Factory " << factory
->type() << "could not create device handler";
271 int key
= handler
->getDeviceID();
272 m_handlerMapMutex
.lock();
273 if ( m_handlerMap
.contains( key
) )
275 debug() << "Key " << key
<< " already exists in handlerMap, replacing";
276 delete m_handlerMap
[key
];
277 m_handlerMap
.remove( key
);
279 m_handlerMap
.insert( key
, handler
);
280 m_handlerMapMutex
.unlock();
281 debug() << "added device " << key
<< " with mount point " << m
->mountPoint();
282 emit
mediumConnected( key
);
283 break; //we found the added medium and don't have to check the other device handlers
289 m_handlerMapMutex
.lock();
290 foreach( DeviceHandler
*dh
, m_handlerMap
)
292 if ( dh
->deviceIsMedium( m
) )
294 int key
= m_handlerMap
.key( dh
);
295 m_handlerMap
.remove( key
);
297 debug() << "removed device " << key
;
298 m_handlerMapMutex
.unlock();
299 emit
mediumRemoved( key
);
300 //we found the medium which was removed, so we can abort the loop
304 m_handlerMapMutex
.unlock();
309 MountPointManager::mediumRemoved( const Medium
*m
)
318 //this works for USB devices, special cases might be required for other devices
319 m_handlerMapMutex
.lock();
320 foreach( DeviceHandler
*dh
, m_handlerMap
)
322 if( dh
->deviceIsMedium( m
) )
324 int key
= m_handlerMap
.key( dh
);
325 m_handlerMap
.remove( key
);
327 debug() << "removed device " << key
;
328 m_handlerMapMutex
.unlock();
329 emit
mediumRemoved( key
);
330 //we found the medium which was removed, so we can abort the loop
334 m_handlerMapMutex
.unlock();
339 MountPointManager::mediumAdded( const Medium
*m
)
343 if ( m
->isMounted() )
345 debug() << "Device added and mounted, checking handlers";
346 foreach( DeviceHandlerFactory
*factory
, m_mediumFactories
)
348 if( factory
->canHandle( m
) )
350 debug() << "found handler for " << m
->id();
351 DeviceHandler
*handler
= factory
->createHandler( m
);
354 debug() << "Factory " << factory
->type() << "could not create device handler";
357 int key
= handler
->getDeviceID();
358 m_handlerMapMutex
.lock();
359 if( m_handlerMap
.contains( key
) )
361 debug() << "Key " << key
<< " already exists in handlerMap, replacing";
362 delete m_handlerMap
[key
];
363 m_handlerMap
.remove( key
);
365 m_handlerMap
.insert( key
, handler
);
366 m_handlerMapMutex
.unlock();
367 debug() << "added device " << key
<< " with mount point " << m
->mountPoint();
368 emit
mediumConnected( key
);
369 break; //we found the added medium and don't have to check the other device handlers
376 MountPointManager::getMountedDeviceIds() const {
377 m_handlerMapMutex
.lock();
378 IdList
list( m_handlerMap
.keys() );
379 m_handlerMapMutex
.unlock();
385 MountPointManager::collectionFolders( )
387 //TODO max: cache data
389 KConfigGroup folders
= Amarok::config( "Collection Folders" );
390 IdList ids
= getMountedDeviceIds();
391 foreach( int id
, ids
)
393 QStringList rpaths
= folders
.readEntry( QString::number( id
), QStringList() );
394 foreach( QString strIt
, rpaths
)
399 absPath
= getMountPointForId( id
);
403 absPath
= getAbsolutePath( id
, strIt
);
405 if ( !result
.contains( absPath
) )
406 result
.append( absPath
);
413 MountPointManager::setCollectionFolders( const QStringList
&folders
)
415 //TODO max: cache data
416 typedef QMap
<int, QStringList
> FolderMap
;
417 KConfigGroup folderConf
= Amarok::config( "Collection Folders" );
420 foreach( QString folder
, folders
)
422 int id
= getIdForUrl( folder
);
423 QString rpath
= getRelativePath( id
, folder
);
424 if ( folderMap
.contains( id
) ) {
425 if ( !folderMap
[id
].contains( rpath
) )
426 folderMap
[id
].append( rpath
);
429 folderMap
[id
] = QStringList( rpath
);
431 //make sure that collection folders on devices which are not in foldermap are deleted
432 IdList ids
= getMountedDeviceIds();
433 foreach( int deviceId
, ids
)
435 if( !folderMap
.contains( deviceId
) )
437 folderConf
.deleteEntry( QString::number( deviceId
) );
440 QMapIterator
<int, QStringList
> i( folderMap
);
444 folderConf
.writeEntry( QString::number( i
.key() ), i
.value() );
449 MountPointManager::migrateStatistics()
451 QStringList urls
= CollectionDB::instance()->query( "SELECT url FROM statistics WHERE deviceid = -2;" );
452 foreach( QString url
, urls
)
454 if ( QFile::exists( url
) )
456 int deviceid
= getIdForUrl( url
);
457 QString rpath
= getRelativePath( deviceid
, url
);
458 QString update
= QString( "UPDATE statistics SET deviceid = %1, url = '%2'" )
460 .arg( CollectionDB::instance()->escapeString( rpath
) );
461 update
+= QString( " WHERE url = '%1' AND deviceid = -2;" )
462 .arg( CollectionDB::instance()->escapeString( url
) );
463 CollectionDB::instance()->query( update
);
469 MountPointManager::updateStatisticsURLs( bool changed
)
472 QTimer::singleShot( 0, this, SLOT( startStatisticsUpdateJob() ) );
476 MountPointManager::startStatisticsUpdateJob()
478 ThreadManager::instance()->queueJob( new UrlUpdateJob( this ) );
482 MountPointManager::checkDeviceAvailability()
484 //code to actively scan for devices which are not supported by KDE mediamanager should go here
485 //method is not actually called yet
489 MountPointManager::deviceAdded( const QString
&udi
)
491 Solid::Predicate predicate
= Solid::Predicate( Solid::DeviceInterface::StorageVolume
, "udi", udi
);
492 QList
<Solid::Device
> devices
= Solid::Device::listFromQuery( predicate
);
493 //there'll be maximum one device because we are using the udi in the predicate
494 if( !devices
.isEmpty() )
496 Solid::StorageVolume
*volume
= devices
[0].as
<Solid::StorageVolume
>();
502 MountPointManager::deviceRemoved( const QString
&udi
)
509 bool UrlUpdateJob::doJob( )
517 void UrlUpdateJob::updateStatistics( )
519 CollectionDB
*collDB
= CollectionDB::instance();
520 MountPointManager
*mpm
= MountPointManager::instance();
521 QStringList urls
= collDB
->query( "SELECT s.deviceid,s.url "
522 "FROM statistics AS s LEFT JOIN tags AS t ON s.deviceid = t.deviceid AND s.url = t.url "
523 "WHERE t.url IS NULL AND s.deviceid != -2;" );
524 debug() << "Trying to update " << urls
.count() / 2 << " statistics rows";
527 int deviceid
= (*it
).toInt();
528 QString rpath
= *++it
;
529 QString realURL
= mpm
->getAbsolutePath( deviceid
, rpath
);
530 if( QFile::exists( realURL
) )
532 int newDeviceid
= mpm
->getIdForUrl( realURL
);
533 if( newDeviceid
== deviceid
)
535 QString newRpath
= mpm
->getRelativePath( newDeviceid
, realURL
);
537 int statCount
= collDB
->query(
538 QString( "SELECT COUNT( url ) FROM statistics WHERE deviceid = %1 AND url = '%2';" )
540 .arg( collDB
->escapeString( newRpath
) ) ).first().toInt();
542 continue; //statistics row with new URL/deviceid values already exists
544 QString sql
= QString( "UPDATE statistics SET deviceid = %1, url = '%2'" )
545 .arg( newDeviceid
).arg( collDB
->escapeString( newRpath
) );
546 sql
+= QString( " WHERE deviceid = %1 AND url = '%2';" )
547 .arg( deviceid
).arg( collDB
->escapeString( rpath
) );
548 collDB
->query( sql
);
553 void UrlUpdateJob::updateLabels( )
555 CollectionDB
*collDB
= CollectionDB::instance();
556 MountPointManager
*mpm
= MountPointManager::instance();
557 QStringList labels
= collDB
->query( "SELECT l.deviceid,l.url "
558 "FROM tags_labels AS l LEFT JOIN tags as t ON l.deviceid = t.deviceid AND l.url = t.url "
559 "WHERE t.url IS NULL;" );
560 debug() << "Trying to update " << labels
.count() / 2 << " tags_labels rows";
563 int deviceid
= (*it
).toInt();
564 QString rpath
= *++it
;
565 QString realUrl
= mpm
->getAbsolutePath( deviceid
, rpath
);
566 if( QFile::exists( realUrl
) )
568 int newDeviceid
= mpm
->getIdForUrl( realUrl
);
569 if( newDeviceid
== deviceid
)
571 QString newRpath
= mpm
->getRelativePath( newDeviceid
, realUrl
);
573 //only update rows if there is not already a row with the new deviceid/rpath and the same labelid
574 QStringList labelids
= collDB
->query(
575 QString( "SELECT labelid FROM tags_labels WHERE deviceid = %1 AND url = '%2';" )
576 .arg( QString::number( newDeviceid
), collDB
->escapeString( newRpath
) ) );
577 QString existingLabelids
;
578 if( !labelids
.isEmpty() )
580 existingLabelids
= " AND labelid NOT IN (";
581 oldForeach( labelids
)
583 if( it
!= labelids
.begin() )
584 existingLabelids
+= ',';
585 existingLabelids
+= *it
;
587 existingLabelids
+= ')';
589 QString sql
= QString( "UPDATE tags_labels SET deviceid = %1, url = '%2' "
590 "WHERE deviceid = %3 AND url = '%4'%5;" )
592 .arg( collDB
->escapeString( newRpath
),
593 QString::number( deviceid
),
594 collDB
->escapeString( rpath
),
596 collDB
->query( sql
);
601 #include "mountpointmanager.moc"