1
/*
2
 *  Copyright (c) 2006-2007 Maximilian Kossick <maximilian.kossick@googlemail.com>
3
 *
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.
8
 *
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.
13
 *
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.
17
 */
18
19
#define DEBUG_PREFIX "MountPointManager"
20
21
#include "MountPointManager.h"
22
23
#include "Amarok.h"
24
#include "CollectionManager.h"
25
#include "Debug.h"
26
#include "PluginManager.h"
27
#include "statusbar/StatusBar.h"
28
#include "SqlStorage.h"
29
30
//solid stuff
31
#include <solid/predicate.h>
32
#include <solid/device.h>
33
#include <solid/deviceinterface.h>
34
#include <solid/devicenotifier.h>
35
#include <solid/storagevolume.h>
36
37
#include <threadweaver/Job.h>
38
#include <threadweaver/ThreadWeaver.h>
39
40
#include <QDesktopServices>
41
#include <QDir>
42
#include <QFile>
43
#include <QList>
44
#include <QStringList>
45
#include <QTimer>
46
47
48
MountPointManager* MountPointManager::s_instance = 0;
49
50
MountPointManager*
51
MountPointManager::instance()
52
{
53
    if( !s_instance )
54
        s_instance = new MountPointManager();
55
    return s_instance;
56
}
57
58
void
59
MountPointManager::destroy()
60
{
61
    delete s_instance;
62
    s_instance = 0;
63
}
64
65
66
MountPointManager::MountPointManager()
67
    : QObject( 0 )
68
{
69
    setObjectName( "MountPointManager" );
70
71
    if ( !Amarok::config( "Collection" ).readEntry( "DynamicCollection", true ) )
72
    {
73
        debug() << "Dynamic Collection deactivated in amarokrc, not loading plugins, not connecting signals";
74
        return;
75
    }
76
77
    connect( Solid::DeviceNotifier::instance(), SIGNAL( deviceAdded( QString ) ), SLOT( deviceAdded( QString ) ) );
78
    connect( Solid::DeviceNotifier::instance(), SIGNAL( deviceRemoved( QString ) ), SLOT( deviceRemoved( QString ) ) );
79
80
    init();
81
82
//     SqlStorage *collDB = CollectionManager::instance()->sqlStorage();
83
84
    //FIXME: Port 2.0
85
//     if ( collDB->adminValue( "Database Stats Version" ).toInt() >= 9 && /* make sure that deviceid actually exists*/
86
//          collDB->query( "SELECT COUNT(url) FROM statistics WHERE deviceid = -2;" ).first().toInt() != 0 )
87
//     {
88
//         connect( this, SIGNAL( mediumConnected( int ) ), SLOT( migrateStatistics() ) );
89
//         QTimer::singleShot( 0, this, SLOT( migrateStatistics() ) );
90
//     }
91
    updateStatisticsURLs();
92
}
93
94
95
MountPointManager::~MountPointManager()
96
{
97
    DEBUG_BLOCK
98
99
    m_handlerMapMutex.lock();
100
    foreach( DeviceHandler *dh, m_handlerMap )
101
        delete dh;
102
    
103
    while( !m_mediumFactories.isEmpty() )
104
        delete m_mediumFactories.takeFirst();
105
    while( !m_remoteFactories.isEmpty() )
106
        delete m_remoteFactories.takeFirst();
107
    m_handlerMapMutex.unlock();
108
}
109
110
111
void
112
MountPointManager::init()
113
{
114
    DEBUG_BLOCK
115
    KService::List plugins = PluginManager::query( "[X-KDE-Amarok-plugintype] == 'device'" );
116
    debug() << "Received [" << QString::number( plugins.count() ) << "] device plugin offers";
117
    oldForeachType( KService::List, plugins )
118
    {
119
        Amarok::Plugin *plugin = PluginManager::createFromService( *it );
120
        if( plugin )
121
        {
122
            DeviceHandlerFactory *factory = static_cast<DeviceHandlerFactory*>( plugin );
123
            if ( factory->canCreateFromMedium() )
124
                m_mediumFactories.append( factory );
125
            else if (factory->canCreateFromConfig() )
126
                m_remoteFactories.append( factory );
127
            else
128
                //FIXME max: better error message
129
                debug() << "Unknown DeviceHandlerFactory";
130
        }
131
        else
132
            debug() << "Plugin could not be loaded";
133
134
        Solid::Predicate predicate = Solid::Predicate( Solid::DeviceInterface::StorageVolume );
135
        QList<Solid::Device> devices = Solid::Device::listFromQuery( predicate );
136
        foreach( const Solid::Device &device, devices )
137
            createHandlerFromDevice( device, device.udi() );
138
    }
139
}
140
141
int
142
MountPointManager::getIdForUrl( const KUrl &url )
143
{
144
    int mountPointLength = 0;
145
    int id = -1;
146
    m_handlerMapMutex.lock();
147
    foreach( DeviceHandler *dh, m_handlerMap )
148
    {
149
        if ( url.path().startsWith( dh->getDevicePath() ) && mountPointLength < dh->getDevicePath().length() )
150
        {
151
            id = m_handlerMap.key( dh );
152
            mountPointLength = dh->getDevicePath().length();
153
        }
154
    }
155
    m_handlerMapMutex.unlock();
156
    if ( mountPointLength > 0 )
157
    {
158
        return id;
159
    }
160
    else
161
    {
162
        //default fallback if we could not identify the mount point.
163
        //treat -1 as mount point / in all other methods
164
        return -1;
165
    }
166
}
167
168
bool
169
MountPointManager::isMounted( const int deviceId ) const
170
{
171
    m_handlerMapMutex.lock();
172
    const bool result = m_handlerMap.contains( deviceId );
173
    m_handlerMapMutex.unlock();
174
    return result;
175
}
176
177
QString
178
MountPointManager::getMountPointForId( const int id ) const
179
{
180
    QString mountPoint;
181
    if ( isMounted( id ) )
182
    {
183
        m_handlerMapMutex.lock();
184
        mountPoint = m_handlerMap[id]->getDevicePath();
185
        m_handlerMapMutex.unlock();
186
    }
187
    else
188
        //TODO better error handling
189
        mountPoint = "/";
190
    return mountPoint;
191
}
192
193
void
194
MountPointManager::getAbsolutePath( const int deviceId, const KUrl& relativePath, KUrl& absolutePath) const
195
{
196
    //debug() << "id is " << deviceId << ", relative path is " << relativePath.path();
197
    if ( deviceId == -1 )
198
    {
199
#ifdef Q_OS_WIN32
200
        absolutePath.setPath( relativePath.path() );
201
#else
202
        absolutePath.setPath( "/" );
203
        absolutePath.addPath( relativePath.path() );
204
#endif
205
        absolutePath.cleanPath();
206
        //debug() << "Deviceid is -1, using relative Path as absolute Path, returning " << absolutePath.path();
207
        return;
208
    }
209
    m_handlerMapMutex.lock();
210
    if ( m_handlerMap.contains( deviceId ) )
211
    {
212
        m_handlerMap[deviceId]->getURL( absolutePath, relativePath );
213
        m_handlerMapMutex.unlock();
214
    }
215
    else
216
    {
217
        m_handlerMapMutex.unlock();
218
        const QStringList lastMountPoint = CollectionManager::instance()->sqlStorage()->query(
219
                                                 QString( "SELECT lastmountpoint FROM devices WHERE id = %1" )
220
                                                 .arg( deviceId ) );
221
        if ( lastMountPoint.count() == 0 )
222
        {
223
            //hmm, no device with that id in the DB...serious problem
224
            getAbsolutePath( -1, relativePath, absolutePath );
225
            warning() << "Device " << deviceId << " not in database, this should never happen! Returning " << absolutePath.path();
226
        }
227
        else
228
        {
229
            absolutePath.setPath( lastMountPoint.first() );
230
            absolutePath.addPath( relativePath.path() );
231
            absolutePath.cleanPath();
232
            debug() << "Device " << deviceId << " not mounted, using last mount point and returning " << absolutePath.path();
233
        }
234
    }
235
}
236
237
QString
238
MountPointManager::getAbsolutePath( const int deviceId, const QString& relativePath ) const
239
{
240
    KUrl rpath;
241
    rpath.setPath( relativePath );
242
    KUrl url;
243
    getAbsolutePath( deviceId, rpath, url );
244
    return url.path();
245
}
246
247
void
248
MountPointManager::getRelativePath( const int deviceId, const KUrl& absolutePath, KUrl& relativePath ) const
249
{
250
    m_handlerMapMutex.lock();
251
    if ( deviceId != -1 && m_handlerMap.contains( deviceId ) )
252
    {
253
        //FIXME max: returns garbage if the absolute path is actually not under the device's mount point
254
        QString rpath = KUrl::relativePath( m_handlerMap[deviceId]->getDevicePath(), absolutePath.path() );
255
        m_handlerMapMutex.unlock();
256
        relativePath.setPath( rpath );
257
    }
258
    else
259
    {
260
        m_handlerMapMutex.unlock();
261
        //TODO: better error handling
262
#ifdef Q_OS_WIN32
263
        QString rpath = absolutePath.path();
264
#else
265
        QString rpath = KUrl::relativePath( "/", absolutePath.path() );
266
#endif
267
        relativePath.setPath( rpath );
268
    }
269
}
270
271
QString
272
MountPointManager::getRelativePath( const int deviceId, const QString& absolutePath ) const
273
{
274
    KUrl url;
275
    getRelativePath( deviceId, KUrl( absolutePath ), url );
276
    return url.path();
277
}
278
279
// void
280
// MountPointManager::mediumChanged( const Medium *m )
281
// {
282
//     DEBUG_BLOCK
283
//     if ( !m ) return;
284
//     if ( m->isMounted() )
285
//     {
286
//         foreach( DeviceHandlerFactory *factory, m_mediumFactories )
287
//         {
288
//             if ( factory->canHandle ( m ) )
289
//             {
290
//                 debug() << "found handler for " << m->id();
291
//                 DeviceHandler *handler = factory->createHandler( m );
292
//                 if( !handler )
293
//                 {
294
//                     debug() << "Factory " << factory->type() << "could not create device handler";
295
//                     break;
296
//                 }
297
//                 int key = handler->getDeviceID();
298
//                 m_handlerMapMutex.lock();
299
//                 if ( m_handlerMap.contains( key ) )
300
//                 {
301
//                     debug() << "Key " << key << " already exists in handlerMap, replacing";
302
//                     delete m_handlerMap[key];
303
//                     m_handlerMap.remove( key );
304
//                 }
305
//                 m_handlerMap.insert( key, handler );
306
//                 m_handlerMapMutex.unlock();
307
//                 debug() << "added device " << key << " with mount point " << m->mountPoint();
308
//                 emit mediumConnected( key );
309
//                 break;  //we found the added medium and don't have to check the other device handlers
310
//             }
311
//         }
312
//     }
313
//     else
314
//     {
315
//         m_handlerMapMutex.lock();
316
//         foreach( DeviceHandler *dh, m_handlerMap )
317
//         {
318
//             if ( dh->deviceIsMedium( m ) )
319
//             {
320
//                 int key = m_handlerMap.key( dh );
321
//                 m_handlerMap.remove( key );
322
//                 delete dh;
323
//                 debug() << "removed device " << key;
324
//                 m_handlerMapMutex.unlock();
325
//                 emit mediumRemoved( key );
326
//                 //we found the medium which was removed, so we can abort the loop
327
//                 return;
328
//             }
329
//         }
330
//         m_handlerMapMutex.unlock();
331
//     }
332
// }
333
//
334
335
IdList
336
MountPointManager::getMountedDeviceIds() const
337
{
338
    m_handlerMapMutex.lock();
339
    IdList list( m_handlerMap.keys() );
340
    m_handlerMapMutex.unlock();
341
    list.append( -1 );
342
    return list;
343
}
344
345
QStringList
346
MountPointManager::collectionFolders()
347
{
348
    DEBUG_BLOCK
349
350
    //TODO max: cache data
351
    QStringList result;
352
    KConfigGroup folders = Amarok::config( "Collection Folders" );
353
    IdList ids = getMountedDeviceIds();
354
    foreach( int id, ids )
355
    {
356
        const QStringList rpaths = folders.readEntry( QString::number( id ), QStringList() );
357
        foreach( const QString &strIt, rpaths )
358
        {
359
            QString absPath;
360
            if ( strIt == "./" )
361
            {
362
                absPath = getMountPointForId( id );
363
            }
364
            else
365
            {
366
                absPath = getAbsolutePath( id, strIt );
367
            }
368
            if ( !result.contains( absPath ) )
369
                result.append( absPath );
370
        }
371
    }
372
    if( result.isEmpty() )
373
    {
374
        const QString musicDir = QDesktopServices::storageLocation( QDesktopServices::MusicLocation );
375
        debug() << "QDesktopServices::MusicLocation: " << musicDir; 
376
377
        if( !musicDir.isEmpty() )
378
        {
379
            const QDir dir( musicDir );
380
            if( dir != QDir::home() && dir.exists() )
381
            {
382
                result << musicDir;
383
            }
384
        }
385
    }
386
    return result;
387
}
388
389
void
390
MountPointManager::setCollectionFolders( const QStringList &folders )
391
{
392
    if( folders.size() == 1 )
393
    {
394
        if( folders[0] == QDesktopServices::storageLocation( QDesktopServices::MusicLocation ) )
395
        {
396
            return;
397
        }
398
    }
399
    typedef QMap<int, QStringList> FolderMap;
400
    KConfigGroup folderConf = Amarok::config( "Collection Folders" );
401
    FolderMap folderMap;
402
    
403
    foreach( const QString &folder, folders )
404
    {
405
        int id = getIdForUrl( folder );
406
        const QString rpath = getRelativePath( id, folder );
407
        if( folderMap.contains( id ) ) {
408
            if( !folderMap[id].contains( rpath ) )
409
                folderMap[id].append( rpath );
410
        }
411
        else
412
            folderMap[id] = QStringList( rpath );
413
    }
414
    //make sure that collection folders on devices which are not in foldermap are deleted
415
    IdList ids = getMountedDeviceIds();
416
    foreach( int deviceId, ids )
417
    {
418
        if( !folderMap.contains( deviceId ) )
419
        {
420
            folderConf.deleteEntry( QString::number( deviceId ) );
421
        }
422
    }
423
    QMapIterator<int, QStringList> i( folderMap );
424
    while( i.hasNext() )
425
    {
426
        i.next();
427
        folderConf.writeEntry( QString::number( i.key() ), i.value() );
428
    }
429
}
430
431
void
432
MountPointManager::migrateStatistics()
433
{
434
    QStringList urls = CollectionManager::instance()->sqlStorage()->query( "SELECT url FROM statistics WHERE deviceid = -2;" );
435
    foreach( const QString &url, urls )
436
    {
437
        if ( QFile::exists( url ) )
438
        {
439
            int deviceid = getIdForUrl( url );
440
            SqlStorage *db = CollectionManager::instance()->sqlStorage();
441
            QString rpath = getRelativePath( deviceid, url );
442
            QString update = QString( "UPDATE statistics SET deviceid = %1, url = '%2'" )
443
                                      .arg( deviceid )
444
                                      .arg( db->escape( rpath ) );
445
            update += QString( " WHERE url = '%1' AND deviceid = -2;" )
446
                               .arg( db->escape( url ) );
447
            db->query( update );
448
        }
449
    }
450
}
451
452
void
453
MountPointManager::updateStatisticsURLs( bool changed )
454
{
455
    if ( changed )
456
        QTimer::singleShot( 0, this, SLOT( startStatisticsUpdateJob() ) );
457
}
458
459
void
460
MountPointManager::startStatisticsUpdateJob()
461
{
462
    AMAROK_NOTIMPLEMENTED
463
    //ThreadWeaver::Weaver::instance()->enqueue( new UrlUpdateJob( this ) );
464
}
465
466
void
467
MountPointManager::checkDeviceAvailability()
468
{
469
    //code to actively scan for devices which are not supported by KDE mediamanager should go here
470
    //method is not actually called yet
471
}
472
473
void
474
MountPointManager::deviceAdded( const QString &udi )
475
{
476
    DEBUG_BLOCK
477
    Solid::Predicate predicate = Solid::Predicate( Solid::DeviceInterface::StorageVolume, "udi", udi );
478
    QList<Solid::Device> devices = Solid::Device::listFromQuery( predicate );
479
    //there'll be maximum one device because we are using the udi in the predicate
480
    if( !devices.isEmpty() )
481
    {
482
        Solid::Device device = devices[0];
483
        createHandlerFromDevice( device, udi );
484
        CollectionManager::instance()->primaryCollection()->collectionUpdated();
485
    }
486
}
487
488
void
489
MountPointManager::deviceRemoved( const QString &udi )
490
{
491
    DEBUG_BLOCK
492
    m_handlerMapMutex.lock();
493
    foreach( DeviceHandler *dh, m_handlerMap )
494
    {
495
        if( dh->deviceMatchesUdi( udi ) )
496
        {
497
            int key = m_handlerMap.key( dh );
498
            m_handlerMap.remove( key );
499
            delete dh;
500
            debug() << "removed device " << key;
501
            m_handlerMapMutex.unlock();
502
            CollectionManager::instance()->primaryCollection()->collectionUpdated();
503
            //we found the medium which was removed, so we can abort the loop
504
            return;
505
        }
506
    }
507
    m_handlerMapMutex.unlock();
508
}
509
510
void MountPointManager::createHandlerFromDevice( const Solid::Device& device, const QString &udi )
511
{
512
    if ( device.isValid() )
513
    {
514
        debug() << "Device added and mounted, checking handlers";
515
        foreach( DeviceHandlerFactory *factory, m_mediumFactories )
516
        {
517
            if( factory->canHandle( device ) )
518
            {
519
                debug() << "found handler for " << udi;
520
                DeviceHandler *handler = factory->createHandler( device, udi );
521
                if( !handler )
522
                {
523
                    debug() << "Factory " << factory->type() << "could not create device handler";
524
                    break;
525
                }
526
                int key = handler->getDeviceID();
527
                m_handlerMapMutex.lock();
528
                if( m_handlerMap.contains( key ) )
529
                {
530
                    debug() << "Key " << key << " already exists in handlerMap, replacing";
531
                    delete m_handlerMap[key];
532
                    m_handlerMap.remove( key );
533
                }
534
                m_handlerMap.insert( key, handler );
535
                m_handlerMapMutex.unlock();
536
//                 debug() << "added device " << key << " with mount point " << volumeAccess->mountPoint();
537
//                 emit mediumConnected( key );
538
                break;  //we found the added medium and don't have to check the other device handlers
539
            }
540
        }
541
    }
542
}
543
544
545
//UrlUpdateJob
546
547
UrlUpdateJob::UrlUpdateJob( QObject *dependent )
548
    : ThreadWeaver::Job( dependent )
549
{
550
    connect( this, SIGNAL( done( ThreadWeaver::Job* ) ), SLOT( deleteLater() ) );
551
}
552
553
void
554
UrlUpdateJob::run()
555
{
556
    DEBUG_BLOCK
557
    updateStatistics();
558
    updateLabels();
559
}
560
561
void UrlUpdateJob::updateStatistics( )
562
{
563
    AMAROK_NOTIMPLEMENTED
564
#if 0
565
    SqlStorage *db = CollectionManager::instance()->sqlStorage();
566
    MountPointManager *mpm = MountPointManager::instance();
567
    QStringList urls = db->query( "SELECT s.deviceid,s.url "
568
                                      "FROM statistics AS s LEFT JOIN tags AS t ON s.deviceid = t.deviceid AND s.url = t.url "
569
                                      "WHERE t.url IS NULL AND s.deviceid != -2;" );
570
    debug() << "Trying to update " << urls.count() / 2 << " statistics rows";
571
    oldForeach( urls )
572
    {
573
        int deviceid = (*it).toInt();
574
        QString rpath = *++it;
575
        QString realURL = mpm->getAbsolutePath( deviceid, rpath );
576
        if( QFile::exists( realURL ) )
577
        {
578
            int newDeviceid = mpm->getIdForUrl( realURL );
579
            if( newDeviceid == deviceid )
580
                continue;
581
            QString newRpath = mpm->getRelativePath( newDeviceid, realURL );
582
583
            int statCount = db->query(
584
                            QString( "SELECT COUNT( url ) FROM statistics WHERE deviceid = %1 AND url = '%2';" )
585
                                        .arg( newDeviceid )
586
                                        .arg( db->escape( newRpath ) ) ).first().toInt();
587
            if( statCount )
588
                continue;       //statistics row with new URL/deviceid values already exists
589
590
            QString sql = QString( "UPDATE statistics SET deviceid = %1, url = '%2'" )
591
                                .arg( newDeviceid ).arg( db->escape( newRpath ) );
592
            sql += QString( " WHERE deviceid = %1 AND url = '%2';" )
593
                                .arg( deviceid ).arg( db->escape( rpath ) );
594
            db->query( sql );
595
        }
596
    }
597
#endif
598
}
599
600
void UrlUpdateJob::updateLabels( )
601
{
602
    AMAROK_NOTIMPLEMENTED
603
604
#if 0
605
    SqlStorage *db = CollectionManager::instance()->sqlStorage();
606
    MountPointManager *mpm = MountPointManager::instance();
607
    QStringList labels = db->query( "SELECT l.deviceid,l.url "
608
                                        "FROM tags_labels AS l LEFT JOIN tags as t ON l.deviceid = t.deviceid AND l.url = t.url "
609
                                        "WHERE t.url IS NULL;" );
610
    debug() << "Trying to update " << labels.count() / 2 << " tags_labels rows";
611
    oldForeach( labels )
612
    {
613
        int deviceid = (*it).toInt();
614
        QString rpath = *++it;
615
        QString realUrl = mpm->getAbsolutePath( deviceid, rpath );
616
        if( QFile::exists( realUrl ) )
617
        {
618
            int newDeviceid = mpm->getIdForUrl( realUrl );
619
            if( newDeviceid == deviceid )
620
                continue;
621
            QString newRpath = mpm->getRelativePath( newDeviceid, realUrl );
622
623
            //only update rows if there is not already a row with the new deviceid/rpath and the same labelid
624
            QStringList labelids = db->query(
625
                                        QString( "SELECT labelid FROM tags_labels WHERE deviceid = %1 AND url = '%2';" )
626
                                                 .arg( QString::number( newDeviceid ), db->escape( newRpath ) ) );
627
            QString existingLabelids;
628
            if( !labelids.isEmpty() )
629
            {
630
                existingLabelids = " AND labelid NOT IN (";
631
                oldForeach( labelids )
632
                {
633
                    if( it != labelids.constBegin() )
634
                        existingLabelids += ',';
635
                    existingLabelids += *it;
636
                }
637
                existingLabelids += ')';
638
            }
639
            QString sql = QString( "UPDATE tags_labels SET deviceid = %1, url = '%2' "
640
                                    "WHERE deviceid = %3 AND url = '%4'%5;" )
641
                                    .arg( newDeviceid )
642
                                    .arg( db->escape( newRpath ),
643
                                          QString::number( deviceid ),
644
                                          db->escape( rpath ),
645
                                          existingLabelids );
646
            db->query( sql );
647
        }
648
    }
649
#endif
650
}
651
652
#include "MountPointManager.moc"