MTP: More prototyping work:

New media scanner test program
Media scanner now cleans up after files that no longer exist
Separate database table for audio files
Extract metadata from audio files with libstagefright

Change-Id: I2bd0fe877836c741658e72fcfeb89c11be0d9b41
Signed-off-by: Mike Lockwood <lockwood@android.com>
diff --git a/media/mtp/MtpMediaScanner.cpp b/media/mtp/MtpMediaScanner.cpp
new file mode 100644
index 0000000..8b08f36
--- /dev/null
+++ b/media/mtp/MtpMediaScanner.cpp
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MtpDatabase.h"
+#include "MtpMediaScanner.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+#include <limits.h>
+
+#include <media/mediascanner.h>
+#include <media/stagefright/StagefrightMediaScanner.h>
+
+namespace android {
+
+class MtpMediaScannerClient : public MediaScannerClient
+{
+public:
+    MtpMediaScannerClient()
+    {
+        reset();
+    }
+
+    virtual ~MtpMediaScannerClient()
+    {
+    }
+
+    // returns true if it succeeded, false if an exception occured in the Java code
+    virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
+    {
+        printf("scanFile %s\n", path);
+        return true;
+    }
+
+    // returns true if it succeeded, false if an exception occured in the Java code
+    virtual bool handleStringTag(const char* name, const char* value)
+    {
+        int temp;
+
+        if (!strcmp(name, "title")) {
+            mTitle = value;
+            mHasTitle = true;
+        } else if  (!strcmp(name, "artist")) {
+            mArtist = value;
+            mHasArtist = true;
+        } else if  (!strcmp(name, "album")) {
+            mAlbum = value;
+            mHasAlbum = true;
+        } else if  (!strcmp(name, "albumartist")) {
+            mAlbumArtist = value;
+            mHasAlbumArtist = true;
+        } else if  (!strcmp(name, "genre")) {
+            // FIXME - handle numeric values here
+            mGenre = value;
+            mHasGenre = true;
+        } else if  (!strcmp(name, "composer")) {
+            mComposer = value;
+            mHasComposer = true;
+        } else if  (!strcmp(name, "tracknumber")) {
+            if (sscanf(value, "%d", &temp) == 1)
+                mTrack = temp;
+        } else if  (!strcmp(name, "discnumber")) {
+            // currently unused
+        } else if  (!strcmp(name, "year") || !strcmp(name, "date")) {
+            if (sscanf(value, "%d", &temp) == 1)
+                mYear = temp;
+        } else if  (!strcmp(name, "duration")) {
+            if (sscanf(value, "%d", &temp) == 1)
+                mDuration = temp;
+        } else {
+            printf("handleStringTag %s : %s\n", name, value);
+        }
+        return true;
+    }
+
+    // returns true if it succeeded, false if an exception occured in the Java code
+    virtual bool setMimeType(const char* mimeType)
+    {
+        mMimeType = mimeType;
+        mHasMimeType = true;
+        return true;
+    }
+
+    // returns true if it succeeded, false if an exception occured in the Java code
+    virtual bool addNoMediaFolder(const char* path)
+    {
+        printf("addNoMediaFolder %s\n", path);
+        return true;
+    }
+
+    void reset()
+    {
+        mHasTitle = false;
+        mHasArtist = false;
+        mHasAlbum = false;
+        mHasAlbumArtist = false;
+        mHasGenre = false;
+        mHasComposer = false;
+        mHasMimeType = false;
+        mTrack = mYear = mDuration = 0;
+    }
+
+    inline const char* getTitle() const { return mHasTitle ? (const char *)mTitle : NULL; }
+    inline const char* getArtist() const { return mHasArtist ? (const char *)mArtist : NULL; }
+    inline const char* getAlbum() const { return mHasAlbum ? (const char *)mAlbum : NULL; }
+    inline const char* getAlbumArtist() const { return mHasAlbumArtist ? (const char *)mAlbumArtist : NULL; }
+    inline const char* getGenre() const { return mHasGenre ? (const char *)mGenre : NULL; }
+    inline const char* getComposer() const { return mHasComposer ? (const char *)mComposer : NULL; }
+    inline const char* getMimeType() const { return mHasMimeType ? (const char *)mMimeType : NULL; }
+    inline int getTrack() const { return mTrack; }
+    inline int getYear() const { return mYear; }
+    inline int getDuration() const { return mDuration; }
+
+private:
+    MtpString   mTitle;
+    MtpString   mArtist;
+    MtpString   mAlbum;
+    MtpString   mAlbumArtist;
+    MtpString   mGenre;
+    MtpString   mComposer;
+    MtpString   mMimeType;
+
+    bool        mHasTitle;
+    bool        mHasArtist;
+    bool        mHasAlbum;
+    bool        mHasAlbumArtist;
+    bool        mHasGenre;
+    bool        mHasComposer;
+    bool        mHasMimeType;
+
+    int         mTrack;
+    int         mYear;
+    int         mDuration;
+};
+
+
+MtpMediaScanner::MtpMediaScanner(MtpStorageID id, const char* filePath, MtpDatabase* db)
+    :   mStorageID(id),
+        mFilePath(filePath),
+        mDatabase(db),
+        mMediaScanner(NULL),
+        mMediaScannerClient(NULL),
+        mFileList(NULL),
+        mFileCount(0)
+{
+    mMediaScanner = new StagefrightMediaScanner;
+    mMediaScannerClient = new MtpMediaScannerClient;
+}
+
+MtpMediaScanner::~MtpMediaScanner() {
+}
+
+bool MtpMediaScanner::scanFiles() {
+    mDatabase->beginTransaction();
+    mFileCount = 0;
+    mFileList = mDatabase->getFileList(mFileCount);
+
+    int ret = scanDirectory(mFilePath, MTP_PARENT_ROOT);
+
+    for (int i = 0; i < mFileCount; i++) {
+        MtpObjectHandle test = mFileList[i];
+        if (! (test & kObjectHandleMarkBit)) {
+            printf("delete missing file %08X\n", test);
+            mDatabase->deleteFile(test);
+        }
+    }
+
+    delete[] mFileList;
+    mFileCount = 0;
+    mDatabase->commitTransaction();
+    return (ret == 0);
+}
+
+
+static const struct MediaFileTypeEntry
+{
+    const char*     extension;
+    MtpObjectFormat format;
+    uint32_t        table;
+} sFileTypes[] =
+{
+    { "MP3",    MTP_FORMAT_MP3,             kObjectHandleTableAudio     },
+    { "M4A",    MTP_FORMAT_UNDEFINED_AUDIO, kObjectHandleTableAudio     },
+    { "WAV",    MTP_FORMAT_WAV,             kObjectHandleTableAudio     },
+    { "AMR",    MTP_FORMAT_UNDEFINED_AUDIO, kObjectHandleTableAudio     },
+    { "AWB",    MTP_FORMAT_UNDEFINED_AUDIO, kObjectHandleTableAudio     },
+    { "WMA",    MTP_FORMAT_WMA,             kObjectHandleTableAudio     },
+    { "OGG",    MTP_FORMAT_OGG,             kObjectHandleTableAudio     },
+    { "OGA",    MTP_FORMAT_UNDEFINED_AUDIO, kObjectHandleTableAudio     },
+    { "AAC",    MTP_FORMAT_AAC,             kObjectHandleTableAudio     },
+    { "MID",    MTP_FORMAT_UNDEFINED_AUDIO, kObjectHandleTableAudio     },
+    { "MIDI",   MTP_FORMAT_UNDEFINED_AUDIO, kObjectHandleTableAudio     },
+    { "XMF",    MTP_FORMAT_UNDEFINED_AUDIO, kObjectHandleTableAudio     },
+    { "RTTTL",  MTP_FORMAT_UNDEFINED_AUDIO, kObjectHandleTableAudio     },
+    { "SMF",    MTP_FORMAT_UNDEFINED_AUDIO, kObjectHandleTableAudio     },
+    { "IMY",    MTP_FORMAT_UNDEFINED_AUDIO, kObjectHandleTableAudio     },
+    { "RTX",    MTP_FORMAT_UNDEFINED_AUDIO, kObjectHandleTableAudio     },
+    { "OTA",    MTP_FORMAT_UNDEFINED_AUDIO, kObjectHandleTableAudio     },
+    { "MPEG",   MTP_FORMAT_UNDEFINED_VIDEO, kObjectHandleTableVideo     },
+    { "MP4",    MTP_FORMAT_UNDEFINED_VIDEO, kObjectHandleTableVideo     },
+    { "M4V",    MTP_FORMAT_UNDEFINED_VIDEO, kObjectHandleTableVideo     },
+    { "3GP",    MTP_FORMAT_UNDEFINED_VIDEO, kObjectHandleTableVideo     },
+    { "3GPP",   MTP_FORMAT_UNDEFINED_VIDEO, kObjectHandleTableVideo     },
+    { "3G2",    MTP_FORMAT_UNDEFINED_VIDEO, kObjectHandleTableVideo     },
+    { "3GPP2",  MTP_FORMAT_UNDEFINED_VIDEO, kObjectHandleTableVideo     },
+    { "WMV",    MTP_FORMAT_UNDEFINED_VIDEO, kObjectHandleTableVideo     },
+    { "ASF",    MTP_FORMAT_UNDEFINED_VIDEO, kObjectHandleTableVideo     },
+    { "JPG",    MTP_FORMAT_EXIF_JPEG,       kObjectHandleTableImage     },
+    { "JPEG",   MTP_FORMAT_EXIF_JPEG,       kObjectHandleTableImage     },
+    { "GIF",    MTP_FORMAT_GIF,             kObjectHandleTableImage     },
+    { "PNG",    MTP_FORMAT_PNG,             kObjectHandleTableImage     },
+    { "BMP",    MTP_FORMAT_BMP,             kObjectHandleTableImage     },
+    { "WBMP",   MTP_FORMAT_BMP,             kObjectHandleTableImage     },
+    { "M3U",    MTP_FORMAT_M3U_PLAYLIST,    kObjectHandleTablePlaylist  },
+    { "PLS",    MTP_FORMAT_PLS_PLAYLIST,    kObjectHandleTablePlaylist  },
+    { "WPL",    MTP_FORMAT_WPL_PLAYLIST,    kObjectHandleTablePlaylist  },
+};
+
+MtpObjectFormat MtpMediaScanner::getFileFormat(const char* path, uint32_t& table)
+{
+    const char* extension = strrchr(path, '.');
+    if (!extension)
+        return MTP_FORMAT_UNDEFINED;
+    extension++; // skip the dot
+
+    for (unsigned i = 0; i < sizeof(sFileTypes) / sizeof(sFileTypes[0]); i++) {
+        if (!strcasecmp(extension, sFileTypes[i].extension)) {
+            table = sFileTypes[i].table;
+            return sFileTypes[i].format;
+        }
+    }
+    table = kObjectHandleTableFile;
+    return MTP_FORMAT_UNDEFINED;
+}
+
+int MtpMediaScanner::scanDirectory(const char* path, MtpObjectHandle parent)
+{
+    char buffer[PATH_MAX];
+    struct dirent* entry;
+
+    unsigned length = strlen(path);
+    if (length > sizeof(buffer) + 2) {
+        fprintf(stderr, "path too long: %s\n", path);
+    }
+
+    DIR* dir = opendir(path);
+    if (!dir) {
+        fprintf(stderr, "opendir %s failed, errno: %d", path, errno);
+        return -1;
+    }
+
+    strncpy(buffer, path, sizeof(buffer));
+    char* fileStart = buffer + length;
+    // make sure we have a trailing slash
+    if (fileStart[-1] != '/') {
+        *(fileStart++) = '/';
+    }
+    int fileNameLength = sizeof(buffer) + fileStart - buffer;
+
+    while ((entry = readdir(dir))) {
+        const char* name = entry->d_name;
+
+        // ignore "." and "..", as well as any files or directories staring with dot
+        if (name[0] == '.') {
+            continue;
+        }
+        if (strlen(name) + 1 > fileNameLength) {
+            fprintf(stderr, "path too long for %s\n", name);
+            continue;
+        }
+        strcpy(fileStart, name);
+
+        struct stat statbuf;
+        memset(&statbuf, 0, sizeof(statbuf));
+        stat(buffer, &statbuf);
+
+        if (entry->d_type == DT_DIR) {
+            MtpObjectHandle handle = mDatabase->getObjectHandle(buffer);
+            if (handle) {
+                markFile(handle);
+            } else {
+                handle = mDatabase->addFile(buffer, MTP_FORMAT_ASSOCIATION,
+                        parent, mStorageID, 0, statbuf.st_mtime);
+            }
+            scanDirectory(buffer, handle);
+        } else if (entry->d_type == DT_REG) {
+            scanFile(buffer, parent, statbuf);
+        }
+    }
+
+    closedir(dir);
+    return 0;
+}
+
+void MtpMediaScanner::scanFile(const char* path, MtpObjectHandle parent, struct stat& statbuf) {
+    uint32_t table;
+    MtpObjectFormat format = getFileFormat(path, table);
+    // don't scan unknown file types
+    if (format == MTP_FORMAT_UNDEFINED)
+        return;
+    MtpObjectHandle handle = mDatabase->getObjectHandle(path);
+    // fixme - rescan if mod date changed
+    if (handle) {
+        markFile(handle);
+    } else {
+        mDatabase->beginTransaction();
+        handle = mDatabase->addFile(path, format, parent, mStorageID,
+                statbuf.st_size, statbuf.st_mtime);
+        if (handle <= 0) {
+            fprintf(stderr, "addFile failed in MtpMediaScanner::scanFile()\n");
+            mDatabase->rollbackTransaction();
+            return;
+        }
+
+        if (table == kObjectHandleTableAudio) {
+            mMediaScannerClient->reset();
+            mMediaScanner->processFile(path, NULL, *mMediaScannerClient);
+            handle = mDatabase->addAudioFile(handle,
+                    mMediaScannerClient->getTitle(),
+                    mMediaScannerClient->getArtist(),
+                    mMediaScannerClient->getAlbum(),
+                    mMediaScannerClient->getAlbumArtist(),
+                    mMediaScannerClient->getGenre(),
+                    mMediaScannerClient->getComposer(),
+                    mMediaScannerClient->getMimeType(),
+                    mMediaScannerClient->getTrack(),
+                    mMediaScannerClient->getYear(),
+                    mMediaScannerClient->getDuration());
+        }
+        mDatabase->commitTransaction();
+    }
+}
+
+void MtpMediaScanner::markFile(MtpObjectHandle handle) {
+    if (mFileList) {
+        handle &= kObjectHandleIndexMask;
+        // binary search for the file in mFileList
+        int low = 0;
+        int high = mFileCount;
+        int index;
+
+        while (low < high) {
+            index = (low + high) >> 1;
+            MtpObjectHandle test = (mFileList[index] & kObjectHandleIndexMask);
+            if (handle < test)
+                high = index;       // item is less than index
+            else if (handle > test)
+                low = index + 1;    // item is greater than index
+            else {
+                mFileList[index] |= kObjectHandleMarkBit;
+                return;
+            }
+        }
+        fprintf(stderr, "file %d not found in mFileList\n", handle);
+    }
+}
+
+}  // namespace android