/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "MozMtpDatabase.h"
#include "MozMtpServer.h"

#include "base/message_loop.h"
#include "DeviceStorage.h"
#include "mozilla/ArrayUtils.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/Scoped.h"
#include "mozilla/Services.h"
#include "nsIFile.h"
#include "nsIObserverService.h"
#include "nsPrintfCString.h"
#include "nsString.h"
#include "prio.h"

#include <dirent.h>
#include <libgen.h>
#include <utime.h>
#include <sys/stat.h>

using namespace android;
using namespace mozilla;

namespace mozilla {
MOZ_TYPE_SPECIFIC_SCOPED_POINTER_TEMPLATE(ScopedCloseDir, PRDir, PR_CloseDir)
}

BEGIN_MTP_NAMESPACE

static const char* kMtpWatcherNotify = "mtp-watcher-notify";

#if 0
// Some debug code for figuring out deadlocks, if you happen to run into
// that scenario

class DebugMutexAutoLock: public MutexAutoLock
{
public:
  DebugMutexAutoLock(mozilla::Mutex& aMutex)
    : MutexAutoLock(aMutex)
  {
    MTP_LOG("Mutex acquired");
  }

  ~DebugMutexAutoLock()
  {
    MTP_LOG("Releasing mutex");
  }
};
#define MutexAutoLock  MTP_LOG("About to enter mutex"); DebugMutexAutoLock

#endif

static const char *
ObjectPropertyAsStr(MtpObjectProperty aProperty)
{
  switch (aProperty) {
    case MTP_PROPERTY_STORAGE_ID:         return "MTP_PROPERTY_STORAGE_ID";
    case MTP_PROPERTY_OBJECT_FORMAT:      return "MTP_PROPERTY_OBJECT_FORMAT";
    case MTP_PROPERTY_PROTECTION_STATUS:  return "MTP_PROPERTY_PROTECTION_STATUS";
    case MTP_PROPERTY_OBJECT_SIZE:        return "MTP_PROPERTY_OBJECT_SIZE";
    case MTP_PROPERTY_OBJECT_FILE_NAME:   return "MTP_PROPERTY_OBJECT_FILE_NAME";
    case MTP_PROPERTY_DATE_CREATED:       return "MTP_PROPERTY_DATE_CREATED";
    case MTP_PROPERTY_DATE_MODIFIED:      return "MTP_PROPERTY_DATE_MODIFIED";
    case MTP_PROPERTY_PARENT_OBJECT:      return "MTP_PROPERTY_PARENT_OBJECT";
    case MTP_PROPERTY_PERSISTENT_UID:     return "MTP_PROPERTY_PERSISTENT_UID";
    case MTP_PROPERTY_NAME:               return "MTP_PROPERTY_NAME";
    case MTP_PROPERTY_DATE_ADDED:         return "MTP_PROPERTY_DATE_ADDED";
    case MTP_PROPERTY_WIDTH:              return "MTP_PROPERTY_WIDTH";
    case MTP_PROPERTY_HEIGHT:             return "MTP_PROPERTY_HEIGHT";
    case MTP_PROPERTY_IMAGE_BIT_DEPTH:    return "MTP_PROPERTY_IMAGE_BIT_DEPTH";
    case MTP_PROPERTY_DISPLAY_NAME:       return "MTP_PROPERTY_DISPLAY_NAME";
  }
  return "MTP_PROPERTY_???";
}

static char*
FormatDate(time_t aTime, char *aDateStr, size_t aDateStrSize)
{
  struct tm tm;
  localtime_r(&aTime, &tm);
  MTP_LOG("(%ld) tm_zone = %s off = %ld", aTime, tm.tm_zone, tm.tm_gmtoff);
  strftime(aDateStr, aDateStrSize, "%Y%m%dT%H%M%S", &tm);
  return aDateStr;
}

MozMtpDatabase::MozMtpDatabase()
  : mMutex("MozMtpDatabase::mMutex"),
    mDb(mMutex),
    mStorage(mMutex),
    mBeginSendObjectCalled(false)
{
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  // We use the index into the array as the handle. Since zero isn't a valid
  // index, we stick a dummy entry there.

  RefPtr<DbEntry> dummy;

  MutexAutoLock lock(mMutex);
  mDb.AppendElement(dummy);
}

//virtual
MozMtpDatabase::~MozMtpDatabase()
{
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
}

void
MozMtpDatabase::AddEntry(DbEntry *entry)
{
  MutexAutoLock lock(mMutex);

  entry->mHandle = GetNextHandle();
  MOZ_ASSERT(mDb.Length() == entry->mHandle);
  mDb.AppendElement(entry);

  MTP_DBG("Handle: 0x%08x Parent: 0x%08x Path:'%s'",
          entry->mHandle, entry->mParent, entry->mPath.get());
}

void
MozMtpDatabase::AddEntryAndNotify(DbEntry* entry, RefCountedMtpServer* aMtpServer)
{
  AddEntry(entry);
  aMtpServer->sendObjectAdded(entry->mHandle);
}

void
MozMtpDatabase::DumpEntries(const char* aLabel)
{
  MutexAutoLock lock(mMutex);

  ProtectedDbArray::size_type numEntries = mDb.Length();
  MTP_LOG("%s: numEntries = %d", aLabel, numEntries);
  ProtectedDbArray::index_type entryIndex;
  for (entryIndex = 1; entryIndex < numEntries; entryIndex++) {
    RefPtr<DbEntry> entry = mDb[entryIndex];
    if (entry) {
      MTP_LOG("%s: mDb[%d]: mHandle: 0x%08x mParent: 0x%08x StorageID: 0x%08x path: '%s'",
              aLabel, entryIndex, entry->mHandle, entry->mParent, entry->mStorageID, entry->mPath.get());
    } else {
      MTP_LOG("%s: mDb[%2d]: entry is NULL", aLabel, entryIndex);
    }
  }
}

MtpObjectHandle
MozMtpDatabase::FindEntryByPath(const nsACString& aPath)
{
  MutexAutoLock lock(mMutex);

  ProtectedDbArray::size_type numEntries = mDb.Length();
  ProtectedDbArray::index_type entryIndex;
  for (entryIndex = 1; entryIndex < numEntries; entryIndex++) {
    RefPtr<DbEntry> entry = mDb[entryIndex];
    if (entry && entry->mPath.Equals(aPath)) {
      return entryIndex;
    }
  }
  return 0;
}

already_AddRefed<MozMtpDatabase::DbEntry>
MozMtpDatabase::GetEntry(MtpObjectHandle aHandle)
{
  MutexAutoLock lock(mMutex);

  RefPtr<DbEntry> entry;

  if (aHandle > 0 && aHandle < mDb.Length()) {
    entry = mDb[aHandle];
  }
  return entry.forget();
}

void
MozMtpDatabase::RemoveEntry(MtpObjectHandle aHandle)
{
  MutexAutoLock lock(mMutex);
  if (!IsValidHandle(aHandle)) {
    return;
  }

  RefPtr<DbEntry> removedEntry = mDb[aHandle];
  mDb[aHandle] = nullptr;
  MTP_DBG("0x%08x removed", aHandle);
  // if the entry is not a folder, just return.
  if (removedEntry->mObjectFormat != MTP_FORMAT_ASSOCIATION) {
    return;
  }

  // Find out and remove the children of aHandle.
  // Since the index for a directory will always be less than the index of any of its children,
  // we can remove the entire subtree in one pass.
  ProtectedDbArray::size_type numEntries = mDb.Length();
  ProtectedDbArray::index_type entryIndex;
  for (entryIndex = aHandle+1; entryIndex < numEntries; entryIndex++) {
    RefPtr<DbEntry> entry = mDb[entryIndex];
    if (entry && IsValidHandle(entry->mParent) && !mDb[entry->mParent]) {
      mDb[entryIndex] = nullptr;
      MTP_DBG("0x%08x removed", aHandle);
    }
  }
}

void
MozMtpDatabase::RemoveEntryAndNotify(MtpObjectHandle aHandle, RefCountedMtpServer* aMtpServer)
{
  RemoveEntry(aHandle);
  aMtpServer->sendObjectRemoved(aHandle);
}

void
MozMtpDatabase::UpdateEntryAndNotify(MtpObjectHandle aHandle, DeviceStorageFile* aFile, RefCountedMtpServer* aMtpServer)
{
  UpdateEntry(aHandle, aFile);
  aMtpServer->sendObjectAdded(aHandle);
}


void
MozMtpDatabase::UpdateEntry(MtpObjectHandle aHandle, DeviceStorageFile* aFile)
{
  MutexAutoLock lock(mMutex);

  RefPtr<DbEntry> entry = mDb[aHandle];

  int64_t fileSize = 0;
  aFile->mFile->GetFileSize(&fileSize);
  entry->mObjectSize = fileSize;

  PRTime dateModifiedMsecs;
  // GetLastModifiedTime returns msecs
  aFile->mFile->GetLastModifiedTime(&dateModifiedMsecs);
  entry->mDateModified = dateModifiedMsecs / PR_MSEC_PER_SEC;
  entry->mDateCreated = entry->mDateModified;
  entry->mDateAdded = entry->mDateModified;

  #if USE_DEBUG
  char dateStr[20];
  MTP_DBG("UpdateEntry (0x%08x file %s) modified (%ld) %s",
          entry->mHandle, entry->mPath.get(),
          entry->mDateModified,
          FormatDate(entry->mDateModified, dateStr, sizeof(dateStr)));
  #endif
}


class MtpWatcherNotifyRunnable final : public Runnable
{
public:
  MtpWatcherNotifyRunnable(nsACString& aStorageName,
                           nsACString& aPath,
                           const char* aEventType)
    : mStorageName(aStorageName),
      mPath(aPath),
      mEventType(aEventType)
  {}

  NS_IMETHOD Run() override
  {
    MOZ_ASSERT(NS_IsMainThread());

    NS_ConvertUTF8toUTF16 storageName(mStorageName);
    NS_ConvertUTF8toUTF16 path(mPath);

    RefPtr<DeviceStorageFile> dsf(
      new DeviceStorageFile(NS_LITERAL_STRING(DEVICESTORAGE_SDCARD),
                            storageName, path));
    NS_ConvertUTF8toUTF16 eventType(mEventType);
    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();

    MTP_DBG("Sending mtp-watcher-notify %s %s %s",
            mEventType.get(), mStorageName.get(), mPath.get());

    obs->NotifyObservers(dsf, kMtpWatcherNotify, eventType.get());
    return NS_OK;
  }

private:
  nsCString mStorageName;
  nsCString mPath;
  nsCString mEventType;
};

// MtpWatcherNotify is used to tell DeviceStorage when a file was changed
// through the MTP server.
void
MozMtpDatabase::MtpWatcherNotify(DbEntry* aEntry, const char* aEventType)
{
  // This function gets called from the MozMtpServer::mServerThread
  MOZ_ASSERT(!NS_IsMainThread());

  MTP_DBG("file: %s %s", aEntry->mPath.get(), aEventType);

  // Tell interested parties that a file was created, deleted, or modified.

  RefPtr<StorageEntry> storageEntry;
  {
    MutexAutoLock lock(mMutex);

    // FindStorage and the mStorage[] access both need to have the mutex held.
    StorageArray::index_type storageIndex = FindStorage(aEntry->mStorageID);
    if (storageIndex == StorageArray::NoIndex) {
      return;
    }
    storageEntry = mStorage[storageIndex];
  }

  // DeviceStorage wants the storageName and the path relative to the root
  // of the storage area, so we need to strip off the storagePath

  nsAutoCString relPath(Substring(aEntry->mPath,
                                  storageEntry->mStoragePath.Length() + 1));

  RefPtr<MtpWatcherNotifyRunnable> r =
    new MtpWatcherNotifyRunnable(storageEntry->mStorageName, relPath, aEventType);
  DebugOnly<nsresult> rv = NS_DispatchToMainThread(r);
  MOZ_ASSERT(NS_SUCCEEDED(rv));
}

// Called to tell the MTP server about new or deleted files,
void
MozMtpDatabase::MtpWatcherUpdate(RefCountedMtpServer* aMtpServer,
                                 DeviceStorageFile* aFile,
                                 const nsACString& aEventType)
{
  // Runs on the MtpWatcherUpdate->mIOThread (see MozMtpServer.cpp)
  MOZ_ASSERT(!NS_IsMainThread());

  // Figure out which storage the belongs to (if any)

  if (!aFile->mFile) {
    // No path - don't bother looking.
    return;
  }
  nsString wideFilePath;
  aFile->mFile->GetPath(wideFilePath);
  NS_ConvertUTF16toUTF8 filePath(wideFilePath);

  nsCString evtType(aEventType);
  MTP_LOG("file %s %s", filePath.get(), evtType.get());

  MtpObjectHandle entryHandle = FindEntryByPath(filePath);

  if (aEventType.EqualsLiteral("modified")) {
    // To update the file information to the newest, we remove the entry for
    // the existing file, then re-add the entry for the file.

    if (entryHandle != 0) {
      // Update entry for the file and tell MTP.
      MTP_LOG("About to update handle 0x%08x file %s", entryHandle, filePath.get());
      UpdateEntryAndNotify(entryHandle, aFile, aMtpServer);
    }
    else {
      // Create entry for the file and tell MTP.
      CreateEntryForFileAndNotify(filePath, aFile, aMtpServer);
    }
    return;
  }

  if (aEventType.EqualsLiteral("deleted")) {
    if (entryHandle == 0) {
      // The entry has already been removed. We can't tell MTP.
      return;
    }
    MTP_LOG("About to call sendObjectRemoved Handle 0x%08x file %s", entryHandle, filePath.get());
    RemoveEntryAndNotify(entryHandle, aMtpServer);
    return;
  }
}

nsCString
MozMtpDatabase::BaseName(const nsCString& path)
{
  nsCOMPtr<nsIFile> file;
  NS_NewNativeLocalFile(path, false, getter_AddRefs(file));
  if (file) {
    nsCString leafName;
    file->GetNativeLeafName(leafName);
    return leafName;
  }
  return path;
}

static nsCString
GetPathWithoutFileName(const nsCString& aFullPath)
{
  nsCString path;

  int32_t offset = aFullPath.RFindChar('/');
  if (offset != kNotFound) {
    // The trailing slash will be as part of 'path'
    path = StringHead(aFullPath, offset + 1);
  }

  MTP_LOG("returning '%s'", path.get());

  return path;
}

void
MozMtpDatabase::CreateEntryForFileAndNotify(const nsACString& aPath,
                                            DeviceStorageFile* aFile,
                                            RefCountedMtpServer* aMtpServer)
{
  // Find the StorageID that this path corresponds to.

  nsCString remainder;
  MtpStorageID storageID = FindStorageIDFor(aPath, remainder);
  if (storageID == 0) {
    // The path in question isn't for a storage area we're monitoring.
    nsCString path(aPath);
    return;
  }

  bool exists = false;
  aFile->mFile->Exists(&exists);
  if (!exists) {
    // File doesn't exist, no sense telling MTP about it.
    // This could happen if Device Storage created and deleted a file right
    // away. Since the notifications wind up being async, the file might
    // not exist any more.
    return;
  }

  // Now walk the remaining directories, finding or creating as required.

  MtpObjectHandle parent = MTP_PARENT_ROOT;
  bool doFind = true;
  int32_t offset = aPath.Length() - remainder.Length();
  int32_t slash;

  do {
    nsDependentCSubstring component;
    slash = aPath.FindChar('/', offset);
    if (slash == kNotFound) {
      component.Rebind(aPath, 0, aPath.Length());
    } else {
      component.Rebind(aPath, 0 , slash);
    }
    if (doFind) {
      MtpObjectHandle entryHandle = FindEntryByPath(component);
      if (entryHandle != 0) {
        // We found an entry.
        parent = entryHandle;
        offset = slash + 1 ;
        continue;
      }
    }

    // We've got a directory component that doesn't exist. This means that all
    // further subdirectories won't exist either, so we can skip searching
    // for them.
    doFind = false;

    // This directory and the file don't exist, create them

    RefPtr<DbEntry> entry = new DbEntry;

    entry->mStorageID = storageID;
    entry->mObjectName = Substring(aPath, offset, slash - offset);
    entry->mParent = parent;
    entry->mDisplayName = entry->mObjectName;
    entry->mPath = component;

    if (slash == kNotFound) {
      // No slash - this is the file component
      entry->mObjectFormat = MTP_FORMAT_DEFINED;

      int64_t fileSize = 0;
      aFile->mFile->GetFileSize(&fileSize);
      entry->mObjectSize = fileSize;

      // Note: Even though PRTime records usec, GetLastModifiedTime returns
      //       msecs.
      PRTime dateModifiedMsecs;
      aFile->mFile->GetLastModifiedTime(&dateModifiedMsecs);
      entry->mDateModified = dateModifiedMsecs / PR_MSEC_PER_SEC;
    } else {
      // Found a slash, this makes this a directory component
      entry->mObjectFormat = MTP_FORMAT_ASSOCIATION;
      entry->mObjectSize = 0;
      time(&entry->mDateModified);
    }
    entry->mDateCreated = entry->mDateModified;
    entry->mDateAdded = entry->mDateModified;

    AddEntryAndNotify(entry, aMtpServer);
    MTP_LOG("About to call sendObjectAdded Handle 0x%08x file %s", entry->mHandle, entry->mPath.get());

    parent = entry->mHandle;
    offset = slash + 1;
  } while (slash != kNotFound);

  return;
}

void
MozMtpDatabase::AddDirectory(MtpStorageID aStorageID,
                             const char* aPath,
                             MtpObjectHandle aParent)
{
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  ScopedCloseDir dir;

  if (!(dir = PR_OpenDir(aPath))) {
    MTP_ERR("Unable to open directory '%s'", aPath);
    return;
  }

  PRDirEntry* dirEntry;
  while ((dirEntry = PR_ReadDir(dir, PR_SKIP_BOTH))) {
    nsPrintfCString filename("%s/%s", aPath, dirEntry->name);
    PRFileInfo64 fileInfo;
    if (PR_GetFileInfo64(filename.get(), &fileInfo) != PR_SUCCESS) {
      MTP_ERR("Unable to retrieve file information for '%s'", filename.get());
      continue;
    }

    RefPtr<DbEntry> entry = new DbEntry;

    entry->mStorageID = aStorageID;
    entry->mParent = aParent;
    entry->mObjectName = dirEntry->name;
    entry->mDisplayName = dirEntry->name;
    entry->mPath = filename;

    // PR_GetFileInfo64 returns timestamps in usecs
    entry->mDateModified = fileInfo.modifyTime / PR_USEC_PER_SEC;
    entry->mDateCreated = fileInfo.creationTime / PR_USEC_PER_SEC;
    time(&entry->mDateAdded);

    if (fileInfo.type == PR_FILE_FILE) {
      entry->mObjectFormat = MTP_FORMAT_DEFINED;
      //TODO: Check how 64-bit filesize are dealt with
      entry->mObjectSize = fileInfo.size;
      AddEntry(entry);
    } else if (fileInfo.type == PR_FILE_DIRECTORY) {
      entry->mObjectFormat = MTP_FORMAT_ASSOCIATION;
      entry->mObjectSize = 0;
      AddEntry(entry);
      AddDirectory(aStorageID, filename.get(), entry->mHandle);
    }
  }
}

MozMtpDatabase::StorageArray::index_type
MozMtpDatabase::FindStorage(MtpStorageID aStorageID)
{
  // Currently, this routine is called from MozMtpDatabase::RemoveStorage
  // and MozMtpDatabase::MtpWatcherNotify, which both hold mMutex.

  StorageArray::size_type numStorages = mStorage.Length();
  StorageArray::index_type storageIndex;

  for (storageIndex = 0; storageIndex < numStorages; storageIndex++) {
    RefPtr<StorageEntry> storage = mStorage[storageIndex];
    if (storage->mStorageID == aStorageID) {
      return storageIndex;
    }
  }
  return StorageArray::NoIndex;
}

// Find the storage ID for the storage area that contains aPath.
MtpStorageID
MozMtpDatabase::FindStorageIDFor(const nsACString& aPath, nsCSubstring& aRemainder)
{
  MutexAutoLock lock(mMutex);

  aRemainder.Truncate();

  StorageArray::size_type numStorages = mStorage.Length();
  StorageArray::index_type storageIndex;

  for (storageIndex = 0; storageIndex < numStorages; storageIndex++) {
    RefPtr<StorageEntry> storage = mStorage[storageIndex];
    if (StringHead(aPath, storage->mStoragePath.Length()).Equals(storage->mStoragePath)) {
      if (aPath.Length() == storage->mStoragePath.Length()) {
        return storage->mStorageID;
      }
      if (aPath[storage->mStoragePath.Length()] == '/') {
        aRemainder = Substring(aPath, storage->mStoragePath.Length() + 1);
        return storage->mStorageID;
      }
    }
  }
  return 0;
}

void
MozMtpDatabase::AddStorage(MtpStorageID aStorageID,
                           const char* aPath,
                           const char* aName)
{
  // This is called on the IOThread from MozMtpStorage::StorageAvailable
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  MTP_DBG("StorageID: 0x%08x aPath: '%s' aName: '%s'",
          aStorageID, aPath, aName);

  PRFileInfo  fileInfo;
  if (PR_GetFileInfo(aPath, &fileInfo) != PR_SUCCESS) {
    MTP_ERR("'%s' doesn't exist", aPath);
    return;
  }
  if (fileInfo.type != PR_FILE_DIRECTORY) {
    MTP_ERR("'%s' isn't a directory", aPath);
    return;
  }

  RefPtr<StorageEntry> storageEntry = new StorageEntry;

  storageEntry->mStorageID = aStorageID;
  storageEntry->mStoragePath = aPath;
  storageEntry->mStorageName = aName;
  {
    MutexAutoLock lock(mMutex);
    mStorage.AppendElement(storageEntry);
  }

  AddDirectory(aStorageID, aPath, MTP_PARENT_ROOT);
  {
    MutexAutoLock lock(mMutex);
    MTP_LOG("added %d items from tree '%s'", mDb.Length(), aPath);
  }
}

void
MozMtpDatabase::RemoveStorage(MtpStorageID aStorageID)
{
  MutexAutoLock lock(mMutex);

  // This is called on the IOThread from MozMtpStorage::StorageAvailable
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  ProtectedDbArray::size_type numEntries = mDb.Length();
  ProtectedDbArray::index_type entryIndex;
  for (entryIndex = 1; entryIndex < numEntries; entryIndex++) {
    RefPtr<DbEntry> entry = mDb[entryIndex];
    if (entry && entry->mStorageID == aStorageID) {
      mDb[entryIndex] = nullptr;
    }
  }
  StorageArray::index_type storageIndex = FindStorage(aStorageID);
  if (storageIndex != StorageArray::NoIndex) {
    mStorage.RemoveElementAt(storageIndex);
  }
}

// called from SendObjectInfo to reserve a database entry for the incoming file
//virtual
MtpObjectHandle
MozMtpDatabase::beginSendObject(const char* aPath,
                                MtpObjectFormat aFormat,
                                MtpObjectHandle aParent,
                                MtpStorageID aStorageID,
                                uint64_t aSize,
                                time_t aModified)
{
  // If MtpServer::doSendObjectInfo receives a request with a parent of
  // MTP_PARENT_ROOT, then it fills in aPath with the fully qualified path
  // and then passes in a parent of zero.

  if (aParent == 0) {
    // Undo what doSendObjectInfo did
    aParent = MTP_PARENT_ROOT;
  }

  RefPtr<DbEntry> entry = new DbEntry;

  entry->mStorageID = aStorageID;
  entry->mParent = aParent;
  entry->mPath = aPath;
  entry->mObjectName = BaseName(entry->mPath);
  entry->mDisplayName = entry->mObjectName;
  entry->mObjectFormat = aFormat;
  entry->mObjectSize = aSize;

  if (aModified != 0) {
    // Currently, due to the way that parseDateTime is coded in
    // frameworks/av/media/mtp/MtpUtils.cpp, aModified winds up being the number
    // of seconds from the epoch in local time, rather than UTC time. So we
    // need to convert it back to being relative to UTC since that's what linux
    // expects time_t to contain.
    //
    // In more concrete testable terms, if the host parses 2015-08-02 02:22:00
    // as a local time in the Pacific timezone, aModified will come to us as
    // 1438482120.
    //
    // What we want is what mktime would pass us with the same date. Using python
    // (because its simple) with the current timezone set to be America/Vancouver:
    //
    // >>> import time
    // >>> time.mktime((2015, 8, 2, 2, 22, 0, 0, 0, -1))
    // 1438507320.0
    // >>> time.localtime(1438507320)
    // time.struct_time(tm_year=2015, tm_mon=8, tm_mday=2, tm_hour=2, tm_min=22, tm_sec=0, tm_wday=6, tm_yday=214, tm_isdst=1)
    //
    // Currently, when a file has a modification time of 2015-08-22 02:22:00 PDT
    // then aModified will come in as 1438482120 which corresponds to
    // 2015-08-22 02:22:00 UTC

    struct tm tm;
    if (gmtime_r(&aModified, &tm) != NULL) {
      // GMT always comes back with tm_isdst = 0, so we set it to -1 in order
      // to have mktime figure out dst based on the date.
      tm.tm_isdst = -1;
      aModified = mktime(&tm);
      if (aModified == (time_t)-1) {
        aModified = 0;
      }
    } else {
      aModified = 0;
    }
  }
  if (aModified == 0) {
    // The ubuntu host doesn't pass in the modified/created times in the
    // SENDOBJECT packet, so aModified winds up being zero. About the best
    // we can do with that is to use the current time.
    time(&aModified);
  }

  // And just an FYI for anybody else looking at timestamps. Under OSX you
  // need to use the Android File Transfer program to copy files into the
  // phone. That utility passes in both date modified and date created
  // timestamps, but they're both equal to the time that the file was copied
  // and not the times that are associated with the files.

  // Now we have aModified in a traditional time_t format, which is the number
  // of seconds from the UTC epoch.

  entry->mDateModified = aModified;
  entry->mDateCreated = entry->mDateModified;
  entry->mDateAdded = entry->mDateModified;

  AddEntry(entry);

  #if USE_DEBUG
  char dateStr[20];
  MTP_LOG("Handle: 0x%08x Parent: 0x%08x Path: '%s' aModified %ld %s",
          entry->mHandle, aParent, aPath, aModified,
          FormatDate(entry->mDateModified, dateStr, sizeof(dateStr)));
  #endif

  mBeginSendObjectCalled = true;
  return entry->mHandle;
}

// called to report success or failure of the SendObject file transfer
// success should signal a notification of the new object's creation,
// failure should remove the database entry created in beginSendObject

//virtual
void
MozMtpDatabase::endSendObject(const char* aPath,
                              MtpObjectHandle aHandle,
                              MtpObjectFormat aFormat,
                              bool aSucceeded)
{
  MTP_LOG("Handle: 0x%08x Path: '%s'", aHandle, aPath);

  if (aSucceeded) {
    RefPtr<DbEntry> entry = GetEntry(aHandle);
    if (entry) {
      // The android MTP server only copies the data in, it doesn't set the
      // modified timestamp, so we do that here.

      struct utimbuf new_times;
      struct stat sb;

      char dateStr[20];
      MTP_LOG("Path: '%s' setting modified time to (%ld) %s",
              entry->mPath.get(), entry->mDateModified,
              FormatDate(entry->mDateModified, dateStr, sizeof(dateStr)));

      stat(entry->mPath.get(), &sb);
      new_times.actime = sb.st_atime;   // Preserve atime
      new_times.modtime = entry->mDateModified;
      utime(entry->mPath.get(), &new_times);

      MtpWatcherNotify(entry, "modified");
    }
  } else {
    RemoveEntry(aHandle);
  }
  mBeginSendObjectCalled = false;
}

//virtual
MtpObjectHandleList*
MozMtpDatabase::getObjectList(MtpStorageID aStorageID,
                              MtpObjectFormat aFormat,
                              MtpObjectHandle aParent)
{
  MTP_LOG("StorageID: 0x%08x Format: 0x%04x Parent: 0x%08x",
          aStorageID, aFormat, aParent);

  // aStorageID == 0xFFFFFFFF for all storage
  // aFormat    == 0          for all formats
  // aParent    == 0xFFFFFFFF for objects with no parents
  // aParent    == 0          for all objects

  //TODO: Optimize

  UniquePtr<MtpObjectHandleList> list(new MtpObjectHandleList());

  MutexAutoLock lock(mMutex);

  ProtectedDbArray::size_type numEntries = mDb.Length();
  ProtectedDbArray::index_type entryIndex;
  for (entryIndex = 1; entryIndex < numEntries; entryIndex++) {
    RefPtr<DbEntry> entry = mDb[entryIndex];
    if (entry &&
        (aStorageID == 0xFFFFFFFF || entry->mStorageID == aStorageID) &&
        (aFormat == 0 || entry->mObjectFormat == aFormat) &&
        (aParent == 0 || entry->mParent == aParent)) {
      list->push(entry->mHandle);
    }
  }
  MTP_LOG("  returning %d items", list->size());
  return list.release();
}

//virtual
int
MozMtpDatabase::getNumObjects(MtpStorageID aStorageID,
                              MtpObjectFormat aFormat,
                              MtpObjectHandle aParent)
{
  MTP_LOG("");

  // aStorageID == 0xFFFFFFFF for all storage
  // aFormat    == 0          for all formats
  // aParent    == 0xFFFFFFFF for objects with no parents
  // aParent    == 0          for all objects

  int count = 0;

  MutexAutoLock lock(mMutex);

  ProtectedDbArray::size_type numEntries = mDb.Length();
  ProtectedDbArray::index_type entryIndex;
  for (entryIndex = 1; entryIndex < numEntries; entryIndex++) {
    RefPtr<DbEntry> entry = mDb[entryIndex];
    if (entry &&
        (aStorageID == 0xFFFFFFFF || entry->mStorageID == aStorageID) &&
        (aFormat == 0 || entry->mObjectFormat == aFormat) &&
        (aParent == 0 || entry->mParent == aParent)) {
      count++;
    }
  }

  MTP_LOG("  returning %d items", count);
  return count;
}

//virtual
MtpObjectFormatList*
MozMtpDatabase::getSupportedPlaybackFormats()
{
  static const uint16_t init_data[] = {MTP_FORMAT_UNDEFINED, MTP_FORMAT_ASSOCIATION,
                                       MTP_FORMAT_TEXT, MTP_FORMAT_HTML, MTP_FORMAT_WAV,
                                       MTP_FORMAT_MP3, MTP_FORMAT_MPEG, MTP_FORMAT_EXIF_JPEG,
                                       MTP_FORMAT_TIFF_EP, MTP_FORMAT_BMP, MTP_FORMAT_GIF,
                                       MTP_FORMAT_PNG, MTP_FORMAT_TIFF, MTP_FORMAT_WMA,
                                       MTP_FORMAT_OGG, MTP_FORMAT_AAC, MTP_FORMAT_MP4_CONTAINER,
                                       MTP_FORMAT_MP2, MTP_FORMAT_3GP_CONTAINER, MTP_FORMAT_FLAC};

  MtpObjectFormatList *list = new MtpObjectFormatList();
  list->appendArray(init_data, MOZ_ARRAY_LENGTH(init_data));

  MTP_LOG("returning Supported Playback Formats");
  return list;
}

//virtual
MtpObjectFormatList*
MozMtpDatabase::getSupportedCaptureFormats()
{
  static const uint16_t init_data[] = {MTP_FORMAT_ASSOCIATION, MTP_FORMAT_PNG};

  MtpObjectFormatList *list = new MtpObjectFormatList();
  list->appendArray(init_data, MOZ_ARRAY_LENGTH(init_data));
  MTP_LOG("returning MTP_FORMAT_ASSOCIATION, MTP_FORMAT_PNG");
  return list;
}

static const MtpObjectProperty sSupportedObjectProperties[] =
{
  MTP_PROPERTY_STORAGE_ID,
  MTP_PROPERTY_OBJECT_FORMAT,
  MTP_PROPERTY_PROTECTION_STATUS,   // UINT16 - always 0
  MTP_PROPERTY_OBJECT_SIZE,
  MTP_PROPERTY_OBJECT_FILE_NAME,    // just the filename - no directory
  MTP_PROPERTY_NAME,
  MTP_PROPERTY_DATE_CREATED,
  MTP_PROPERTY_DATE_MODIFIED,
  MTP_PROPERTY_PARENT_OBJECT,
  MTP_PROPERTY_PERSISTENT_UID,
  MTP_PROPERTY_DATE_ADDED,
};

//virtual
MtpObjectPropertyList*
MozMtpDatabase::getSupportedObjectProperties(MtpObjectFormat aFormat)
{
  MTP_LOG("");
  MtpObjectPropertyList *list = new MtpObjectPropertyList();
  list->appendArray(sSupportedObjectProperties,
                    MOZ_ARRAY_LENGTH(sSupportedObjectProperties));
  return list;
}

//virtual
MtpDevicePropertyList*
MozMtpDatabase::getSupportedDeviceProperties()
{
  MTP_LOG("");
  static const uint16_t init_data[] = { MTP_DEVICE_PROPERTY_UNDEFINED };

  MtpDevicePropertyList *list = new MtpDevicePropertyList();
  list->appendArray(init_data, MOZ_ARRAY_LENGTH(init_data));
  return list;
}

//virtual
MtpResponseCode
MozMtpDatabase::getObjectPropertyValue(MtpObjectHandle aHandle,
                                       MtpObjectProperty aProperty,
                                       MtpDataPacket& aPacket)
{
  RefPtr<DbEntry> entry = GetEntry(aHandle);
  if (!entry) {
    MTP_ERR("Invalid Handle: 0x%08x", aHandle);
    return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
  }

  MTP_LOG("Handle: 0x%08x '%s' Property: %s 0x%08x",
          aHandle, entry->mDisplayName.get(), ObjectPropertyAsStr(aProperty), aProperty);

  switch (aProperty)
  {
    case MTP_PROPERTY_STORAGE_ID:     aPacket.putUInt32(entry->mStorageID); break;
    case MTP_PROPERTY_PARENT_OBJECT:  aPacket.putUInt32(entry->mParent); break;
    case MTP_PROPERTY_OBJECT_FORMAT:  aPacket.putUInt16(entry->mObjectFormat); break;
    case MTP_PROPERTY_OBJECT_SIZE:    aPacket.putUInt64(entry->mObjectSize); break;
    case MTP_PROPERTY_DISPLAY_NAME:   aPacket.putString(entry->mDisplayName.get()); break;
    case MTP_PROPERTY_PERSISTENT_UID:
      // the same as aPacket.putUInt128
      aPacket.putUInt64(entry->mHandle);
      aPacket.putUInt64(entry->mStorageID);
      break;
    case MTP_PROPERTY_NAME:           aPacket.putString(entry->mDisplayName.get()); break;

    default:
      MTP_LOG("Invalid Property: 0x%08x", aProperty);
      return MTP_RESPONSE_INVALID_OBJECT_PROP_CODE;
  }

  return MTP_RESPONSE_OK;
}

static int
GetTypeOfObjectProp(MtpObjectProperty aProperty)
{
  struct PropertyTableEntry {
    MtpObjectProperty property;
    int type;
  };

  static const PropertyTableEntry kObjectPropertyTable[] = {
    {MTP_PROPERTY_STORAGE_ID,        MTP_TYPE_UINT32  },
    {MTP_PROPERTY_OBJECT_FORMAT,     MTP_TYPE_UINT16  },
    {MTP_PROPERTY_PROTECTION_STATUS, MTP_TYPE_UINT16  },
    {MTP_PROPERTY_OBJECT_SIZE,       MTP_TYPE_UINT64  },
    {MTP_PROPERTY_OBJECT_FILE_NAME,  MTP_TYPE_STR     },
    {MTP_PROPERTY_DATE_CREATED,      MTP_TYPE_STR     },
    {MTP_PROPERTY_DATE_MODIFIED,     MTP_TYPE_STR     },
    {MTP_PROPERTY_PARENT_OBJECT,     MTP_TYPE_UINT32  },
    {MTP_PROPERTY_DISPLAY_NAME,      MTP_TYPE_STR     },
    {MTP_PROPERTY_NAME,              MTP_TYPE_STR     },
    {MTP_PROPERTY_PERSISTENT_UID,    MTP_TYPE_UINT128 },
    {MTP_PROPERTY_DATE_ADDED,        MTP_TYPE_STR     },
  };

  int count = sizeof(kObjectPropertyTable) / sizeof(kObjectPropertyTable[0]);
  const PropertyTableEntry* entryProp = kObjectPropertyTable;
  int type = 0;

  for (int i = 0; i < count; ++i, ++entryProp) {
    if (entryProp->property == aProperty) {
      type = entryProp->type;
      break;
    }
  }

  return type;
}

//virtual
MtpResponseCode
MozMtpDatabase::setObjectPropertyValue(MtpObjectHandle aHandle,
                                       MtpObjectProperty aProperty,
                                       MtpDataPacket& aPacket)
{
  MTP_LOG("Handle: 0x%08x Property: 0x%08x", aHandle, aProperty);

  // Only support file name change
  if (aProperty != MTP_PROPERTY_OBJECT_FILE_NAME) {
    MTP_ERR("property 0x%x not supported", aProperty);
    return  MTP_RESPONSE_OBJECT_PROP_NOT_SUPPORTED;
  }

  if (GetTypeOfObjectProp(aProperty) != MTP_TYPE_STR) {
    MTP_ERR("property type 0x%x not supported", GetTypeOfObjectProp(aProperty));
    return MTP_RESPONSE_GENERAL_ERROR;
  }

  RefPtr<DbEntry> entry = GetEntry(aHandle);
  if (!entry) {
    MTP_ERR("Invalid Handle: 0x%08x", aHandle);
    return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
  }

  MtpStringBuffer buf;
  aPacket.getString(buf);

  nsDependentCString newFileName(buf);
  nsCString newFileFullPath(GetPathWithoutFileName(entry->mPath) + newFileName);

  if (PR_Rename(entry->mPath.get(), newFileFullPath.get()) != PR_SUCCESS) {
    MTP_ERR("Failed to rename '%s' to '%s'",
            entry->mPath.get(), newFileFullPath.get());
    return MTP_RESPONSE_GENERAL_ERROR;
  }

  MTP_LOG("renamed '%s' to '%s'", entry->mPath.get(), newFileFullPath.get());

  entry->mPath = newFileFullPath;
  entry->mObjectName = BaseName(entry->mPath);
  entry->mDisplayName = entry->mObjectName;

  return MTP_RESPONSE_OK;
}

//virtual
MtpResponseCode
MozMtpDatabase::getDevicePropertyValue(MtpDeviceProperty aProperty,
                                       MtpDataPacket& aPacket)
{
  MTP_LOG("(GENERAL ERROR)");
  return MTP_RESPONSE_GENERAL_ERROR;
}

//virtual
MtpResponseCode
MozMtpDatabase::setDevicePropertyValue(MtpDeviceProperty aProperty,
                                       MtpDataPacket& aPacket)
{
  MTP_LOG("(NOT SUPPORTED)");
  return MTP_RESPONSE_OPERATION_NOT_SUPPORTED;
}

//virtual
MtpResponseCode
MozMtpDatabase::resetDeviceProperty(MtpDeviceProperty aProperty)
{
  MTP_LOG("(NOT SUPPORTED)");
  return MTP_RESPONSE_OPERATION_NOT_SUPPORTED;
}

void
MozMtpDatabase::QueryEntries(MozMtpDatabase::MatchType aMatchType,
                             uint32_t aMatchField1,
                             uint32_t aMatchField2,
                             UnprotectedDbArray &result)
{
  MutexAutoLock lock(mMutex);

  ProtectedDbArray::size_type numEntries = mDb.Length();
  ProtectedDbArray::index_type entryIdx;
  RefPtr<DbEntry> entry;

  result.Clear();

  switch (aMatchType) {

    case MatchAll:
      for (entryIdx = 0; entryIdx < numEntries; entryIdx++) {
        if (mDb[entryIdx]) {
          result.AppendElement(mDb[entryIdx]);
        }
      }
      break;

    case MatchHandle:
      for (entryIdx = 0; entryIdx < numEntries; entryIdx++) {
        entry = mDb[entryIdx];
        if (entry && entry->mHandle == aMatchField1) {
          result.AppendElement(entry);
          // Handles are unique - return the one that we found.
          return;
        }
      }
      break;

    case MatchParent:
      for (entryIdx = 0; entryIdx < numEntries; entryIdx++) {
        entry = mDb[entryIdx];
        if (entry && entry->mParent == aMatchField1) {
          result.AppendElement(entry);
        }
      }
      break;

    case MatchFormat:
      for (entryIdx = 0; entryIdx < numEntries; entryIdx++) {
        entry = mDb[entryIdx];
        if (entry && entry->mObjectFormat == aMatchField1) {
          result.AppendElement(entry);
        }
      }
      break;

    case MatchHandleFormat:
      for (entryIdx = 0; entryIdx < numEntries; entryIdx++) {
        entry = mDb[entryIdx];
        if (entry && entry->mHandle == aMatchField1) {
          if (entry->mObjectFormat == aMatchField2) {
            result.AppendElement(entry);
          }
          // Only 1 entry can match my aHandle. So we can return early.
          return;
        }
      }
      break;

    case MatchParentFormat:
      for (entryIdx = 0; entryIdx < numEntries; entryIdx++) {
        entry = mDb[entryIdx];
        if (entry && entry->mParent == aMatchField1 && entry->mObjectFormat == aMatchField2) {
          result.AppendElement(entry);
        }
      }
      break;

    default:
      MOZ_ASSERT(!"Invalid MatchType");
  }
}

//virtual
MtpResponseCode
MozMtpDatabase::getObjectPropertyList(MtpObjectHandle aHandle,
                                      uint32_t aFormat,
                                      uint32_t aProperty,
                                      int aGroupCode,
                                      int aDepth,
                                      MtpDataPacket& aPacket)
{
  MTP_LOG("Handle: 0x%08x Format: 0x%08x aProperty: 0x%08x aGroupCode: %d aDepth %d",
          aHandle, aFormat, aProperty, aGroupCode, aDepth);

  if (aDepth > 1) {
    return MTP_RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED;
  }
  if (aGroupCode != 0) {
    return MTP_RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED;
  }

  MatchType matchType = MatchAll;
  uint32_t matchField1 = 0;
  uint32_t matchField2 = 0;

  // aHandle == 0 implies all objects at the root level
  // further specificed by aFormat and/or aDepth

  if (aFormat == 0) {
    if (aHandle == 0xffffffff) {
      // select all objects
      matchType = MatchAll;
    } else {
      if (aDepth == 1) {
        // select objects whose Parent matches aHandle
        matchType = MatchParent;
        matchField1 = aHandle;
      } else {
        // select object whose handle matches aHandle
        matchType = MatchHandle;
        matchField1 = aHandle;
      }
    }
  } else {
    if (aHandle == 0xffffffff) {
      // select all objects whose format matches aFormat
      matchType = MatchFormat;
      matchField1 = aFormat;
    } else {
      if (aDepth == 1) {
        // select objects whose Parent is aHandle and format matches aFormat
        matchType = MatchParentFormat;
        matchField1 = aHandle;
        matchField2 = aFormat;
      } else {
        // select objects whose handle is aHandle and format matches aFormat
        matchType = MatchHandleFormat;
        matchField1 = aHandle;
        matchField2 = aFormat;
      }
    }
  }

  UnprotectedDbArray result;
  QueryEntries(matchType, matchField1, matchField2, result);

  const MtpObjectProperty *objectPropertyList;
  size_t numObjectProperties = 0;
  MtpObjectProperty objectProperty;

  if (aProperty == 0xffffffff) {
    // return all supported properties
    numObjectProperties = MOZ_ARRAY_LENGTH(sSupportedObjectProperties);
    objectPropertyList = sSupportedObjectProperties;
  } else {
    // return property indicated by aProperty
    numObjectProperties = 1;
    objectProperty = aProperty;
    objectPropertyList = &objectProperty;
  }

  UnprotectedDbArray::size_type numEntries = result.Length();
  UnprotectedDbArray::index_type entryIdx;

  char dateStr[20];

  aPacket.putUInt32(numObjectProperties * numEntries);
  for (entryIdx = 0; entryIdx < numEntries; entryIdx++) {
    RefPtr<DbEntry> entry = result[entryIdx];

    for (size_t propertyIdx = 0; propertyIdx < numObjectProperties; propertyIdx++) {
      aPacket.putUInt32(entry->mHandle);
      MtpObjectProperty prop = objectPropertyList[propertyIdx];
      aPacket.putUInt16(prop);
      switch (prop) {

        case MTP_PROPERTY_STORAGE_ID:
          aPacket.putUInt16(MTP_TYPE_UINT32);
          aPacket.putUInt32(entry->mStorageID);
          break;

        case MTP_PROPERTY_PARENT_OBJECT:
          aPacket.putUInt16(MTP_TYPE_UINT32);
          aPacket.putUInt32(entry->mParent);
          break;

        case MTP_PROPERTY_PERSISTENT_UID:
          aPacket.putUInt16(MTP_TYPE_UINT128);
          // the same as aPacket.putUInt128
          aPacket.putUInt64(entry->mHandle);
          aPacket.putUInt64(entry->mStorageID);
          break;

        case MTP_PROPERTY_OBJECT_FORMAT:
          aPacket.putUInt16(MTP_TYPE_UINT16);
          aPacket.putUInt16(entry->mObjectFormat);
          break;

        case MTP_PROPERTY_OBJECT_SIZE:
          aPacket.putUInt16(MTP_TYPE_UINT64);
          aPacket.putUInt64(entry->mObjectSize);
          break;

        case MTP_PROPERTY_OBJECT_FILE_NAME:
        case MTP_PROPERTY_NAME:
          aPacket.putUInt16(MTP_TYPE_STR);
          aPacket.putString(entry->mObjectName.get());
          break;

        case MTP_PROPERTY_PROTECTION_STATUS:
          aPacket.putUInt16(MTP_TYPE_UINT16);
          aPacket.putUInt16(0); // 0 = No Protection
          break;

        case MTP_PROPERTY_DATE_CREATED: {
          aPacket.putUInt16(MTP_TYPE_STR);
          aPacket.putString(FormatDate(entry->mDateCreated, dateStr, sizeof(dateStr)));
          MTP_LOG("mDateCreated: (%ld) %s", entry->mDateCreated, dateStr);
          break;
        }

        case MTP_PROPERTY_DATE_MODIFIED: {
          aPacket.putUInt16(MTP_TYPE_STR);
          aPacket.putString(FormatDate(entry->mDateModified, dateStr, sizeof(dateStr)));
          MTP_LOG("mDateModified: (%ld) %s", entry->mDateModified, dateStr);
          break;
        }

        case MTP_PROPERTY_DATE_ADDED: {
          aPacket.putUInt16(MTP_TYPE_STR);
          aPacket.putString(FormatDate(entry->mDateAdded, dateStr, sizeof(dateStr)));
          MTP_LOG("mDateAdded: (%ld) %s", entry->mDateAdded, dateStr);
          break;
        }

        default:
          MTP_ERR("Unrecognized property code: %u", prop);
          return MTP_RESPONSE_GENERAL_ERROR;
      }
    }
  }
  return MTP_RESPONSE_OK;
}

//virtual
MtpResponseCode
MozMtpDatabase::getObjectInfo(MtpObjectHandle aHandle,
                              MtpObjectInfo& aInfo)
{
  RefPtr<DbEntry> entry = GetEntry(aHandle);
  if (!entry) {
    MTP_ERR("Handle 0x%08x is invalid", aHandle);
    return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
  }

  MTP_LOG("Handle: 0x%08x Display:'%s' Object:'%s'", aHandle, entry->mDisplayName.get(), entry->mObjectName.get());

  aInfo.mHandle = aHandle;
  aInfo.mStorageID = entry->mStorageID;
  aInfo.mFormat = entry->mObjectFormat;
  aInfo.mProtectionStatus = 0x0;

  if (entry->mObjectSize > 0xFFFFFFFFuLL) {
    aInfo.mCompressedSize = 0xFFFFFFFFuLL;
  } else {
    aInfo.mCompressedSize = entry->mObjectSize;
  }

  aInfo.mThumbFormat = MTP_FORMAT_UNDEFINED;
  aInfo.mThumbCompressedSize = 0;
  aInfo.mThumbPixWidth = 0;
  aInfo.mThumbPixHeight  = 0;
  aInfo.mImagePixWidth = 0;
  aInfo.mImagePixHeight = 0;
  aInfo.mImagePixDepth = 0;
  aInfo.mParent = entry->mParent;
  aInfo.mAssociationType = 0;
  aInfo.mAssociationDesc = 0;
  aInfo.mSequenceNumber = 0;
  aInfo.mName = ::strdup(entry->mObjectName.get());
  aInfo.mDateCreated = entry->mDateCreated;
  aInfo.mDateModified = entry->mDateModified;

  MTP_LOG("aInfo.mDateCreated = %ld entry->mDateCreated = %ld",
          aInfo.mDateCreated, entry->mDateCreated);
  MTP_LOG("aInfo.mDateModified = %ld entry->mDateModified = %ld",
          aInfo.mDateModified, entry->mDateModified);

  aInfo.mKeywords = ::strdup("fxos,touch");

  return MTP_RESPONSE_OK;
}

//virtual
void*
MozMtpDatabase::getThumbnail(MtpObjectHandle aHandle, size_t& aOutThumbSize)
{
  MTP_LOG("Handle: 0x%08x (returning nullptr)", aHandle);

  aOutThumbSize = 0;

  return nullptr;
}

//virtual
MtpResponseCode
MozMtpDatabase::getObjectFilePath(MtpObjectHandle aHandle,
                                  MtpString& aOutFilePath,
                                  int64_t& aOutFileLength,
                                  MtpObjectFormat& aOutFormat)
{
  RefPtr<DbEntry> entry = GetEntry(aHandle);
  if (!entry) {
    MTP_ERR("Handle 0x%08x is invalid", aHandle);
    return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
  }

  MTP_LOG("Handle: 0x%08x FilePath: '%s'", aHandle, entry->mPath.get());

  aOutFilePath = entry->mPath.get();
  aOutFileLength = entry->mObjectSize;
  aOutFormat = entry->mObjectFormat;

  return MTP_RESPONSE_OK;
}

//virtual
MtpResponseCode
MozMtpDatabase::deleteFile(MtpObjectHandle aHandle)
{
  RefPtr<DbEntry> entry = GetEntry(aHandle);
  if (!entry) {
    MTP_ERR("Invalid Handle: 0x%08x", aHandle);
    return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
  }

  MTP_LOG("Handle: 0x%08x '%s'", aHandle, entry->mPath.get());

  // File deletion will happen in lower level implementation.
  // The only thing we need to do is removing the entry from the db.
  RemoveEntry(aHandle);

  // Tell Device Storage that the file is gone.
  MtpWatcherNotify(entry, "deleted");

  return MTP_RESPONSE_OK;
}

#if 0
//virtual
MtpResponseCode
MozMtpDatabase::moveFile(MtpObjectHandle aHandle, MtpObjectHandle aNewParent)
{
  MTP_LOG("Handle: 0x%08x NewParent: 0x%08x", aHandle, aNewParent);

  // change parent

  return MTP_RESPONSE_OK
}

//virtual
MtpResponseCode
MozMtpDatabase::copyFile(MtpObjectHandle aHandle, MtpObjectHandle aNewParent)
{
  MTP_LOG("Handle: 0x%08x NewParent: 0x%08x", aHandle, aNewParent);

  // duplicate DbEntry
  // change parent

  return MTP_RESPONSE_OK
}
#endif

//virtual
MtpObjectHandleList*
MozMtpDatabase::getObjectReferences(MtpObjectHandle aHandle)
{
  MTP_LOG("Handle: 0x%08x (returning nullptr)", aHandle);
  return nullptr;
}

//virtual
MtpResponseCode
MozMtpDatabase::setObjectReferences(MtpObjectHandle aHandle,
                                    MtpObjectHandleList* aReferences)
{
  MTP_LOG("Handle: 0x%08x (NOT SUPPORTED)", aHandle);
  return MTP_RESPONSE_OPERATION_NOT_SUPPORTED;
}

//virtual
MtpProperty*
MozMtpDatabase::getObjectPropertyDesc(MtpObjectProperty aProperty,
                                      MtpObjectFormat aFormat)
{
  MTP_LOG("Property: %s 0x%08x", ObjectPropertyAsStr(aProperty), aProperty);

  MtpProperty* result = nullptr;
  switch (aProperty)
  {
    case MTP_PROPERTY_PROTECTION_STATUS:
      result = new MtpProperty(aProperty, MTP_TYPE_UINT16);
      break;
    case MTP_PROPERTY_OBJECT_FORMAT:
      result = new MtpProperty(aProperty, MTP_TYPE_UINT16, false, aFormat);
      break;
    case MTP_PROPERTY_STORAGE_ID:
    case MTP_PROPERTY_PARENT_OBJECT:
    case MTP_PROPERTY_WIDTH:
    case MTP_PROPERTY_HEIGHT:
    case MTP_PROPERTY_IMAGE_BIT_DEPTH:
      result = new MtpProperty(aProperty, MTP_TYPE_UINT32);
      break;
    case MTP_PROPERTY_OBJECT_SIZE:
      result = new MtpProperty(aProperty, MTP_TYPE_UINT64);
      break;
    case MTP_PROPERTY_DISPLAY_NAME:
    case MTP_PROPERTY_NAME:
      result = new MtpProperty(aProperty, MTP_TYPE_STR);
      break;
    case MTP_PROPERTY_OBJECT_FILE_NAME:
      result = new MtpProperty(aProperty, MTP_TYPE_STR, true);
      break;
    case MTP_PROPERTY_DATE_CREATED:
    case MTP_PROPERTY_DATE_MODIFIED:
    case MTP_PROPERTY_DATE_ADDED:
      result = new MtpProperty(aProperty, MTP_TYPE_STR);
      result->setFormDateTime();
      break;
    case MTP_PROPERTY_PERSISTENT_UID:
      result = new MtpProperty(aProperty, MTP_TYPE_UINT128);
      break;
    default:
      break;
  }

  return result;
}

//virtual
MtpProperty*
MozMtpDatabase::getDevicePropertyDesc(MtpDeviceProperty aProperty)
{
  MTP_LOG("(returning MTP_DEVICE_PROPERTY_UNDEFINED)");
  return new MtpProperty(MTP_DEVICE_PROPERTY_UNDEFINED, MTP_TYPE_UNDEFINED);
}

//virtual
void
MozMtpDatabase::sessionStarted()
{
  MTP_LOG("");
}

//virtual
void
MozMtpDatabase::sessionEnded()
{
  MTP_LOG("");
}

END_MTP_NAMESPACE
