1 // Copyright 2014 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/storage_monitor/storage_monitor_mac.h"
11 #include "base/apple/bridging.h"
12 #include "base/apple/foundation_util.h"
13 #include "base/functional/bind.h"
14 #include "base/mac/mac_util.h"
15 #include "base/strings/sys_string_conversions.h"
16 #include "base/strings/utf_string_conversions.h"
17 #include "base/task/task_traits.h"
18 #include "base/task/thread_pool.h"
19 #include "base/threading/scoped_blocking_call.h"
20 #include "components/storage_monitor/image_capture_device_manager.h"
21 #include "components/storage_monitor/media_storage_util.h"
22 #include "components/storage_monitor/storage_info.h"
23 #include "content/public/browser/browser_task_traits.h"
24 #include "content/public/browser/browser_thread.h"
26 namespace storage_monitor {
30 const char16_t kDiskImageModelName[] = u"Disk Image";
32 std::u16string GetUTF16FromDictionary(CFDictionaryRef dictionary,
35 base::apple::GetValueFromDictionary<CFStringRef>(dictionary, key);
37 return std::u16string();
38 return base::SysCFStringRefToUTF16(value);
41 std::u16string JoinName(const std::u16string& name,
42 const std::u16string& addition) {
47 return name + u' ' + addition;
50 StorageInfo::Type GetDeviceType(bool is_removable, bool has_dcim) {
52 return StorageInfo::FIXED_MASS_STORAGE;
54 return StorageInfo::REMOVABLE_MASS_STORAGE_WITH_DCIM;
55 return StorageInfo::REMOVABLE_MASS_STORAGE_NO_DCIM;
58 StorageInfo BuildStorageInfo(
59 CFDictionaryRef dict, std::string* bsd_name) {
60 base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
61 base::BlockingType::MAY_BLOCK);
63 CFStringRef device_bsd_name =
64 base::apple::GetValueFromDictionary<CFStringRef>(
65 dict, kDADiskDescriptionMediaBSDNameKey);
66 if (device_bsd_name && bsd_name)
67 *bsd_name = base::SysCFStringRefToUTF8(device_bsd_name);
69 CFURLRef url = base::apple::GetValueFromDictionary<CFURLRef>(
70 dict, kDADiskDescriptionVolumePathKey);
71 base::FilePath location =
72 base::apple::NSURLToFilePath(base::apple::CFToNSPtrCast(url));
73 CFNumberRef size_number = base::apple::GetValueFromDictionary<CFNumberRef>(
74 dict, kDADiskDescriptionMediaSizeKey);
75 uint64_t size_in_bytes = 0;
77 CFNumberGetValue(size_number, kCFNumberLongLongType, &size_in_bytes);
79 std::u16string vendor =
80 GetUTF16FromDictionary(dict, kDADiskDescriptionDeviceVendorKey);
81 std::u16string model =
82 GetUTF16FromDictionary(dict, kDADiskDescriptionDeviceModelKey);
83 std::u16string label =
84 GetUTF16FromDictionary(dict, kDADiskDescriptionVolumeNameKey);
86 CFUUIDRef uuid = base::apple::GetValueFromDictionary<CFUUIDRef>(
87 dict, kDADiskDescriptionVolumeUUIDKey);
88 std::string unique_id;
90 base::apple::ScopedCFTypeRef<CFStringRef> uuid_string(
91 CFUUIDCreateString(nullptr, uuid));
92 if (uuid_string.get())
93 unique_id = base::SysCFStringRefToUTF8(uuid_string);
95 if (unique_id.empty()) {
96 std::u16string revision =
97 GetUTF16FromDictionary(dict, kDADiskDescriptionDeviceRevisionKey);
98 std::u16string unique_id2 = vendor;
99 unique_id2 = JoinName(unique_id2, model);
100 unique_id2 = JoinName(unique_id2, revision);
101 unique_id = base::UTF16ToUTF8(unique_id2);
104 CFBooleanRef is_removable_ref =
105 base::apple::GetValueFromDictionary<CFBooleanRef>(
106 dict, kDADiskDescriptionMediaRemovableKey);
107 bool is_removable = is_removable_ref && CFBooleanGetValue(is_removable_ref);
108 // Checking for DCIM only matters on removable devices.
109 bool has_dcim = is_removable && MediaStorageUtil::HasDcim(location);
110 StorageInfo::Type device_type = GetDeviceType(is_removable, has_dcim);
111 std::string device_id;
112 if (!unique_id.empty())
113 device_id = StorageInfo::MakeDeviceId(device_type, unique_id);
115 return StorageInfo(device_id, location.value(), label, vendor, model,
119 struct EjectDiskOptions {
120 std::string bsd_name;
121 base::OnceCallback<void(StorageMonitor::EjectStatus)> callback;
122 base::apple::ScopedCFTypeRef<DADiskRef> disk;
125 void PostEjectCallback(DADiskRef disk,
126 DADissenterRef dissenter,
128 std::unique_ptr<EjectDiskOptions> options_deleter(
129 static_cast<EjectDiskOptions*>(context));
131 std::move(options_deleter->callback).Run(StorageMonitor::EJECT_IN_USE);
135 std::move(options_deleter->callback).Run(StorageMonitor::EJECT_OK);
138 void PostUnmountCallback(DADiskRef disk,
139 DADissenterRef dissenter,
141 std::unique_ptr<EjectDiskOptions> options_deleter(
142 static_cast<EjectDiskOptions*>(context));
144 std::move(options_deleter->callback).Run(StorageMonitor::EJECT_IN_USE);
148 DADiskEject(options_deleter->disk.get(), kDADiskEjectOptionDefault,
149 PostEjectCallback, options_deleter.release());
152 void EjectDisk(EjectDiskOptions* options) {
153 DADiskUnmount(options->disk.get(), kDADiskUnmountOptionWhole,
154 PostUnmountCallback, options);
159 StorageMonitorMac::StorageMonitorMac() = default;
161 StorageMonitorMac::~StorageMonitorMac() {
162 if (session_.get()) {
163 DASessionUnscheduleFromRunLoop(
164 session_, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
168 void StorageMonitorMac::Init() {
169 session_.reset(DASessionCreate(nullptr));
171 // Register for callbacks for attached, changed, and removed devices.
172 // This will send notifications for existing devices too.
173 DARegisterDiskAppearedCallback(
175 kDADiskDescriptionMatchVolumeMountable,
176 DiskAppearedCallback,
178 DARegisterDiskDisappearedCallback(
180 kDADiskDescriptionMatchVolumeMountable,
181 DiskDisappearedCallback,
183 DARegisterDiskDescriptionChangedCallback(
185 kDADiskDescriptionMatchVolumeMountable,
186 kDADiskDescriptionWatchVolumePath,
187 DiskDescriptionChangedCallback,
190 DASessionScheduleWithRunLoop(
191 session_, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
193 image_capture_device_manager_ = std::make_unique<ImageCaptureDeviceManager>();
194 image_capture_device_manager_->SetNotifications(receiver());
197 void StorageMonitorMac::UpdateDisk(UpdateType update_type,
198 std::string* bsd_name,
199 const StorageInfo& info) {
200 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
203 pending_disk_updates_--;
204 bool initialization_complete = false;
205 if (!IsInitialized() && pending_disk_updates_ == 0)
206 initialization_complete = true;
208 if (info.device_id().empty() || bsd_name->empty()) {
209 if (initialization_complete)
214 std::map<std::string, StorageInfo>::iterator it =
215 disk_info_map_.find(*bsd_name);
216 if (it != disk_info_map_.end()) {
217 // If an attached notification was previously posted then post a detached
218 // notification now. This is used for devices that are being removed or
219 // devices that have changed.
220 if (ShouldPostNotificationForDisk(it->second)) {
221 receiver()->ProcessDetach(it->second.device_id());
225 if (update_type == UPDATE_DEVICE_REMOVED) {
226 if (it != disk_info_map_.end())
227 disk_info_map_.erase(it);
229 disk_info_map_[*bsd_name] = info;
230 if (ShouldPostNotificationForDisk(info))
231 receiver()->ProcessAttach(info);
234 // We're not really honestly sure we're done, but this looks the best we
235 // can do. Any misses should go out through notifications.
236 if (initialization_complete)
240 bool StorageMonitorMac::GetStorageInfoForPath(const base::FilePath& path,
241 StorageInfo* device_info) const {
244 if (!path.IsAbsolute())
247 base::FilePath current = path;
248 const base::FilePath root(base::FilePath::kSeparators);
249 while (current != root) {
251 if (FindDiskWithMountPoint(current, &info)) {
255 current = current.DirName();
261 void StorageMonitorMac::EjectDevice(
262 const std::string& device_id,
263 base::OnceCallback<void(EjectStatus)> callback) {
264 StorageInfo::Type type;
266 if (!StorageInfo::CrackDeviceId(device_id, &type, &uuid)) {
267 std::move(callback).Run(EJECT_FAILURE);
271 if (type == StorageInfo::MAC_IMAGE_CAPTURE &&
272 image_capture_device_manager_.get()) {
273 image_capture_device_manager_->EjectDevice(uuid, std::move(callback));
277 std::string bsd_name;
278 for (std::map<std::string, StorageInfo>::iterator
279 it = disk_info_map_.begin(); it != disk_info_map_.end(); ++it) {
280 if (it->second.device_id() == device_id) {
281 bsd_name = it->first;
282 disk_info_map_.erase(it);
287 if (bsd_name.empty()) {
288 std::move(callback).Run(EJECT_NO_SUCH_DEVICE);
292 receiver()->ProcessDetach(device_id);
294 base::apple::ScopedCFTypeRef<DADiskRef> disk(
295 DADiskCreateFromBSDName(nullptr, session_, bsd_name.c_str()));
297 std::move(callback).Run(StorageMonitor::EJECT_FAILURE);
300 // Get the reference to the full disk for ejecting.
301 disk.reset(DADiskCopyWholeDisk(disk));
303 std::move(callback).Run(StorageMonitor::EJECT_FAILURE);
307 EjectDiskOptions* options = new EjectDiskOptions;
308 options->bsd_name = bsd_name;
309 options->callback = std::move(callback);
310 options->disk = std::move(disk);
311 content::GetUIThreadTaskRunner({})->PostTask(
312 FROM_HERE, base::BindOnce(EjectDisk, options));
316 void StorageMonitorMac::DiskAppearedCallback(DADiskRef disk, void* context) {
317 StorageMonitorMac* monitor = static_cast<StorageMonitorMac*>(context);
318 monitor->GetDiskInfoAndUpdate(disk, UPDATE_DEVICE_ADDED);
322 void StorageMonitorMac::DiskDisappearedCallback(DADiskRef disk, void* context) {
323 StorageMonitorMac* monitor = static_cast<StorageMonitorMac*>(context);
324 monitor->GetDiskInfoAndUpdate(disk, UPDATE_DEVICE_REMOVED);
328 void StorageMonitorMac::DiskDescriptionChangedCallback(DADiskRef disk,
331 StorageMonitorMac* monitor = static_cast<StorageMonitorMac*>(context);
332 monitor->GetDiskInfoAndUpdate(disk, UPDATE_DEVICE_CHANGED);
335 void StorageMonitorMac::GetDiskInfoAndUpdate(
337 StorageMonitorMac::UpdateType update_type) {
338 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
340 pending_disk_updates_++;
342 base::apple::ScopedCFTypeRef<CFDictionaryRef> dict(
343 DADiskCopyDescription(disk));
344 std::string* bsd_name = new std::string;
345 base::ThreadPool::PostTaskAndReplyWithResult(
346 FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
347 base::BindOnce(&BuildStorageInfo, dict, bsd_name),
348 base::BindOnce(&StorageMonitorMac::UpdateDisk, AsWeakPtr(), update_type,
349 base::Owned(bsd_name)));
353 bool StorageMonitorMac::ShouldPostNotificationForDisk(
354 const StorageInfo& info) const {
355 // Only post notifications about disks that have no empty fields and
356 // are removable. Also exclude disk images (DMGs).
357 return !info.device_id().empty() && !info.location().empty() &&
358 info.model_name() != kDiskImageModelName &&
359 StorageInfo::IsMassStorageDevice(info.device_id());
362 bool StorageMonitorMac::FindDiskWithMountPoint(
363 const base::FilePath& mount_point,
364 StorageInfo* info) const {
365 for (const auto& disk_info : disk_info_map_) {
366 if (disk_info.second.location() == mount_point.value()) {
367 *info = disk_info.second;
374 StorageMonitor* StorageMonitor::CreateInternal() {
375 return new StorageMonitorMac();
378 } // namespace storage_monitor