| // Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "device_manager.h" |
| |
| #include <libudev.h> |
| |
| #include <set> |
| |
| #include <base/bind.h> |
| #include <base/logging.h> |
| #include <base/memory/scoped_ptr.h> |
| #include <base/stl_util.h> |
| |
| #include "build_config.h" |
| #include "device_event_delegate.h" |
| #include "service_constants.h" |
| |
| // TODO(thestig) Merge these once libchrome catches up to Chromium's base, |
| // or when mtpd moves into its own repo. http://crbug.com/221123 |
| #if defined(CROS_BUILD) |
| #include <base/file_path.h> |
| #include <base/string_number_conversions.h> |
| #include <base/string_split.h> |
| #include <base/stringprintf.h> |
| #else |
| #include <base/files/file_path.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/string_split.h> |
| #include <base/strings/stringprintf.h> |
| #endif |
| |
| namespace { |
| |
| // For GetObjectHandles PTP operations, this tells GetObjectHandles to only |
| // list the objects of the root of a store. |
| // Use this when referring to the root node in the context of ReadDirectory(). |
| // This is an implementation detail that is not exposed to the outside. |
| const uint32_t kPtpGohRootParent = 0xFFFFFFFF; |
| |
| // Used to identify a PTP USB device interface. |
| const char kPtpUsbInterfaceClass[] = "6"; |
| const char kPtpUsbInterfaceSubClass[] = "1"; |
| const char kPtpUsbInterfaceProtocol[] = "1"; |
| |
| // Used to identify a vendor-specific USB device interface. |
| // Manufacturers sometimes do not report MTP/PTP capable devices using the |
| // well known PTP interface class. See libgphoto2 and libmtp device databases |
| // for examples. |
| const char kVendorSpecificUsbInterfaceClass[] = "255"; |
| |
| const char kUsbPrefix[] = "usb"; |
| const char kUDevEventType[] = "udev"; |
| const char kUDevUsbSubsystem[] = "usb"; |
| |
| gboolean GlibRunClosure(gpointer data) { |
| base::Closure* cb = reinterpret_cast<base::Closure*>(data); |
| cb->Run(); |
| delete cb; |
| return FALSE; |
| } |
| |
| std::string RawDeviceToString(const LIBMTP_raw_device_t& device) { |
| return base::StringPrintf("%s:%u,%d", kUsbPrefix, device.bus_location, |
| device.devnum); |
| } |
| |
| std::string StorageToString(const std::string& usb_bus_str, |
| uint32_t storage_id) { |
| return base::StringPrintf("%s:%u", usb_bus_str.c_str(), storage_id); |
| } |
| |
| struct LibmtpFileDeleter { |
| void operator()(LIBMTP_file_t* file) { |
| LIBMTP_destroy_file_t(file); |
| } |
| }; |
| |
| } // namespace |
| |
| namespace mtpd { |
| |
| DeviceManager::DeviceManager(DeviceEventDelegate* delegate) |
| : udev_(udev_new()), |
| udev_monitor_(NULL), |
| udev_monitor_fd_(-1), |
| delegate_(delegate), |
| weak_ptr_factory_(this) { |
| // Set up udev monitoring. |
| CHECK(delegate_); |
| CHECK(udev_); |
| udev_monitor_ = udev_monitor_new_from_netlink(udev_, kUDevEventType); |
| CHECK(udev_monitor_); |
| int ret = udev_monitor_filter_add_match_subsystem_devtype(udev_monitor_, |
| kUDevUsbSubsystem, |
| NULL); |
| CHECK_EQ(0, ret); |
| ret = udev_monitor_enable_receiving(udev_monitor_); |
| CHECK_EQ(0, ret); |
| udev_monitor_fd_ = udev_monitor_get_fd(udev_monitor_); |
| CHECK_GE(udev_monitor_fd_, 0); |
| |
| // Initialize libmtp. |
| LIBMTP_Init(); |
| |
| // Trigger a device scan. |
| AddDevices(NULL /* no callback source */); |
| } |
| |
| DeviceManager::~DeviceManager() { |
| RemoveDevices(true /* remove all */); |
| } |
| |
| // static |
| bool DeviceManager::ParseStorageName(const std::string& storage_name, |
| std::string* usb_bus_str, |
| uint32_t* storage_id) { |
| std::vector<std::string> split_str; |
| base::SplitString(storage_name, ':', &split_str); |
| if (split_str.size() != 3) |
| return false; |
| |
| if (split_str[0] != kUsbPrefix) |
| return false; |
| |
| uint32_t id = 0; |
| if (!base::StringToUint(split_str[2], &id)) |
| return false; |
| |
| *usb_bus_str = base::StringPrintf("%s:%s", kUsbPrefix, split_str[1].c_str()); |
| *storage_id = id; |
| return true; |
| } |
| |
| // static |
| bool DeviceManager::IsFolder(const LIBMTP_file_t* path_component, |
| size_t component_idx, |
| size_t num_path_components, |
| uint32_t* file_id) { |
| if (path_component->filetype != LIBMTP_FILETYPE_FOLDER) |
| return false; |
| |
| *file_id = path_component->item_id; |
| return true; |
| } |
| |
| // static |
| bool DeviceManager::IsValidComponentInFilePath( |
| const LIBMTP_file_t* path_component, |
| size_t component_idx, |
| size_t num_path_components, |
| uint32_t* file_id) { |
| bool is_file = (path_component->filetype != LIBMTP_FILETYPE_FOLDER); |
| bool is_last = (component_idx == num_path_components - 1); |
| if (is_file != is_last) |
| return false; |
| |
| *file_id = path_component->item_id; |
| return true; |
| } |
| |
| // static |
| bool DeviceManager::IsValidComponentInFileOrFolderPath( |
| const LIBMTP_file_t* path_component, |
| size_t component_idx, |
| size_t num_path_components, |
| uint32_t* file_id) { |
| bool is_file = (path_component->filetype != LIBMTP_FILETYPE_FOLDER); |
| bool is_last = (component_idx == num_path_components - 1); |
| if (is_file && !is_last) |
| return false; |
| |
| *file_id = path_component->item_id; |
| return true; |
| } |
| |
| int DeviceManager::GetDeviceEventDescriptor() const { |
| return udev_monitor_fd_; |
| } |
| |
| void DeviceManager::ProcessDeviceEvents() { |
| udev_device* dev = udev_monitor_receive_device(udev_monitor_); |
| CHECK(dev); |
| HandleDeviceNotification(dev); |
| udev_device_unref(dev); |
| } |
| |
| std::vector<std::string> DeviceManager::EnumerateStorages() { |
| std::vector<std::string> ret; |
| for (MtpDeviceMap::const_iterator device_it = device_map_.begin(); |
| device_it != device_map_.end(); |
| ++device_it) { |
| const std::string& usb_bus_str = device_it->first; |
| const MtpStorageMap& storage_map = device_it->second.second; |
| for (MtpStorageMap::const_iterator storage_it = storage_map.begin(); |
| storage_it != storage_map.end(); |
| ++storage_it) { |
| ret.push_back(StorageToString(usb_bus_str, storage_it->first)); |
| LOG(INFO) << "Found storage: " |
| << StorageToString(usb_bus_str, storage_it->first); |
| } |
| } |
| return ret; |
| } |
| |
| bool DeviceManager::HasStorage(const std::string& storage_name) { |
| return GetStorageInfo(storage_name) != NULL; |
| } |
| |
| const StorageInfo* DeviceManager::GetStorageInfo( |
| const std::string& storage_name) { |
| std::string usb_bus_str; |
| uint32_t storage_id = 0; |
| if (!ParseStorageName(storage_name, &usb_bus_str, &storage_id)) |
| return NULL; |
| |
| MtpDeviceMap::const_iterator device_it = device_map_.find(usb_bus_str); |
| if (device_it == device_map_.end()) |
| return NULL; |
| |
| const MtpStorageMap& storage_map = device_it->second.second; |
| MtpStorageMap::const_iterator storage_it = storage_map.find(storage_id); |
| return (storage_it != storage_map.end()) ? &(storage_it->second) : NULL; |
| } |
| |
| bool DeviceManager::ReadDirectoryByPath(const std::string& storage_name, |
| const std::string& file_path, |
| std::vector<FileEntry>* out) { |
| LIBMTP_mtpdevice_t* mtp_device = NULL; |
| uint32_t storage_id = 0; |
| if (!GetDeviceAndStorageId(storage_name, &mtp_device, &storage_id)) |
| return false; |
| |
| uint32_t file_id = 0; |
| if (!PathToFileId(mtp_device, storage_id, file_path, IsFolder, &file_id)) |
| return false; |
| return ReadDirectory(mtp_device, storage_id, file_id, out); |
| } |
| |
| bool DeviceManager::ReadDirectoryById(const std::string& storage_name, |
| uint32_t file_id, |
| std::vector<FileEntry>* out) { |
| LIBMTP_mtpdevice_t* mtp_device = NULL; |
| uint32_t storage_id = 0; |
| if (!GetDeviceAndStorageId(storage_name, &mtp_device, &storage_id)) |
| return false; |
| if (file_id == kRootFileId) |
| file_id = kPtpGohRootParent; |
| return ReadDirectory(mtp_device, storage_id, file_id, out); |
| } |
| |
| bool DeviceManager::ReadFileChunkByPath(const std::string& storage_name, |
| const std::string& file_path, |
| uint32_t offset, |
| uint32_t count, |
| std::vector<uint8_t>* out) { |
| LIBMTP_mtpdevice_t* mtp_device = NULL; |
| uint32_t storage_id = 0; |
| if (!GetDeviceAndStorageId(storage_name, &mtp_device, &storage_id)) |
| return false; |
| |
| uint32_t file_id = 0; |
| if (!PathToFileId(mtp_device, storage_id, file_path, |
| IsValidComponentInFilePath, &file_id)) { |
| return false; |
| } |
| return ReadFileChunk(mtp_device, file_id, offset, count, out); |
| } |
| |
| bool DeviceManager::ReadFileChunkById(const std::string& storage_name, |
| uint32_t file_id, |
| uint32_t offset, |
| uint32_t count, |
| std::vector<uint8_t>* out) { |
| LIBMTP_mtpdevice_t* mtp_device = NULL; |
| uint32_t storage_id = 0; |
| if (!GetDeviceAndStorageId(storage_name, &mtp_device, &storage_id)) |
| return false; |
| return ReadFileChunk(mtp_device, file_id, offset, count, out); |
| } |
| |
| bool DeviceManager::GetFileInfoByPath(const std::string& storage_name, |
| const std::string& file_path, |
| FileEntry* out) { |
| LIBMTP_mtpdevice_t* mtp_device = NULL; |
| uint32_t storage_id = 0; |
| if (!GetDeviceAndStorageId(storage_name, &mtp_device, &storage_id)) |
| return false; |
| |
| uint32_t file_id = 0; |
| if (!PathToFileId(mtp_device, storage_id, file_path, |
| IsValidComponentInFileOrFolderPath, &file_id)) { |
| return false; |
| } |
| |
| if (file_id == kPtpGohRootParent) |
| file_id = kRootFileId; |
| return GetFileInfo(mtp_device, storage_id, file_id, out); |
| } |
| |
| bool DeviceManager::GetFileInfoById(const std::string& storage_name, |
| uint32_t file_id, |
| FileEntry* out) { |
| LIBMTP_mtpdevice_t* mtp_device = NULL; |
| uint32_t storage_id = 0; |
| if (!GetDeviceAndStorageId(storage_name, &mtp_device, &storage_id)) |
| return false; |
| return GetFileInfo(mtp_device, storage_id, file_id, out); |
| } |
| |
| bool DeviceManager::AddStorageForTest(const std::string& storage_name, |
| const StorageInfo& storage_info) { |
| std::string device_location; |
| uint32_t storage_id; |
| if (!ParseStorageName(storage_name, &device_location, &storage_id)) |
| return false; |
| |
| MtpDeviceMap::iterator it = device_map_.find(device_location); |
| if (it == device_map_.end()) { |
| // New device case. |
| MtpStorageMap new_storage_map; |
| new_storage_map.insert(std::make_pair(storage_id, storage_info)); |
| MtpDevice new_mtp_device = |
| std::make_pair(static_cast<LIBMTP_mtpdevice_t*>(NULL), |
| new_storage_map); |
| device_map_.insert(std::make_pair(device_location, new_mtp_device)); |
| return true; |
| } |
| |
| // Existing device case. |
| // There should be no real LIBMTP_mtpdevice_t device for this dummy storage. |
| MtpDevice& existing_mtp_device = it->second; |
| if (existing_mtp_device.first) |
| return false; |
| |
| // And the storage should not already exist. |
| MtpStorageMap& existing_mtp_storage_map = existing_mtp_device.second; |
| if (ContainsKey(existing_mtp_storage_map, storage_id)) |
| return false; |
| |
| existing_mtp_storage_map.insert(std::make_pair(storage_id, storage_info)); |
| return true; |
| } |
| |
| bool DeviceManager::PathToFileId(LIBMTP_mtpdevice_t* device, |
| uint32_t storage_id, |
| const std::string& file_path, |
| ProcessPathComponentFunc process_func, |
| uint32_t* file_id) { |
| std::vector<base::FilePath::StringType> path_components; |
| base::FilePath(file_path).GetComponents(&path_components); |
| uint32_t current_file_id = kPtpGohRootParent; |
| const size_t num_path_components = path_components.size(); |
| for (size_t i = 0; i < num_path_components; ++i) { |
| if (path_components[i] == "/") |
| continue; |
| |
| LIBMTP_file_t* files = |
| LIBMTP_Get_Files_And_Folders(device, storage_id, current_file_id); |
| // Iterate through all files. |
| const uint32_t old_file_id = current_file_id; |
| LIBMTP_file_t* file = files; |
| while (file != NULL) { |
| scoped_ptr<LIBMTP_file_t, LibmtpFileDeleter> current_file(file); |
| file = file->next; |
| if (current_file.get()->filename != path_components[i]) |
| continue; |
| |
| // Found matching file name. See if it is valid. |
| if (!process_func(current_file.get(), i, num_path_components, |
| ¤t_file_id)) { |
| return false; |
| } |
| } |
| // If no matching component was found. |
| if (old_file_id == current_file_id) |
| return false; |
| } |
| // Successfully iterated through all path components. |
| *file_id = current_file_id; |
| return true; |
| } |
| |
| bool DeviceManager::ReadDirectory(LIBMTP_mtpdevice_t* device, |
| uint32_t storage_id, |
| uint32_t file_id, |
| std::vector<FileEntry>* out) { |
| LIBMTP_file_t* file = |
| LIBMTP_Get_Files_And_Folders(device, storage_id, file_id); |
| while (file != NULL) { |
| scoped_ptr<LIBMTP_file_t, LibmtpFileDeleter> current_file(file); |
| file = file->next; |
| out->push_back(FileEntry(*current_file)); |
| } |
| return true; |
| } |
| |
| bool DeviceManager::ReadFileChunk(LIBMTP_mtpdevice_t* device, |
| uint32_t file_id, |
| uint32_t offset, |
| uint32_t count, |
| std::vector<uint8_t>* out) { |
| // The root node is a virtual node and cannot be read from. |
| if (file_id == kRootFileId) |
| return false; |
| |
| uint8_t* data = NULL; |
| uint32_t bytes_read = 0; |
| int transfer_status = LIBMTP_Get_File_Chunk(device, |
| file_id, |
| offset, |
| count, |
| &data, |
| &bytes_read); |
| |
| // Own |data| in a scoper so it gets freed when this function returns. |
| scoped_ptr_malloc<uint8_t> scoped_data(data); |
| |
| if (transfer_status != 0 || bytes_read != count) |
| return false; |
| |
| for (size_t i = 0; i < count; ++i) |
| out->push_back(data[i]); |
| return true; |
| } |
| |
| bool DeviceManager::GetFileInfo(LIBMTP_mtpdevice_t* device, |
| uint32_t storage_id, |
| uint32_t file_id, |
| FileEntry* out) { |
| LIBMTP_file_t* file = (file_id == kRootFileId) ? |
| LIBMTP_new_file_t() : |
| LIBMTP_Get_Filemetadata(device, file_id); |
| if (!file) |
| return false; |
| |
| // LIBMTP_Get_Filemetadata() does not know how to handle the root node, so |
| // fill in relevant fields in the struct manually. The rest of the struct has |
| // already been initialized by LIBMTP_new_file_t(). |
| if (file_id == kRootFileId) { |
| file->storage_id = storage_id; |
| file->filename = strdup("/"); |
| file->filetype = LIBMTP_FILETYPE_FOLDER; |
| } |
| |
| *out = FileEntry(*file); |
| LIBMTP_destroy_file_t(file); |
| return true; |
| } |
| |
| bool DeviceManager::GetDeviceAndStorageId(const std::string& storage_name, |
| LIBMTP_mtpdevice_t** mtp_device, |
| uint32_t* storage_id) { |
| std::string usb_bus_str; |
| uint32_t id = 0; |
| if (!ParseStorageName(storage_name, &usb_bus_str, &id)) |
| return false; |
| |
| MtpDeviceMap::const_iterator device_it = device_map_.find(usb_bus_str); |
| if (device_it == device_map_.end()) |
| return false; |
| |
| const MtpStorageMap& storage_map = device_it->second.second; |
| if (!ContainsKey(storage_map, id)) |
| return false; |
| |
| *storage_id = id; |
| *mtp_device = device_it->second.first; |
| return true; |
| } |
| |
| void DeviceManager::HandleDeviceNotification(udev_device* device) { |
| const char* action = udev_device_get_property_value(device, "ACTION"); |
| const char* interface = udev_device_get_property_value(device, "INTERFACE"); |
| if (!action || !interface) |
| return; |
| |
| // Check the USB interface. Since this gets called many times by udev for a |
| // given physical action, use the udev "INTERFACE" event property as a quick |
| // way of getting one unique and interesting udev event for a given physical |
| // action. At the same time, do some light filtering and ignore events for |
| // uninteresting devices. |
| const std::string kEventInterface(interface); |
| std::vector<std::string> split_usb_interface; |
| base::SplitString(kEventInterface, '/', &split_usb_interface); |
| if (split_usb_interface.size() != 3) |
| return; |
| |
| // Check to see if the device has a vendor-specific interface class. |
| // In this case, continue and let libmtp figure it out. |
| const std::string& usb_interface_class = split_usb_interface[0]; |
| const std::string& usb_interface_subclass = split_usb_interface[1]; |
| const std::string& usb_interface_protocol = split_usb_interface[2]; |
| bool is_interesting_device = |
| (usb_interface_class == kVendorSpecificUsbInterfaceClass); |
| if (!is_interesting_device) { |
| // Many MTP/PTP devices have this PTP interface. |
| is_interesting_device = |
| (usb_interface_class == kPtpUsbInterfaceClass && |
| usb_interface_subclass == kPtpUsbInterfaceSubClass && |
| usb_interface_protocol == kPtpUsbInterfaceProtocol); |
| } |
| if (!is_interesting_device) |
| return; |
| |
| // Handle the action. |
| const std::string kEventAction(action); |
| if (kEventAction == "add") { |
| // Some devices do not respond well when immediately probed. Thus there is |
| // a 1 second wait here to give the device to settle down. |
| GSource* source = g_timeout_source_new_seconds(1); |
| base::Closure* cb = |
| new base::Closure(base::Bind(&DeviceManager::AddDevices, |
| weak_ptr_factory_.GetWeakPtr(), |
| source)); |
| g_source_set_callback(source, &GlibRunClosure, cb, NULL); |
| g_source_attach(source, NULL); |
| return; |
| } |
| if (kEventAction == "remove") { |
| RemoveDevices(false /* !remove_all */); |
| return; |
| } |
| // udev notes the existence of other actions like "change" and "move", but |
| // they have never been observed with real MTP/PTP devices in testing. |
| } |
| |
| void DeviceManager::AddDevices(GSource* source) { |
| if (source) { |
| // Matches g_source_attach(). |
| g_source_destroy(source); |
| // Matches the implicit add-ref in g_timeout_source_new_seconds(). |
| g_source_unref(source); |
| } |
| |
| // Get raw devices. |
| LIBMTP_raw_device_t* raw_devices = NULL; |
| int raw_devices_count = 0; |
| LIBMTP_error_number_t err = |
| LIBMTP_Detect_Raw_Devices(&raw_devices, &raw_devices_count); |
| if (err != LIBMTP_ERROR_NONE) { |
| LOG(ERROR) << "LIBMTP_Detect_Raw_Devices failed with " << err; |
| return; |
| } |
| |
| // Iterate through raw devices. |
| for (int i = 0; i < raw_devices_count; ++i) { |
| const std::string usb_bus_str = RawDeviceToString(raw_devices[i]); |
| // Skip devices that have already been opened. |
| if (ContainsKey(device_map_, usb_bus_str)) |
| continue; |
| |
| // Open the mtp device. |
| LIBMTP_mtpdevice_t* mtp_device = |
| LIBMTP_Open_Raw_Device_Uncached(&raw_devices[i]); |
| if (!mtp_device) { |
| LOG(ERROR) << "LIBMTP_Open_Raw_Device_Uncached failed for " |
| << usb_bus_str; |
| continue; |
| } |
| |
| // Fetch fallback vendor / product info. |
| scoped_ptr_malloc<char> duplicated_string; |
| duplicated_string.reset(LIBMTP_Get_Manufacturername(mtp_device)); |
| std::string fallback_vendor; |
| if (duplicated_string.get()) |
| fallback_vendor = duplicated_string.get(); |
| |
| duplicated_string.reset(LIBMTP_Get_Modelname(mtp_device)); |
| std::string fallback_product; |
| if (duplicated_string.get()) |
| fallback_product = duplicated_string.get(); |
| |
| // Iterate through storages on the device and add them. |
| MtpStorageMap storage_map; |
| for (LIBMTP_devicestorage_t* storage = mtp_device->storage; |
| storage != NULL; |
| storage = storage->next) { |
| const std::string storage_name = |
| StorageToString(usb_bus_str, storage->id); |
| StorageInfo info(storage_name, |
| raw_devices[i].device_entry, |
| *storage, |
| fallback_vendor, |
| fallback_product); |
| bool storage_added = |
| storage_map.insert(std::make_pair(storage->id, info)).second; |
| CHECK(storage_added); |
| delegate_->StorageAttached(storage_name); |
| LOG(INFO) << "Added storage " << storage_name; |
| } |
| bool device_added = device_map_.insert( |
| std::make_pair(usb_bus_str, |
| std::make_pair(mtp_device, storage_map))).second; |
| CHECK(device_added); |
| LOG(INFO) << "Added device " << usb_bus_str << " with " |
| << storage_map.size() << " storages"; |
| } |
| free(raw_devices); |
| } |
| |
| void DeviceManager::RemoveDevices(bool remove_all) { |
| LIBMTP_raw_device_t* raw_devices = NULL; |
| int raw_devices_count = 0; |
| |
| if (!remove_all) { |
| LIBMTP_error_number_t err = |
| LIBMTP_Detect_Raw_Devices(&raw_devices, &raw_devices_count); |
| if (!(err == LIBMTP_ERROR_NONE || err == LIBMTP_ERROR_NO_DEVICE_ATTACHED)) { |
| LOG(ERROR) << "LIBMTP_Detect_Raw_Devices failed with " << err; |
| return; |
| } |
| } |
| |
| // Populate |devices_set| with all known attached devices. |
| typedef std::set<std::string> MtpDeviceSet; |
| MtpDeviceSet devices_set; |
| for (MtpDeviceMap::const_iterator it = device_map_.begin(); |
| it != device_map_.end(); |
| ++it) { |
| devices_set.insert(it->first); |
| } |
| |
| // And remove the ones that are still attached. |
| for (int i = 0; i < raw_devices_count; ++i) |
| devices_set.erase(RawDeviceToString(raw_devices[i])); |
| |
| // The ones left in the set are the detached devices. |
| for (MtpDeviceSet::const_iterator it = devices_set.begin(); |
| it != devices_set.end(); |
| ++it) { |
| LOG(INFO) << "Removed " << *it; |
| MtpDeviceMap::iterator device_it = device_map_.find(*it); |
| if (device_it == device_map_.end()) { |
| NOTREACHED(); |
| continue; |
| } |
| |
| // Remove all the storages on that device. |
| const std::string& usb_bus_str = device_it->first; |
| const MtpStorageMap& storage_map = device_it->second.second; |
| for (MtpStorageMap::const_iterator storage_it = storage_map.begin(); |
| storage_it != storage_map.end(); |
| ++storage_it) { |
| delegate_->StorageDetached( |
| StorageToString(usb_bus_str, storage_it->first)); |
| } |
| |
| // Delete the device's map entry and cleanup. |
| LIBMTP_mtpdevice_t* mtp_device = device_it->second.first; |
| device_map_.erase(device_it); |
| |
| // |mtp_device| can be NULL in testing. |
| if (!mtp_device) |
| continue; |
| |
| // When |remove_all| is false, the device has already been detached |
| // and this runs after the fact. As such, this call will very |
| // likely fail and spew a bunch of error messages. Call it anyway to |
| // let libmtp do any cleanup it can. |
| LIBMTP_Release_Device(mtp_device); |
| } |
| } |
| |
| } // namespace mtpd |