| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 // RemovableDeviceNotificationsLinux implementation. | |
| 6 | |
| 7 #include "chrome/browser/media_gallery/removable_device_notifications_linux.h" | |
| 8 | |
| 9 #include <libudev.h> | |
| 10 #include <mntent.h> | |
| 11 #include <stdio.h> | |
| 12 | |
| 13 #include <list> | |
| 14 | |
| 15 #include "base/bind.h" | |
| 16 #include "base/file_path.h" | |
| 17 #include "base/memory/scoped_generic_obj.h" | |
| 18 #include "base/metrics/histogram.h" | |
| 19 #include "base/stl_util.h" | |
| 20 #include "base/string_number_conversions.h" | |
| 21 #include "base/string_util.h" | |
| 22 #include "base/stringprintf.h" | |
| 23 #include "base/system_monitor/system_monitor.h" | |
| 24 #include "base/utf_string_conversions.h" | |
| 25 #include "chrome/browser/media_gallery/media_device_notifications_utils.h" | |
| 26 #include "chrome/browser/media_gallery/media_gallery_constants.h" | |
| 27 #include "chrome/browser/media_gallery/media_storage_util.h" | |
| 28 | |
| 29 namespace chrome { | |
| 30 | |
| 31 using base::SystemMonitor; | |
| 32 using content::BrowserThread; | |
| 33 | |
| 34 namespace { | |
| 35 | |
| 36 static RemovableDeviceNotificationsLinux* | |
| 37 g_removable_device_notifications_linux = NULL; | |
| 38 | |
| 39 // List of file systems we care about. | |
| 40 const char* const kKnownFileSystems[] = { | |
| 41 "ext2", | |
| 42 "ext3", | |
| 43 "ext4", | |
| 44 "fat", | |
| 45 "hfsplus", | |
| 46 "iso9660", | |
| 47 "msdos", | |
| 48 "ntfs", | |
| 49 "udf", | |
| 50 "vfat", | |
| 51 }; | |
| 52 | |
| 53 // udev device property constants. | |
| 54 const char kBlockSubsystemKey[] = "block"; | |
| 55 const char kDevName[] = "DEVNAME"; | |
| 56 const char kDiskDeviceTypeKey[] = "disk"; | |
| 57 const char kFsUUID[] = "ID_FS_UUID"; | |
| 58 const char kLabel[] = "ID_FS_LABEL"; | |
| 59 const char kModel[] = "ID_MODEL"; | |
| 60 const char kModelID[] = "ID_MODEL_ID"; | |
| 61 const char kRemovableSysAttr[] = "removable"; | |
| 62 const char kSerial[] = "ID_SERIAL"; | |
| 63 const char kSerialShort[] = "ID_SERIAL_SHORT"; | |
| 64 const char kVendor[] = "ID_VENDOR"; | |
| 65 const char kVendorID[] = "ID_VENDOR_ID"; | |
| 66 | |
| 67 // (mount point, mount device) | |
| 68 // A mapping from mount point to mount device, as extracted from the mtab | |
| 69 // file. | |
| 70 typedef std::map<FilePath, FilePath> MountPointDeviceMap; | |
| 71 | |
| 72 // Reads mtab file entries into |mtab|. | |
| 73 void ReadMtab(const FilePath& mtab_path, | |
| 74 const std::set<std::string>& interesting_file_systems, | |
| 75 MountPointDeviceMap* mtab) { | |
| 76 mtab->clear(); | |
| 77 | |
| 78 FILE* fp = setmntent(mtab_path.value().c_str(), "r"); | |
| 79 if (!fp) | |
| 80 return; | |
| 81 | |
| 82 mntent entry; | |
| 83 char buf[512]; | |
| 84 | |
| 85 // We return the same device mounted to multiple locations, but hide | |
| 86 // devices that have been mounted over. | |
| 87 while (getmntent_r(fp, &entry, buf, sizeof(buf))) { | |
| 88 // We only care about real file systems. | |
| 89 if (!ContainsKey(interesting_file_systems, entry.mnt_type)) | |
| 90 continue; | |
| 91 | |
| 92 (*mtab)[FilePath(entry.mnt_dir)] = FilePath(entry.mnt_fsname); | |
| 93 } | |
| 94 endmntent(fp); | |
| 95 } | |
| 96 | |
| 97 // ScopedGenericObj functor for UdevObjectRelease(). | |
| 98 class ScopedReleaseUdevObject { | |
| 99 public: | |
| 100 void operator()(struct udev* udev) const { | |
| 101 udev_unref(udev); | |
| 102 } | |
| 103 }; | |
| 104 typedef ScopedGenericObj<struct udev*, | |
| 105 ScopedReleaseUdevObject> ScopedUdevObject; | |
| 106 | |
| 107 // ScopedGenericObj functor for UdevDeviceObjectRelease(). | |
| 108 class ScopedReleaseUdevDeviceObject { | |
| 109 public: | |
| 110 void operator()(struct udev_device* device) const { | |
| 111 udev_device_unref(device); | |
| 112 } | |
| 113 }; | |
| 114 typedef ScopedGenericObj<struct udev_device*, | |
| 115 ScopedReleaseUdevDeviceObject> ScopedUdevDeviceObject; | |
| 116 | |
| 117 // Wrapper function for udev_device_get_property_value() that also checks for | |
| 118 // valid but empty values. | |
| 119 std::string GetUdevDevicePropertyValue(struct udev_device* udev_device, | |
| 120 const char* key) { | |
| 121 const char* value = udev_device_get_property_value(udev_device, key); | |
| 122 if (!value) | |
| 123 return std::string(); | |
| 124 return (strlen(value) > 0) ? value : std::string(); | |
| 125 } | |
| 126 | |
| 127 // Construct a device id using label or manufacturer (vendor and model) details. | |
| 128 std::string MakeDeviceUniqueId(struct udev_device* device) { | |
| 129 std::string uuid = GetUdevDevicePropertyValue(device, kFsUUID); | |
| 130 if (!uuid.empty()) | |
| 131 return kFSUniqueIdPrefix + uuid; | |
| 132 | |
| 133 // If one of the vendor, model, serial information is missing, its value | |
| 134 // in the string is empty. | |
| 135 // Format: VendorModelSerial:VendorInfo:ModelInfo:SerialShortInfo | |
| 136 // E.g.: VendorModelSerial:Kn:DataTravel_12.10:8000000000006CB02CDB | |
| 137 std::string vendor = GetUdevDevicePropertyValue(device, kVendorID); | |
| 138 std::string model = GetUdevDevicePropertyValue(device, kModelID); | |
| 139 std::string serial_short = GetUdevDevicePropertyValue(device, | |
| 140 kSerialShort); | |
| 141 if (vendor.empty() && model.empty() && serial_short.empty()) | |
| 142 return std::string(); | |
| 143 | |
| 144 return base::StringPrintf("%s%s%s%s%s%s", | |
| 145 kVendorModelSerialPrefix, | |
| 146 vendor.c_str(), kNonSpaceDelim, | |
| 147 model.c_str(), kNonSpaceDelim, | |
| 148 serial_short.c_str()); | |
| 149 } | |
| 150 | |
| 151 // Get the device information using udev library. | |
| 152 // On success, returns true and fill in |unique_id|, |name|, and |removable|. | |
| 153 bool GetDeviceInfo(const FilePath& device_path, std::string* unique_id, | |
| 154 string16* name, bool* removable) { | |
| 155 DCHECK(!device_path.empty()); | |
| 156 | |
| 157 ScopedUdevObject udev_obj(udev_new()); | |
| 158 if (!udev_obj.get()) | |
| 159 return false; | |
| 160 | |
| 161 struct stat device_stat; | |
| 162 if (stat(device_path.value().c_str(), &device_stat) < 0) | |
| 163 return false; | |
| 164 | |
| 165 char device_type; | |
| 166 if (S_ISCHR(device_stat.st_mode)) | |
| 167 device_type = 'c'; | |
| 168 else if (S_ISBLK(device_stat.st_mode)) | |
| 169 device_type = 'b'; | |
| 170 else | |
| 171 return false; // Not a supported type. | |
| 172 | |
| 173 ScopedUdevDeviceObject device( | |
| 174 udev_device_new_from_devnum(udev_obj, device_type, device_stat.st_rdev)); | |
| 175 if (!device.get()) | |
| 176 return false; | |
| 177 | |
| 178 // Construct a device name using label or manufacturer (vendor and model) | |
| 179 // details. | |
| 180 if (name) { | |
| 181 std::string device_label = GetUdevDevicePropertyValue(device, kLabel); | |
| 182 if (device_label.empty()) | |
| 183 device_label = GetUdevDevicePropertyValue(device, kSerial); | |
| 184 if (device_label.empty()) { | |
| 185 // Format: VendorInfo ModelInfo | |
| 186 // E.g.: KnCompany Model2010 | |
| 187 std::string vendor_name = GetUdevDevicePropertyValue(device, kVendor); | |
| 188 std::string model_name = GetUdevDevicePropertyValue(device, kModel); | |
| 189 if (vendor_name.empty() && model_name.empty()) | |
| 190 return false; | |
| 191 | |
| 192 if (vendor_name.empty()) | |
| 193 device_label = model_name; | |
| 194 else if (model_name.empty()) | |
| 195 device_label = vendor_name; | |
| 196 else | |
| 197 device_label = vendor_name + kSpaceDelim + model_name; | |
| 198 } | |
| 199 | |
| 200 if (IsStringUTF8(device_label)) | |
| 201 *name = UTF8ToUTF16(device_label); | |
| 202 } | |
| 203 | |
| 204 if (unique_id) { | |
| 205 *unique_id = MakeDeviceUniqueId(device); | |
| 206 if (unique_id->empty()) | |
| 207 return false; | |
| 208 } | |
| 209 | |
| 210 if (removable) { | |
| 211 const char* value = udev_device_get_sysattr_value(device, | |
| 212 kRemovableSysAttr); | |
| 213 if (!value) { | |
| 214 // |parent_device| is owned by |device| and does not need to be cleaned | |
| 215 // up. | |
| 216 struct udev_device* parent_device = | |
| 217 udev_device_get_parent_with_subsystem_devtype(device, | |
| 218 kBlockSubsystemKey, | |
| 219 kDiskDeviceTypeKey); | |
| 220 value = udev_device_get_sysattr_value(parent_device, kRemovableSysAttr); | |
| 221 } | |
| 222 *removable = (value && atoi(value) == 1); | |
| 223 } | |
| 224 return true; | |
| 225 } | |
| 226 | |
| 227 } // namespace | |
| 228 | |
| 229 RemovableDeviceNotificationsLinux::MountPointInfo::MountPointInfo() { | |
| 230 } | |
| 231 | |
| 232 RemovableDeviceNotificationsLinux::RemovableDeviceNotificationsLinux( | |
| 233 const FilePath& path) | |
| 234 : initialized_(false), | |
| 235 mtab_path_(path), | |
| 236 get_device_info_func_(&GetDeviceInfo) { | |
| 237 DCHECK(!g_removable_device_notifications_linux); | |
| 238 g_removable_device_notifications_linux = this; | |
| 239 } | |
| 240 | |
| 241 RemovableDeviceNotificationsLinux::RemovableDeviceNotificationsLinux( | |
| 242 const FilePath& path, | |
| 243 GetDeviceInfoFunc get_device_info_func) | |
| 244 : initialized_(false), | |
| 245 mtab_path_(path), | |
| 246 get_device_info_func_(get_device_info_func) { | |
| 247 DCHECK(!g_removable_device_notifications_linux); | |
| 248 g_removable_device_notifications_linux = this; | |
| 249 } | |
| 250 | |
| 251 RemovableDeviceNotificationsLinux::~RemovableDeviceNotificationsLinux() { | |
| 252 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); | |
| 253 DCHECK_EQ(this, g_removable_device_notifications_linux); | |
| 254 g_removable_device_notifications_linux = NULL; | |
| 255 } | |
| 256 | |
| 257 // static | |
| 258 RemovableDeviceNotificationsLinux* | |
| 259 RemovableDeviceNotificationsLinux::GetInstance() { | |
| 260 DCHECK(g_removable_device_notifications_linux != NULL); | |
| 261 return g_removable_device_notifications_linux; | |
| 262 } | |
| 263 | |
| 264 void RemovableDeviceNotificationsLinux::Init() { | |
| 265 DCHECK(!mtab_path_.empty()); | |
| 266 | |
| 267 // Put |kKnownFileSystems| in std::set to get O(log N) access time. | |
| 268 for (size_t i = 0; i < arraysize(kKnownFileSystems); ++i) | |
| 269 known_file_systems_.insert(kKnownFileSystems[i]); | |
| 270 | |
| 271 BrowserThread::PostTask( | |
| 272 BrowserThread::FILE, FROM_HERE, | |
| 273 base::Bind(&RemovableDeviceNotificationsLinux::InitOnFileThread, this)); | |
| 274 } | |
| 275 | |
| 276 FilePath RemovableDeviceNotificationsLinux::GetDeviceMountPoint( | |
| 277 const std::string& device_id) const { | |
| 278 | |
| 279 MediaStorageUtil::Type type; | |
| 280 MediaStorageUtil::CrackDeviceId(device_id, &type, NULL); | |
| 281 if (type == MediaStorageUtil::MTP_OR_PTP) | |
| 282 return FilePath(); | |
| 283 DCHECK(type == MediaStorageUtil::REMOVABLE_MASS_STORAGE_WITH_DCIM || | |
| 284 type == MediaStorageUtil::REMOVABLE_MASS_STORAGE_NO_DCIM || | |
| 285 type == MediaStorageUtil::FIXED_MASS_STORAGE); | |
| 286 | |
| 287 FilePath mount_device; | |
| 288 for (MountMap::const_iterator it = mount_info_map_.begin(); | |
| 289 it != mount_info_map_.end(); | |
| 290 ++it) { | |
| 291 if (it->second.device_id == device_id) { | |
| 292 mount_device = it->second.mount_device; | |
| 293 break; | |
| 294 } | |
| 295 } | |
| 296 if (mount_device.empty()) | |
| 297 return mount_device; | |
| 298 | |
| 299 const ReferencedMountPoint& referenced_info = | |
| 300 mount_priority_map_.find(mount_device)->second; | |
| 301 for (ReferencedMountPoint::const_iterator it = referenced_info.begin(); | |
| 302 it != referenced_info.end(); | |
| 303 ++it) { | |
| 304 if (it->second) | |
| 305 return it->first; | |
| 306 } | |
| 307 // If none of them are default, just return the first. | |
| 308 return FilePath(referenced_info.begin()->first); | |
| 309 } | |
| 310 | |
| 311 bool RemovableDeviceNotificationsLinux::GetDeviceInfoForPath( | |
| 312 const FilePath& path, | |
| 313 SystemMonitor::RemovableStorageInfo* device_info) const { | |
| 314 if (!path.IsAbsolute()) | |
| 315 return false; | |
| 316 | |
| 317 FilePath current = path; | |
| 318 while (!ContainsKey(mount_info_map_, current) && current != current.DirName()) | |
| 319 current = current.DirName(); | |
| 320 | |
| 321 MountMap::const_iterator mount_info = mount_info_map_.find(current); | |
| 322 if (mount_info == mount_info_map_.end()) | |
| 323 return false; | |
| 324 | |
| 325 if (device_info) { | |
| 326 device_info->device_id = mount_info->second.device_id; | |
| 327 device_info->name = mount_info->second.device_name; | |
| 328 device_info->location = current.value(); | |
| 329 } | |
| 330 return true; | |
| 331 } | |
| 332 | |
| 333 void RemovableDeviceNotificationsLinux::OnFilePathChanged(const FilePath& path, | |
| 334 bool error) { | |
| 335 if (path != mtab_path_) { | |
| 336 // This cannot happen unless FilePathWatcher is buggy. Just ignore this | |
| 337 // notification and do nothing. | |
| 338 NOTREACHED(); | |
| 339 return; | |
| 340 } | |
| 341 if (error) { | |
| 342 LOG(ERROR) << "Error watching " << mtab_path_.value(); | |
| 343 return; | |
| 344 } | |
| 345 | |
| 346 UpdateMtab(); | |
| 347 } | |
| 348 | |
| 349 void RemovableDeviceNotificationsLinux::InitOnFileThread() { | |
| 350 DCHECK(!initialized_); | |
| 351 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); | |
| 352 initialized_ = true; | |
| 353 | |
| 354 // The callback passed to Watch() has to be unretained. Otherwise | |
| 355 // RemovableDeviceNotificationsLinux will live longer than expected, and | |
| 356 // FilePathWatcher will get in trouble at shutdown time. | |
| 357 bool ret = file_watcher_.Watch( | |
| 358 mtab_path_, | |
| 359 base::Bind(&RemovableDeviceNotificationsLinux::OnFilePathChanged, | |
| 360 base::Unretained(this))); | |
| 361 if (!ret) { | |
| 362 LOG(ERROR) << "Adding watch for " << mtab_path_.value() << " failed"; | |
| 363 return; | |
| 364 } | |
| 365 | |
| 366 UpdateMtab(); | |
| 367 } | |
| 368 | |
| 369 void RemovableDeviceNotificationsLinux::UpdateMtab() { | |
| 370 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); | |
| 371 | |
| 372 MountPointDeviceMap new_mtab; | |
| 373 ReadMtab(mtab_path_, known_file_systems_, &new_mtab); | |
| 374 | |
| 375 // Check existing mtab entries for unaccounted mount points. | |
| 376 // These mount points must have been removed in the new mtab. | |
| 377 std::list<FilePath> mount_points_to_erase; | |
| 378 std::list<FilePath> multiple_mounted_devices_needing_reattachment; | |
| 379 for (MountMap::const_iterator old_iter = mount_info_map_.begin(); | |
| 380 old_iter != mount_info_map_.end(); ++old_iter) { | |
| 381 const FilePath& mount_point = old_iter->first; | |
| 382 const FilePath& mount_device = old_iter->second.mount_device; | |
| 383 MountPointDeviceMap::iterator new_iter = new_mtab.find(mount_point); | |
| 384 // |mount_point| not in |new_mtab| or |mount_device| is no longer mounted at | |
| 385 // |mount_point|. | |
| 386 if (new_iter == new_mtab.end() || (new_iter->second != mount_device)) { | |
| 387 MountPriorityMap::iterator priority = | |
| 388 mount_priority_map_.find(mount_device); | |
| 389 DCHECK(priority != mount_priority_map_.end()); | |
| 390 ReferencedMountPoint::const_iterator has_priority = | |
| 391 priority->second.find(mount_point); | |
| 392 if (MediaStorageUtil::IsRemovableDevice(old_iter->second.device_id)) { | |
| 393 DCHECK(has_priority != priority->second.end()); | |
| 394 if (has_priority->second) { | |
| 395 SystemMonitor::Get()->ProcessRemovableStorageDetached( | |
| 396 old_iter->second.device_id); | |
| 397 } | |
| 398 if (priority->second.size() > 1) | |
| 399 multiple_mounted_devices_needing_reattachment.push_back(mount_device); | |
| 400 } | |
| 401 priority->second.erase(mount_point); | |
| 402 if (priority->second.empty()) | |
| 403 mount_priority_map_.erase(mount_device); | |
| 404 mount_points_to_erase.push_back(mount_point); | |
| 405 } | |
| 406 } | |
| 407 | |
| 408 // Erase the |mount_info_map_| entries afterwards. Erasing in the loop above | |
| 409 // using the iterator is slightly more efficient, but more tricky, since | |
| 410 // calling std::map::erase() on an iterator invalidates it. | |
| 411 for (std::list<FilePath>::const_iterator it = mount_points_to_erase.begin(); | |
| 412 it != mount_points_to_erase.end(); | |
| 413 ++it) { | |
| 414 mount_info_map_.erase(*it); | |
| 415 } | |
| 416 | |
| 417 // For any multiply mounted device where the mount that we had notified | |
| 418 // got detached, send a notification of attachment for one of the other | |
| 419 // mount points. | |
| 420 for (std::list<FilePath>::const_iterator it = | |
| 421 multiple_mounted_devices_needing_reattachment.begin(); | |
| 422 it != multiple_mounted_devices_needing_reattachment.end(); | |
| 423 ++it) { | |
| 424 ReferencedMountPoint::iterator first_mount_point_info = | |
| 425 mount_priority_map_.find(*it)->second.begin(); | |
| 426 const FilePath& mount_point = first_mount_point_info->first; | |
| 427 first_mount_point_info->second = true; | |
| 428 | |
| 429 const MountPointInfo& mount_info = | |
| 430 mount_info_map_.find(mount_point)->second; | |
| 431 DCHECK(MediaStorageUtil::IsRemovableDevice(mount_info.device_id)); | |
| 432 base::SystemMonitor::Get()->ProcessRemovableStorageAttached( | |
| 433 mount_info.device_id, mount_info.device_name, mount_point.value()); | |
| 434 } | |
| 435 | |
| 436 // Check new mtab entries against existing ones. | |
| 437 for (MountPointDeviceMap::iterator new_iter = new_mtab.begin(); | |
| 438 new_iter != new_mtab.end(); ++new_iter) { | |
| 439 const FilePath& mount_point = new_iter->first; | |
| 440 const FilePath& mount_device = new_iter->second; | |
| 441 MountMap::iterator old_iter = mount_info_map_.find(mount_point); | |
| 442 if (old_iter == mount_info_map_.end() || | |
| 443 old_iter->second.mount_device != mount_device) { | |
| 444 // New mount point found or an existing mount point found with a new | |
| 445 // device. | |
| 446 AddNewMount(mount_device, mount_point); | |
| 447 } | |
| 448 } | |
| 449 } | |
| 450 | |
| 451 void RemovableDeviceNotificationsLinux::AddNewMount( | |
| 452 const FilePath& mount_device, const FilePath& mount_point) { | |
| 453 MountPriorityMap::iterator priority = | |
| 454 mount_priority_map_.find(mount_device); | |
| 455 if (priority != mount_priority_map_.end()) { | |
| 456 const FilePath& other_mount_point = priority->second.begin()->first; | |
| 457 priority->second[mount_point] = false; | |
| 458 mount_info_map_[mount_point] = | |
| 459 mount_info_map_.find(other_mount_point)->second; | |
| 460 return; | |
| 461 } | |
| 462 | |
| 463 std::string unique_id; | |
| 464 string16 name; | |
| 465 bool removable; | |
| 466 bool result = get_device_info_func_(mount_device, &unique_id, &name, | |
| 467 &removable); | |
| 468 | |
| 469 // Keep track of GetDeviceInfo result, to see how often we fail to get device | |
| 470 // details. | |
| 471 UMA_HISTOGRAM_BOOLEAN("MediaDeviceNotification.device_info_available", | |
| 472 result); | |
| 473 if (!result) | |
| 474 return; | |
| 475 | |
| 476 // Keep track of device uuid, to see how often we receive empty values. | |
| 477 UMA_HISTOGRAM_BOOLEAN("MediaDeviceNotification.device_uuid_available", | |
| 478 !unique_id.empty()); | |
| 479 UMA_HISTOGRAM_BOOLEAN("MediaDeviceNotification.device_name_available", | |
| 480 !name.empty()); | |
| 481 | |
| 482 if (unique_id.empty() || name.empty()) | |
| 483 return; | |
| 484 | |
| 485 bool has_dcim = IsMediaDevice(mount_point.value()); | |
| 486 MediaStorageUtil::Type type; | |
| 487 if (removable) { | |
| 488 if (has_dcim) { | |
| 489 type = MediaStorageUtil::REMOVABLE_MASS_STORAGE_WITH_DCIM; | |
| 490 } else { | |
| 491 type = MediaStorageUtil::REMOVABLE_MASS_STORAGE_NO_DCIM; | |
| 492 } | |
| 493 } else { | |
| 494 type = MediaStorageUtil::FIXED_MASS_STORAGE; | |
| 495 } | |
| 496 std::string device_id = MediaStorageUtil::MakeDeviceId(type, unique_id); | |
| 497 | |
| 498 MountPointInfo mount_point_info; | |
| 499 mount_point_info.mount_device = mount_device; | |
| 500 mount_point_info.device_id = device_id; | |
| 501 mount_point_info.device_name = name; | |
| 502 | |
| 503 mount_info_map_[mount_point] = mount_point_info; | |
| 504 mount_priority_map_[mount_device][mount_point] = removable; | |
| 505 | |
| 506 if (removable) { | |
| 507 SystemMonitor::Get()->ProcessRemovableStorageAttached(device_id, name, | |
| 508 mount_point.value()); | |
| 509 } | |
| 510 } | |
| 511 | |
| 512 } // namespace chrome | |
| OLD | NEW |