dom/system/gonk/AutoMounter.cpp
author Alphan Chen <alchen@mozilla.com>
Mon, 22 Sep 2014 11:42:33 +0800
changeset 229665 e99d8abbc53065b5a931f11330f6ab27bca9f969
parent 227138 196665310e4a78337022531441bab250ba7beab7
child 236076 869eba5ab4a0944071e2b0149240fe6c919e8c0e
permissions -rw-r--r--
Bug 1060196 - Mark the volume as if we've started unmounting. r=dhylands

/* 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 <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>

#include <arpa/inet.h>
#include <linux/types.h>
#include <linux/netlink.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <android/log.h>
#include <cutils/properties.h>

#include "AutoMounter.h"
#include "nsVolumeService.h"
#include "AutoMounterSetting.h"
#include "base/message_loop.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/FileUtils.h"
#include "mozilla/Hal.h"
#include "mozilla/StaticPtr.h"
#include "MozMtpServer.h"
#include "MozMtpStorage.h"
#include "nsAutoPtr.h"
#include "nsCharSeparatedTokenizer.h"
#include "nsMemory.h"
#include "nsString.h"
#include "nsThreadUtils.h"
#include "nsXULAppAPI.h"
#include "OpenFileFinder.h"
#include "Volume.h"
#include "VolumeManager.h"

using namespace mozilla::hal;
USING_MTP_NAMESPACE

/**************************************************************************
*
* The following "switch" files are available for monitoring usb
* connections:
*
*   /sys/devices/virtual/switch/usb_connected/state
*   /sys/devices/virtual/switch/usb_configuration/state
*
*   Under gingerbread, only the usb_configuration seems to be available.
*   Starting with honeycomb, usb_connected was also added.
*
*   When a cable insertion/removal occurs, then a uevent similar to the
*   following will be generted:
*
*    change@/devices/virtual/switch/usb_configuration
*      ACTION=change
*      DEVPATH=/devices/virtual/switch/usb_configuration
*      SUBSYSTEM=switch
*      SWITCH_NAME=usb_configuration
*      SWITCH_STATE=0
*      SEQNUM=5038
*
*    SWITCH_STATE will be 0 after a removal and 1 after an insertion
*
**************************************************************************/

#define USB_CONFIGURATION_SWITCH_NAME   NS_LITERAL_STRING("usb_configuration")

#define GB_SYS_UMS_ENABLE     "/sys/devices/virtual/usb_composite/usb_mass_storage/enable"
#define GB_SYS_USB_CONFIGURED "/sys/devices/virtual/switch/usb_configuration/state"

#define ICS_SYS_USB_FUNCTIONS "/sys/devices/virtual/android_usb/android0/functions"
#define ICS_SYS_UMS_DIRECTORY "/sys/devices/virtual/android_usb/android0/f_mass_storage"
#define ICS_SYS_MTP_DIRECTORY "/sys/devices/virtual/android_usb/android0/f_mtp"
#define ICS_SYS_USB_STATE     "/sys/devices/virtual/android_usb/android0/state"

#define USE_DEBUG 0

#undef LOG
#define LOG(args...)  __android_log_print(ANDROID_LOG_INFO,  "AutoMounter", ## args)
#define LOGW(args...) __android_log_print(ANDROID_LOG_WARN,  "AutoMounter", ## args)
#define ERR(args...)  __android_log_print(ANDROID_LOG_ERROR, "AutoMounter", ## args)

#if USE_DEBUG
#define DBG(args...)  __android_log_print(ANDROID_LOG_DEBUG, "AutoMounter" , ## args)
#else
#define DBG(args...)
#endif

namespace mozilla {
namespace system {

#define SYS_USB_CONFIG          "sys.usb.config"
#define PERSIST_SYS_USB_CONFIG  "persist.sys.usb.config"

#define USB_FUNC_ADB    "adb"
#define USB_FUNC_MTP    "mtp"
#define USB_FUNC_RNDIS  "rndis"
#define USB_FUNC_UMS    "mass_storage"

class AutoMounter;

static void SetAutoMounterStatus(int32_t aStatus);

/***************************************************************************/

inline const char* SwitchStateStr(const SwitchEvent& aEvent)
{
  return aEvent.status() == SWITCH_STATE_ON ? "plugged" : "unplugged";
}

/***************************************************************************/

static bool
IsUsbCablePluggedIn()
{
#if 0
  // Use this code when bug 745078 gets fixed (or use whatever the
  // appropriate method is)
  return GetCurrentSwitchEvent(SWITCH_USB) == SWITCH_STATE_ON;
#else
  // Until then, just go read the file directly
  if (access(ICS_SYS_USB_STATE, F_OK) == 0) {
    char usbState[20];
    if (ReadSysFile(ICS_SYS_USB_STATE, usbState, sizeof(usbState))) {
      DBG("IsUsbCablePluggedIn: state = '%s'", usbState);
      return strcmp(usbState, "CONFIGURED") == 0 ||
             strcmp(usbState, "CONNECTED") == 0;
    }
    ERR("Error reading file '%s': %s", ICS_SYS_USB_STATE, strerror(errno));
    return false;
  }
  bool configured;
  if (ReadSysFile(GB_SYS_USB_CONFIGURED, &configured)) {
    return configured;
  }
  ERR("Error reading file '%s': %s", GB_SYS_USB_CONFIGURED, strerror(errno));
  return false;
#endif
}

static bool
IsUsbConfigured()
{
  if (access(ICS_SYS_USB_STATE, F_OK) == 0) {
    char usbState[20];
    if (ReadSysFile(ICS_SYS_USB_STATE, usbState, sizeof(usbState))) {
      DBG("IsUsbConfigured: state = '%s'", usbState);
      return strcmp(usbState, "CONFIGURED") == 0;
    }
    ERR("Error reading file '%s': %s", ICS_SYS_USB_STATE, strerror(errno));
    return false;
  }
  bool configured;
  if (ReadSysFile(GB_SYS_USB_CONFIGURED, &configured)) {
    return configured;
  }
  ERR("Error reading file '%s': %s", GB_SYS_USB_CONFIGURED, strerror(errno));
  return false;
}

/***************************************************************************/

// The AutoVolumeManagerStateObserver allows the AutoMounter to know when
// the volume manager changes state (i.e. it has finished initialization)
class AutoVolumeManagerStateObserver : public VolumeManager::StateObserver
{
public:
  virtual void Notify(const VolumeManager::StateChangedEvent& aEvent);
};

// The AutoVolumeEventObserver allows the AutoMounter to know about card
// insertion and removal, as well as state changes in the volume.
class AutoVolumeEventObserver : public Volume::EventObserver
{
public:
  virtual void Notify(Volume* const& aEvent);
};

class AutoMounterResponseCallback : public VolumeResponseCallback
{
public:
  AutoMounterResponseCallback()
    : mErrorCount(0)
  {
  }

protected:
  virtual void ResponseReceived(const VolumeCommand* aCommand);

private:
    const static int kMaxErrorCount = 3; // Max number of errors before we give up

    int   mErrorCount;
};

/***************************************************************************/

class AutoMounter
{
public:
  NS_INLINE_DECL_REFCOUNTING(AutoMounter)

  typedef nsTArray<RefPtr<Volume>> VolumeArray;

  AutoMounter()
    : mState(STATE_IDLE),
      mResponseCallback(new AutoMounterResponseCallback),
      mMode(AUTOMOUNTER_DISABLE)
  {
    VolumeManager::RegisterStateObserver(&mVolumeManagerStateObserver);
    Volume::RegisterObserver(&mVolumeEventObserver);

    // It's possible that the VolumeManager is already in the READY state,
    // so we call CheckVolumeSettings here to cover that case. Otherwise,
    // we'll pick it up when the VolumeManage state changes to VOLUMES_READY.
    CheckVolumeSettings();

    DBG("Calling UpdateState from constructor");
    UpdateState();
  }

  ~AutoMounter()
  {
    VolumeManager::VolumeArray::size_type numVolumes = VolumeManager::NumVolumes();
    VolumeManager::VolumeArray::index_type volIndex;
    for (volIndex = 0; volIndex < numVolumes; volIndex++) {
      RefPtr<Volume> vol = VolumeManager::GetVolume(volIndex);
      if (vol) {
        vol->UnregisterObserver(&mVolumeEventObserver);
      }
    }
    Volume::UnregisterObserver(&mVolumeEventObserver);
    VolumeManager::UnregisterStateObserver(&mVolumeManagerStateObserver);
  }

  void CheckVolumeSettings()
  {
    if (VolumeManager::State() != VolumeManager::VOLUMES_READY) {
      DBG("CheckVolumeSettings: VolumeManager is NOT READY yet");
      return;
    }
    DBG("CheckVolumeSettings: VolumeManager is READY");

    // The VolumeManager knows about all of the volumes from vold. We now
    // know the names of all of the volumes, so we can find out what the
    // initial sharing settings are set to.

    VolumeManager::VolumeArray::size_type numVolumes = VolumeManager::NumVolumes();
    VolumeManager::VolumeArray::index_type i;
    for (i = 0; i < numVolumes; i++) {
      RefPtr<Volume> vol = VolumeManager::GetVolume(i);
      if (vol) {
        vol->RegisterObserver(&mVolumeEventObserver);
        // We need to pick up the intial value of the
        // ums.volume.NAME.enabled setting.
        AutoMounterSetting::CheckVolumeSettings(vol->Name());

        // Note: eventually CheckVolumeSettings will call
        //       AutoMounter::SetSharingMode, which will in turn call
        //       UpdateState if needed.
      }
    }
  }

  void UpdateState();

  void ConfigureUsbFunction(const char* aUsbFunc);

  void StartMtpServer();
  void StopMtpServer();

  void StartUmsSharing();
  void StopUmsSharing();


  const char* ModeStr(int32_t aMode)
  {
    switch (aMode) {
      case AUTOMOUNTER_DISABLE:                 return "Disable";
      case AUTOMOUNTER_ENABLE_UMS:              return "Enable-UMS";
      case AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED:  return "DisableWhenUnplugged";
      case AUTOMOUNTER_ENABLE_MTP:              return "Enable-MTP";
    }
    return "??? Unknown ???";
  }

  bool IsModeEnabled(int32_t aMode)
  {
    return aMode == AUTOMOUNTER_ENABLE_MTP ||
           aMode == AUTOMOUNTER_ENABLE_UMS;
  }

  void SetMode(int32_t aMode)
  {
    if ((aMode == AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED) &&
        (mMode == AUTOMOUNTER_DISABLE)) {
      // If it's already disabled, then leave it as disabled.
      // AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED implies "enabled until unplugged"
      aMode = AUTOMOUNTER_DISABLE;
    }

    if (aMode == AUTOMOUNTER_DISABLE &&
        mMode == AUTOMOUNTER_ENABLE_UMS && IsUsbCablePluggedIn()) {
      // On many devices (esp non-Samsung), we can't force the disable, so we
      // need to defer until the USB cable is actually unplugged.
      // See bug 777043.
      //
      // Otherwise our attempt to disable it will fail, and we'll wind up in a bad
      // state where the AutoMounter thinks that Sharing has been turned off, but
      // the files are actually still being Shared because the attempt to unshare
      // failed.
      LOG("Attempting to disable UMS. Deferring until USB cable is unplugged.");
      aMode = AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED;
    }

    if (aMode != mMode) {
      LOG("Changing mode from '%s' to '%s'", ModeStr(mMode), ModeStr(aMode));
      mMode = aMode;
      DBG("Calling UpdateState due to mode set to %d", mMode);
      UpdateState();
    }
  }

  void SetSharingMode(const nsACString& aVolumeName, bool aAllowSharing)
  {
    RefPtr<Volume> vol = VolumeManager::FindVolumeByName(aVolumeName);
    if (!vol) {
      return;
    }
    if (vol->IsSharingEnabled() == aAllowSharing) {
      return;
    }
    vol->SetUnmountRequested(false);
    vol->SetMountRequested(false);
    vol->SetSharingEnabled(aAllowSharing);
    DBG("Calling UpdateState due to volume %s sharing set to %d",
        vol->NameStr(), (int)aAllowSharing);
    UpdateState();
  }

  void FormatVolume(const nsACString& aVolumeName)
  {
    RefPtr<Volume> vol = VolumeManager::FindVolumeByName(aVolumeName);
    if (!vol) {
      return;
    }
    if (vol->IsFormatRequested()) {
      return;
    }
    vol->SetUnmountRequested(false);
    vol->SetMountRequested(false);
    vol->SetFormatRequested(true);
    DBG("Calling UpdateState due to volume %s formatting set to %d",
        vol->NameStr(), (int)vol->IsFormatRequested());
    UpdateState();
  }

  void MountVolume(const nsACString& aVolumeName)
  {
    RefPtr<Volume> vol = VolumeManager::FindVolumeByName(aVolumeName);
    if (!vol) {
      return;
    }
    vol->SetUnmountRequested(false);
    if (vol->IsMountRequested() || vol->mState == nsIVolume::STATE_MOUNTED) {
      return;
    }
    vol->SetMountRequested(true);
    DBG("Calling UpdateState due to volume %s mounting set to %d",
        vol->NameStr(), (int)vol->IsMountRequested());
    UpdateState();
  }

  void UnmountVolume(const nsACString& aVolumeName)
  {
    RefPtr<Volume> vol = VolumeManager::FindVolumeByName(aVolumeName);
    if (!vol) {
      return;
    }
    if (vol->IsUnmountRequested()) {
      return;
    }
    vol->SetMountRequested(false);
    vol->SetUnmountRequested(true);
    DBG("Calling UpdateState due to volume %s unmounting set to %d",
        vol->NameStr(), (int)vol->IsUnmountRequested());
    UpdateState();
  }

private:

  enum STATE
  {
    // IDLE - Nothing is being shared
    STATE_IDLE,

    // We've detected that conditions are right to enable mtp. So we've
    // set sys.usb.config to include mtp, and we're waiting for the USB
    // subsystem to be "configured". Once mtp shows up in
    //  then we know
    // that its been configured and we can open /dev/mtp_usb
    STATE_MTP_CONFIGURING,

    // mtp has been configured (i.e. mtp now shows up in
    // /sys/devices/virtual/android_usb/android0/functions so we can start
    // the mtp server.
    STATE_MTP_STARTED,

    // The mtp server has reported sessionStarted. We'll leave this state
    // when we receive sessionEnded.
    STATE_MTP_CONNECTED,

    // We've added mass_storage (aka UMS) to sys.usb.config and we're waiting for
    // mass_storage to appear in /sys/devices/virtual/android_usb/android0/functions
    STATE_UMS_CONFIGURING,

    // mass_storage has been configured and we can start sharing once the user
    // enables it.
    STATE_UMS_CONFIGURED,

    // USB Tethering is enabled
    STATE_RNDIS_CONFIGURED,
  };

  const char *StateStr(STATE aState)
  {
    switch (aState) {
      case STATE_IDLE:              return "IDLE";
      case STATE_MTP_CONFIGURING:   return "MTP_CONFIGURING";
      case STATE_MTP_CONNECTED:     return "MTP_CONNECTED";
      case STATE_MTP_STARTED:       return "MTP_STARTED";
      case STATE_UMS_CONFIGURING:   return "UMS_CONFIGURING";
      case STATE_UMS_CONFIGURED:    return "UMS_CONFIGURED";
      case STATE_RNDIS_CONFIGURED:  return "RNDIS_CONFIGURED";
    }
    return "STATE_???";
  }

  void SetState(STATE aState)
  {
    const char *oldStateStr = StateStr(mState);
    mState = aState;
    const char *newStateStr = StateStr(mState);
    LOG("AutoMounter state changed from %s to %s", oldStateStr, newStateStr);
  }

  STATE                           mState;

  AutoVolumeEventObserver         mVolumeEventObserver;
  AutoVolumeManagerStateObserver  mVolumeManagerStateObserver;
  RefPtr<VolumeResponseCallback>  mResponseCallback;
  int32_t                         mMode;
  MozMtpStorage::Array            mMozMtpStorage;
};

static StaticRefPtr<AutoMounter> sAutoMounter;
static StaticRefPtr<MozMtpServer> sMozMtpServer;

/***************************************************************************/

void
AutoVolumeManagerStateObserver::Notify(const VolumeManager::StateChangedEvent &)
{
  LOG("VolumeManager state changed event: %s", VolumeManager::StateStr());

  if (!sAutoMounter) {
    return;
  }

  // In the event that the VolumeManager just entered the VOLUMES_READY state,
  // we call CheckVolumeSettings here (it's possible that this method never
  // gets called if the VolumeManager was already in the VOLUMES_READY state
  // by the time the AutoMounter was constructed).
  sAutoMounter->CheckVolumeSettings();

  DBG("Calling UpdateState due to VolumeManagerStateObserver");
  sAutoMounter->UpdateState();
}

void
AutoVolumeEventObserver::Notify(Volume * const &)
{
  if (!sAutoMounter) {
    return;
  }
  DBG("Calling UpdateState due to VolumeEventStateObserver");
  sAutoMounter->UpdateState();
}

void
AutoMounterResponseCallback::ResponseReceived(const VolumeCommand* aCommand)
{

  if (WasSuccessful()) {
    DBG("Calling UpdateState due to Volume::OnSuccess");
    mErrorCount = 0;
    sAutoMounter->UpdateState();
    return;
  }
  ERR("Command '%s' failed: %d '%s'",
      aCommand->CmdStr(), ResponseCode(), ResponseStr().get());

  if (++mErrorCount < kMaxErrorCount) {
    DBG("Calling UpdateState due to VolumeResponseCallback::OnError");
    sAutoMounter->UpdateState();
  }
}

static bool
IsUsbFunctionEnabled(const char* aConfig, const char* aUsbFunc)
{
  nsAutoCString config(aConfig);
  nsCCharSeparatedTokenizer tokenizer(config, ',');

  while (tokenizer.hasMoreTokens()) {
    nsAutoCString token(tokenizer.nextToken());
    if (token.Equals(aUsbFunc)) {
      DBG("IsUsbFunctionEnabled('%s', '%s'): returning true", aConfig, aUsbFunc);
      return true;
    }
  }
  DBG("IsUsbFunctionEnabled('%s', '%s'): returning false", aConfig, aUsbFunc);
  return false;
}

static void
SetUsbFunction(const char* aUsbFunc)
{
  char oldSysUsbConfig[PROPERTY_VALUE_MAX];
  property_get(SYS_USB_CONFIG, oldSysUsbConfig, "");

  if (IsUsbFunctionEnabled(oldSysUsbConfig, aUsbFunc)) {
    // The function is already configured. Nothing else to do.
    DBG("SetUsbFunction('%s') - already set - nothing to do", aUsbFunc);
    return;
  }

  char newSysUsbConfig[PROPERTY_VALUE_MAX];

  if (strcmp(aUsbFunc, USB_FUNC_MTP) == 0) {
    // We're enabling MTP. For this we'll wind up using mtp, or mtp,adb
    strlcpy(newSysUsbConfig, USB_FUNC_MTP, sizeof(newSysUsbConfig));
  } else if (strcmp(aUsbFunc, USB_FUNC_UMS) == 0) {
    // We're enabling UMS. For this we make the assumption that the persisted
    // property has mass_storage enabled.
    property_get(PERSIST_SYS_USB_CONFIG, newSysUsbConfig, "");
  } else {
    printf_stderr("AutoMounter::SetUsbFunction Unrecognized aUsbFunc '%s'\n", aUsbFunc);
    MOZ_ASSERT(0);
    return;
  }

  // Make sure the new value that we write into sys.usb.config keeps the adb
  // (or non-adb) of the current string.

  if (IsUsbFunctionEnabled(oldSysUsbConfig, USB_FUNC_ADB)) {
    // ADB was turned on - keep it on.
    if (!IsUsbFunctionEnabled(newSysUsbConfig, USB_FUNC_ADB)) {
      // Add adb to the new string
      strlcat(newSysUsbConfig, ",", sizeof(newSysUsbConfig));
      strlcat(newSysUsbConfig, USB_FUNC_ADB, sizeof(newSysUsbConfig));
    }
  } else {
    // ADB was turned off - keep it off
    if (IsUsbFunctionEnabled(newSysUsbConfig, USB_FUNC_ADB)) {
      // Remove ADB from the new string.
      if (strcmp(newSysUsbConfig, USB_FUNC_ADB) == 0) {
        newSysUsbConfig[0] = '\0';
      } else {
        nsAutoCString withoutAdb(newSysUsbConfig);
        withoutAdb.ReplaceSubstring( "," USB_FUNC_ADB, "");
        strlcpy(newSysUsbConfig, withoutAdb.get(), sizeof(newSysUsbConfig));
      }
    }
  }

  LOG("SetUsbFunction(%s) %s to '%s'", aUsbFunc, SYS_USB_CONFIG, newSysUsbConfig);
  property_set(SYS_USB_CONFIG, newSysUsbConfig);
}

void
AutoMounter::StartMtpServer()
{
  if (sMozMtpServer) {
    // Mtp Server is already running - nothing to do
    return;
  }
  LOG("Starting MtpServer");
  sMozMtpServer = new MozMtpServer();
  sMozMtpServer->Run();

  VolumeArray::index_type volIndex;
  VolumeArray::size_type  numVolumes = VolumeManager::NumVolumes();
  for (volIndex = 0; volIndex < numVolumes; volIndex++) {
    RefPtr<Volume> vol = VolumeManager::GetVolume(volIndex);
    nsRefPtr<MozMtpStorage> storage = new MozMtpStorage(vol, sMozMtpServer);
    mMozMtpStorage.AppendElement(storage);
  }
}

void
AutoMounter::StopMtpServer()
{
  LOG("Stopping MtpServer");

  mMozMtpStorage.Clear();
  sMozMtpServer = nullptr;
}

/***************************************************************************/

void
AutoMounter::UpdateState()
{
  static bool inUpdateState = false;
  if (inUpdateState) {
    // When UpdateState calls SetISharing, this causes a volume state
    // change to occur, which would normally cause UpdateState to be called
    // again. We want the volume state change to go out (so that device
    // storage will see the change in sharing state), but since we're
    // already in UpdateState we just want to prevent recursion from screwing
    // things up.
    return;
  }
  AutoRestore<bool> inUpdateStateDetector(inUpdateState);
  inUpdateState = true;

  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  // If the following preconditions are met:
  //    - UMS is available (i.e. compiled into the kernel)
  //    - UMS is enabled
  //    - AutoMounter is enabled
  //    - USB cable is plugged in
  //  then we will try to unmount and share
  //  otherwise we will try to unshare and mount.

  if (VolumeManager::State() != VolumeManager::VOLUMES_READY) {
    // The volume manager isn't in a ready state, so there
    // isn't anything else that we can do.
    LOG("UpdateState: VolumeManager not ready yet");
    return;
  }

  if (mResponseCallback->IsPending()) {
    // We only deal with one outstanding volume command at a time,
    // so we need to wait for it to finish.
    return;
  }

  // Calling setprop sys.usb.config mtp,adb (or adding mass_storage) will
  // cause /sys/devices/virtual/android_usb/android0/state to go:
  // CONFIGURED -> DISCONNECTED -> CONNECTED -> CONFIGURED
  //
  // Since IsUsbCablePluggedIn returns state == CONFIGURED, it will look
  // like a cable pull and replugin.

  bool umsAvail = false;
  bool umsConfigured = false;
  bool umsEnabled = false;
  bool mtpAvail = false;
  bool mtpConfigured = false;
  bool mtpEnabled = false;
  bool rndisConfigured = false;
  bool usbCablePluggedIn = IsUsbCablePluggedIn();

  if (access(ICS_SYS_USB_FUNCTIONS, F_OK) == 0) {
    char functionsStr[60];
    if (!ReadSysFile(ICS_SYS_USB_FUNCTIONS, functionsStr, sizeof(functionsStr))) {
      ERR("Error reading file '%s': %s", ICS_SYS_USB_FUNCTIONS, strerror(errno));
      functionsStr[0] = '\0';
    }
    DBG("UpdateState: USB functions: '%s'", functionsStr);

    bool  usbConfigured = IsUsbConfigured();
    umsAvail = (access(ICS_SYS_UMS_DIRECTORY, F_OK) == 0);
    if (umsAvail) {
      umsConfigured = usbConfigured && strstr(functionsStr, USB_FUNC_UMS) != nullptr;
      umsEnabled = (mMode == AUTOMOUNTER_ENABLE_UMS) ||
                   ((mMode == AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED) && umsConfigured);
    } else {
      umsConfigured = false;
      umsEnabled = false;
    }

    mtpAvail = (access(ICS_SYS_MTP_DIRECTORY, F_OK) == 0);
    if (mtpAvail) {
      mtpConfigured = usbConfigured && strstr(functionsStr, USB_FUNC_MTP) != nullptr;
      mtpEnabled = (mMode == AUTOMOUNTER_ENABLE_MTP) ||
                   ((mMode == AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED) && mtpConfigured);
    } else {
      mtpConfigured = false;
      mtpEnabled = false;
    }

    rndisConfigured = strstr(functionsStr, USB_FUNC_RNDIS) != nullptr;
  }

  bool enabled = mtpEnabled || umsEnabled;

  if (mMode == AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED) {
    // DISABLE_WHEN_UNPLUGGED implies already enabled.
    enabled = usbCablePluggedIn;
    if (!usbCablePluggedIn) {
      mMode = AUTOMOUNTER_DISABLE;
      mtpEnabled = false;
      umsEnabled = false;
    }
  }

  DBG("UpdateState: ums:A%dC%dE%d mtp:A%dC%dE%d rndis:%d mode:%d usb:%d mState:%s",
      umsAvail, umsConfigured, umsEnabled,
      mtpAvail, mtpConfigured, mtpEnabled, rndisConfigured,
      mMode, usbCablePluggedIn, StateStr(mState));

  switch (mState) {

    case STATE_IDLE:
      if (!usbCablePluggedIn) {
        // Stay in the IDLE state. We'll get a CONNECTED or CONFIGURED
        // UEvent when the usb cable is plugged in.
        break;
      }
      if (rndisConfigured) {
        // USB Tethering uses RNDIS. We'll just wait until its turned off.
        SetState(STATE_RNDIS_CONFIGURED);
        break;
      }
      if (mtpEnabled) {
        if (mtpConfigured) {
          // The USB layer has already been configured. Now we can go ahead
          // and start the MTP server. This particular codepath will not
          // normally be taken, but it could happen if you stop and restart
          // b2g while sys.usb.config is set to enable mtp.
          StartMtpServer();
          SetState(STATE_MTP_STARTED);
        } else {
          // We need to configure USB to use mtp. Wait for it to be configured
          // before we start the MTP server.
          SetUsbFunction(USB_FUNC_MTP);
          SetState(STATE_MTP_CONFIGURING);
        }
      } else if (umsConfigured) {
        // UMS is already configured.
        SetState(STATE_UMS_CONFIGURED);
      } else if (umsAvail) {
        // We do this whether or not UMS is enabled. With UMS, it's the
        // sharing of the volume which is significant. What is important
        // is that we don't leave it in MTP mode when MTP isn't enabled.
        SetUsbFunction(USB_FUNC_UMS);
        SetState(STATE_UMS_CONFIGURING);
      }
      break;

    case STATE_MTP_CONFIGURING:
      // While configuring, the USB configuration state will change from
      // CONFIGURED -> CONNECTED -> DISCONNECTED -> CONNECTED -> CONFIGURED
      // so we don't check for cable unplugged here.
      if (mtpEnabled && mtpConfigured) {
        // The USB layer has been configured. Now we can go ahead and start
        // the MTP server.
        StartMtpServer();
        SetState(STATE_MTP_STARTED);
        break;
      }
      if (rndisConfigured) {
        SetState(STATE_RNDIS_CONFIGURED);
        break;
      }
      break;

    case STATE_MTP_STARTED:
      if (usbCablePluggedIn && mtpConfigured && mtpEnabled) {
        // Everything is still good. Leave the MTP server running
        break;
      }
      DBG("STATE_MTP_STARTED: About to StopMtpServer "
          "mtpConfigured = %d mtpEnabled = %d usbCablePluggedIn: %d",
          mtpConfigured, mtpEnabled, usbCablePluggedIn);
      StopMtpServer();
      if (rndisConfigured) {
        SetState(STATE_RNDIS_CONFIGURED);
        break;
      }
      if (umsAvail) {
        // Switch back to UMS
        SetUsbFunction(USB_FUNC_UMS);
        SetState(STATE_UMS_CONFIGURING);
        break;
      }
      SetState(STATE_IDLE);
      break;

    case STATE_UMS_CONFIGURING:
      // While configuring, the USB configuration state will change from
      // CONFIGURED -> CONNECTED -> DISCONNECTED -> CONNECTED -> CONFIGURED
      // so we don't check for cable unplugged here. However, having said
      // that, we'll often sit in this state while the cable is unplugged,
      // since we might not get any events until the cable gets plugged back
      // in. This is why we need to check for mtpEnabled once we get the
      // configured event.
      if (umsConfigured) {
        if (mtpEnabled) {
          // MTP was enabled. Start reconfiguring.
          SetState(STATE_MTP_CONFIGURING);
          SetUsbFunction(USB_FUNC_MTP);
          break;
        }
        SetState(STATE_UMS_CONFIGURED);
      }
      if (rndisConfigured) {
        SetState(STATE_RNDIS_CONFIGURED);
        break;
      }
      break;

    case STATE_UMS_CONFIGURED:
      if (usbCablePluggedIn) {
        if (mtpEnabled) {
          // MTP was enabled. Start reconfiguring.
          SetState(STATE_MTP_CONFIGURING);
          SetUsbFunction(USB_FUNC_MTP);
          break;
        }
        if (umsConfigured && umsEnabled) {
          // This is the normal state when UMS is enabled.
          break;
        }
      }
      if (rndisConfigured) {
        SetState(STATE_RNDIS_CONFIGURED);
        break;
      }
      SetState(STATE_IDLE);
      break;

    case STATE_RNDIS_CONFIGURED:
      if (usbCablePluggedIn && rndisConfigured) {
        // Normal state when RNDIS is enabled.
        break;
      }
      SetState(STATE_IDLE);
      break;

    default:
      SetState(STATE_IDLE);
      break;
  }

  bool tryToShare = umsEnabled && usbCablePluggedIn;
  LOG("UpdateState: ums:A%dC%dE%d mtp:A%dC%dE%d mode:%d usb:%d tryToShare:%d state:%s",
      umsAvail, umsConfigured, umsEnabled,
      mtpAvail, mtpConfigured, mtpEnabled,
      mMode, usbCablePluggedIn, tryToShare, StateStr(mState));

  bool filesOpen = false;
  static unsigned filesOpenDelayCount = 0;
  VolumeArray::index_type volIndex;
  VolumeArray::size_type  numVolumes = VolumeManager::NumVolumes();
  for (volIndex = 0; volIndex < numVolumes; volIndex++) {
    RefPtr<Volume>  vol = VolumeManager::GetVolume(volIndex);
    Volume::STATE   volState = vol->State();

    if (vol->State() == nsIVolume::STATE_MOUNTED) {
      LOG("UpdateState: Volume %s is %s and %s @ %s gen %d locked %d sharing %s",
          vol->NameStr(), vol->StateStr(),
          vol->MediaPresent() ? "inserted" : "missing",
          vol->MountPoint().get(), vol->MountGeneration(),
          (int)vol->IsMountLocked(),
          vol->CanBeShared() ? (vol->IsSharingEnabled() ?
                                 (vol->IsSharing() ? "en-y" : "en-n") : "dis") : "x");
      if (vol->IsSharing() && !usbCablePluggedIn) {
        // We call SetIsSharing(true) below to indicate intent to share. This
        // causes a state change which notifys apps, and they'll close any
        // open files, which will initiate the change away from the mounted
        // state and into the sharing state. Normally, when the volume
        // transitions back to the mounted state, then vol->mIsSharing gets set
        // to false. However, if the user pulls the USB cable before we
        // actually start sharing, then the volume never actually leaves
        // the mounted state (and hence never transitions from
        // sharing -> mounted), and mIsSharing never gets set back to false.
        // So we clear mIsSharing here.
        vol->SetIsSharing(false);
      }
    } else {
      LOG("UpdateState: Volume %s is %s and %s", vol->NameStr(), vol->StateStr(),
          vol->MediaPresent() ? "inserted" : "missing");
    }
    if (!vol->MediaPresent()) {
      // No media - nothing we can do
      continue;
    }

    if ((tryToShare && vol->IsSharingEnabled()) ||
        vol->IsFormatRequested() ||
        vol->IsUnmountRequested()) {
      switch (volState) {
        // We're going to try to unmount the volume
        case nsIVolume::STATE_MOUNTED: {
          if (vol->IsMountLocked()) {
            // The volume is currently locked, so leave it in the mounted
            // state.
            LOGW("UpdateState: Mounted volume %s is locked, not sharing or formatting",
                 vol->NameStr());
            break;
          }

          // Mark the volume as if we've started sharing/formatting/unmmounting.
          // This will cause apps which watch device storage notifications to see
          // the volume go into the different state, and prompt them to close any
          // open files that they might have.
          if (tryToShare && vol->IsSharingEnabled()) {
            vol->SetIsSharing(true);
          } else if (vol->IsFormatRequested()){
            vol->SetIsFormatting(true);
          } else if (vol->IsUnmountRequested()){
            vol->SetIsUnmounting(true);
          }

          // Check to see if there are any open files on the volume and
          // don't initiate the unmount while there are open files.
          OpenFileFinder::Info fileInfo;
          OpenFileFinder fileFinder(vol->MountPoint());
          if (fileFinder.First(&fileInfo)) {
            LOGW("The following files are open under '%s'",
                 vol->MountPoint().get());
            do {
              LOGW("  PID: %d file: '%s' app: '%s' comm: '%s' exe: '%s'\n",
                   fileInfo.mPid,
                   fileInfo.mFileName.get(),
                   fileInfo.mAppName.get(),
                   fileInfo.mComm.get(),
                   fileInfo.mExe.get());
            } while (fileFinder.Next(&fileInfo));
            LOGW("UpdateState: Mounted volume %s has open files, not sharing or formatting",
                 vol->NameStr());

            // Check again in a few seconds to see if the files are closed.
            // Since we're trying to share the volume, this implies that we're
            // plugged into the PC via USB and this in turn implies that the
            // battery is charging, so we don't need to be too concerned about
            // wasting battery here.
            //
            // If we just detected that there were files open, then we use
            // a short timer. We will have told the apps that we're trying
            // trying to share, and they'll be closing their files. This makes
            // the sharing more responsive. If after a few seconds, the apps
            // haven't closed their files, then we back off.

            int delay = 1000;
            if (filesOpenDelayCount > 10) {
              delay = 5000;
            }
            MessageLoopForIO::current()->
              PostDelayedTask(FROM_HERE,
                              NewRunnableMethod(this, &AutoMounter::UpdateState),
                              delay);
            filesOpen = true;
            break;
          }

          // Volume is mounted, we need to unmount before
          // we can share.
          LOG("UpdateState: Unmounting %s", vol->NameStr());
          vol->StartUnmount(mResponseCallback);
          return; // UpdateState will be called again when the Unmount command completes
        }
        case nsIVolume::STATE_IDLE: {
          LOG("UpdateState: Volume %s is nsIVolume::STATE_IDLE", vol->NameStr());
          if (vol->IsFormatting() && !vol->IsFormatRequested()) {
            vol->SetFormatRequested(false);
            LOG("UpdateState: Mounting %s", vol->NameStr());
            vol->StartMount(mResponseCallback);
            break;
          }
          if (tryToShare && vol->IsSharingEnabled()) {
            // Volume is unmounted. We can go ahead and share.
            LOG("UpdateState: Sharing %s", vol->NameStr());
            vol->StartShare(mResponseCallback);
          } else if (vol->IsFormatRequested()){
            // Volume is unmounted. We can go ahead and format.
            LOG("UpdateState: Formatting %s", vol->NameStr());
            vol->StartFormat(mResponseCallback);
          }
          return; // UpdateState will be called again when the Share/Format command completes
        }
        default: {
          // Not in a state that we can do anything about.
          break;
        }
      }
    } else {
      // We're going to try and unshare and remount the volumes
      switch (volState) {
        case nsIVolume::STATE_SHARED: {
          // Volume is shared. We can go ahead and unshare.
          LOG("UpdateState: Unsharing %s", vol->NameStr());
          vol->StartUnshare(mResponseCallback);
          return; // UpdateState will be called again when the Unshare command completes
        }
        case nsIVolume::STATE_IDLE: {
          if (!vol->IsUnmountRequested()) {
            // Volume is unmounted and mount-requested, try to mount.

            LOG("UpdateState: Mounting %s", vol->NameStr());
            vol->StartMount(mResponseCallback);
          }
          return; // UpdateState will be called again when Mount command completes
        }
        default: {
          // Not in a state that we can do anything about.
          break;
        }
      }
    }
  }

  int32_t status = AUTOMOUNTER_STATUS_DISABLED;
  if (filesOpen) {
    filesOpenDelayCount++;
    status = AUTOMOUNTER_STATUS_FILES_OPEN;
  } else if (enabled) {
    filesOpenDelayCount = 0;
    status = AUTOMOUNTER_STATUS_ENABLED;
  }
  SetAutoMounterStatus(status);
}

/***************************************************************************/

static void
InitAutoMounterIOThread()
{
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
  MOZ_ASSERT(!sAutoMounter);

  sAutoMounter = new AutoMounter();
}

static void
ShutdownAutoMounterIOThread()
{
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  sAutoMounter = nullptr;
  ShutdownVolumeManager();
}

static void
SetAutoMounterModeIOThread(const int32_t& aMode)
{
  MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
  MOZ_ASSERT(sAutoMounter);

  sAutoMounter->SetMode(aMode);
}

static void
SetAutoMounterSharingModeIOThread(const nsCString& aVolumeName, const bool& aAllowSharing)
{
  MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
  MOZ_ASSERT(sAutoMounter);

  sAutoMounter->SetSharingMode(aVolumeName, aAllowSharing);
}

static void
AutoMounterFormatVolumeIOThread(const nsCString& aVolumeName)
{
  MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
  MOZ_ASSERT(sAutoMounter);

  sAutoMounter->FormatVolume(aVolumeName);
}

static void
AutoMounterMountVolumeIOThread(const nsCString& aVolumeName)
{
  MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
  MOZ_ASSERT(sAutoMounter);

  sAutoMounter->MountVolume(aVolumeName);
}

static void
AutoMounterUnmountVolumeIOThread(const nsCString& aVolumeName)
{
  MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
  MOZ_ASSERT(sAutoMounter);

  sAutoMounter->UnmountVolume(aVolumeName);
}

static void
UsbCableEventIOThread()
{
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  if (!sAutoMounter) {
    return;
  }
  DBG("Calling UpdateState due to USBCableEvent");
  sAutoMounter->UpdateState();
}

/**************************************************************************
*
*   Public API
*
*   Since the AutoMounter runs in IO Thread context, we need to switch
*   to IOThread context before we can do anything.
*
**************************************************************************/

class UsbCableObserver MOZ_FINAL : public SwitchObserver
{
  ~UsbCableObserver()
  {
    UnregisterSwitchObserver(SWITCH_USB, this);
  }

public:
  NS_INLINE_DECL_REFCOUNTING(UsbCableObserver)

  UsbCableObserver()
  {
    RegisterSwitchObserver(SWITCH_USB, this);
  }

  virtual void Notify(const SwitchEvent& aEvent)
  {
    DBG("UsbCable switch device: %d state: %s\n",
        aEvent.device(), SwitchStateStr(aEvent));
    XRE_GetIOMessageLoop()->PostTask(
        FROM_HERE,
        NewRunnableFunction(UsbCableEventIOThread));
  }
};

static StaticRefPtr<UsbCableObserver> sUsbCableObserver;
static StaticRefPtr<AutoMounterSetting> sAutoMounterSetting;

static void
InitVolumeConfig()
{
  // This function uses /system/etc/volume.cfg to add additional volumes
  // to the Volume Manager.
  //
  // This is useful on devices like the Nexus 4, which have no physical sd card
  // or dedicated partition.
  //
  // The format of the volume.cfg file is as follows:
  // create volume-name mount-point
  // Blank lines and lines starting with the hash character "#" will be ignored.

  nsCOMPtr<nsIVolumeService> vs = do_GetService(NS_VOLUMESERVICE_CONTRACTID);
  NS_ENSURE_TRUE_VOID(vs);

  ScopedCloseFile fp;
  int n = 0;
  char line[255];
  char *command, *vol_name_cstr, *mount_point_cstr, *save_ptr;
  const char *filename = "/system/etc/volume.cfg";
  if (!(fp = fopen(filename, "r"))) {
    LOG("Unable to open volume configuration file '%s' - ignoring", filename);
    return;
  }
  while(fgets(line, sizeof(line), fp)) {
    const char *delim = " \t\n";
    n++;

    if (line[0] == '#')
      continue;
    if (!(command = strtok_r(line, delim, &save_ptr))) {
      // Blank line - ignore
      continue;
    }
    if (!strcmp(command, "create")) {
      if (!(vol_name_cstr = strtok_r(nullptr, delim, &save_ptr))) {
        ERR("No vol_name in %s line %d",  filename, n);
        continue;
      }
      if (!(mount_point_cstr = strtok_r(nullptr, delim, &save_ptr))) {
        ERR("No mount point for volume '%s'. %s line %d", vol_name_cstr, filename, n);
        continue;
      }
      nsString  mount_point = NS_ConvertUTF8toUTF16(mount_point_cstr);
      nsString vol_name = NS_ConvertUTF8toUTF16(vol_name_cstr);
      nsresult rv;
      rv = vs->CreateFakeVolume(vol_name, mount_point);
      NS_ENSURE_SUCCESS_VOID(rv);
      rv = vs->SetFakeVolumeState(vol_name, nsIVolume::STATE_MOUNTED);
      NS_ENSURE_SUCCESS_VOID(rv);
    }
    else {
      ERR("Unrecognized command: '%s'", command);
    }
  }
}

void
InitAutoMounter()
{
  InitVolumeConfig();
  InitVolumeManager();
  sAutoMounterSetting = new AutoMounterSetting();

  XRE_GetIOMessageLoop()->PostTask(
      FROM_HERE,
      NewRunnableFunction(InitAutoMounterIOThread));

  // Switch Observers need to run on the main thread, so we need to
  // start it here and have it send events to the AutoMounter running
  // on the IO Thread.
  sUsbCableObserver = new UsbCableObserver();
}

int32_t
GetAutoMounterStatus()
{
  if (sAutoMounterSetting) {
    return sAutoMounterSetting->GetStatus();
  }
  return AUTOMOUNTER_STATUS_DISABLED;
}

//static
void
SetAutoMounterStatus(int32_t aStatus)
{
  if (sAutoMounterSetting) {
    sAutoMounterSetting->SetStatus(aStatus);
  }
}

void
SetAutoMounterMode(int32_t aMode)
{
  XRE_GetIOMessageLoop()->PostTask(
      FROM_HERE,
      NewRunnableFunction(SetAutoMounterModeIOThread, aMode));
}

void
SetAutoMounterSharingMode(const nsCString& aVolumeName, bool aAllowSharing)
{
  XRE_GetIOMessageLoop()->PostTask(
      FROM_HERE,
      NewRunnableFunction(SetAutoMounterSharingModeIOThread,
                          aVolumeName, aAllowSharing));
}

void
AutoMounterFormatVolume(const nsCString& aVolumeName)
{
  XRE_GetIOMessageLoop()->PostTask(
      FROM_HERE,
      NewRunnableFunction(AutoMounterFormatVolumeIOThread,
                          aVolumeName));
}

void
AutoMounterMountVolume(const nsCString& aVolumeName)
{
  XRE_GetIOMessageLoop()->PostTask(
      FROM_HERE,
      NewRunnableFunction(AutoMounterMountVolumeIOThread,
                          aVolumeName));
}

void
AutoMounterUnmountVolume(const nsCString& aVolumeName)
{
  XRE_GetIOMessageLoop()->PostTask(
      FROM_HERE,
      NewRunnableFunction(AutoMounterUnmountVolumeIOThread,
                          aVolumeName));
}

void
ShutdownAutoMounter()
{
  if (sAutoMounter) {
    DBG("ShutdownAutoMounter: About to StopMtpServer");
    sAutoMounter->StopMtpServer();
  }
  sAutoMounterSetting = nullptr;
  sUsbCableObserver = nullptr;

  XRE_GetIOMessageLoop()->PostTask(
      FROM_HERE,
      NewRunnableFunction(ShutdownAutoMounterIOThread));
}

} // system
} // mozilla