Commit 4af98c6b authored by gbillock@chromium.org's avatar gbillock@chromium.org

[Media Galleries] Add an ImageCaptureCore listener for Mac. (part 2)

This listener uses the ImageCapture API to watch for attach and detach
events of PTP devices and other devices which can be read through the
library. It forwards such notifications through the SystemMonitor.

Also provides an API for clients to access such devices directly and
retrieve the media contents from them.

R=thestig@chromium.org,sail@chromium.org
BUG=151681


Review URL: https://chromiumcodereview.appspot.com/11442057

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@175471 0039d316-1c4b-4281-b951-d872f2087c98
parent 893fcc63
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7 MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7
DEFINE_EMPTY_PROTOCOL(NSDraggingDestination) DEFINE_EMPTY_PROTOCOL(NSDraggingDestination)
DEFINE_EMPTY_PROTOCOL(ICCameraDeviceDownloadDelegate)
#endif // MAC_OS_X_VERSION_10_7 #endif // MAC_OS_X_VERSION_10_7
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
#include "base/memory/ref_counted.h" #include "base/memory/ref_counted.h"
namespace chrome { namespace chrome {
class ImageCaptureDeviceManager;
class RemovableDeviceNotificationsMac; class RemovableDeviceNotificationsMac;
} }
...@@ -31,6 +32,8 @@ class ChromeBrowserMainPartsMac : public ChromeBrowserMainPartsPosix { ...@@ -31,6 +32,8 @@ class ChromeBrowserMainPartsMac : public ChromeBrowserMainPartsPosix {
scoped_refptr<chrome::RemovableDeviceNotificationsMac> scoped_refptr<chrome::RemovableDeviceNotificationsMac>
removable_device_notifications_mac_; removable_device_notifications_mac_;
scoped_ptr<chrome::ImageCaptureDeviceManager> image_capture_device_manager_;
DISALLOW_COPY_AND_ASSIGN(ChromeBrowserMainPartsMac); DISALLOW_COPY_AND_ASSIGN(ChromeBrowserMainPartsMac);
}; };
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
#include "chrome/browser/mac/keychain_reauthorize.h" #include "chrome/browser/mac/keychain_reauthorize.h"
#import "chrome/browser/mac/keystone_glue.h" #import "chrome/browser/mac/keystone_glue.h"
#include "chrome/browser/metrics/metrics_service.h" #include "chrome/browser/metrics/metrics_service.h"
#include "chrome/browser/system_monitor/image_capture_device_manager.h"
#include "chrome/browser/system_monitor/removable_device_notifications_mac.h" #include "chrome/browser/system_monitor/removable_device_notifications_mac.h"
#include "chrome/common/chrome_paths.h" #include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h" #include "chrome/common/chrome_switches.h"
...@@ -283,6 +284,7 @@ void ChromeBrowserMainPartsMac::PreMainMessageLoopStart() { ...@@ -283,6 +284,7 @@ void ChromeBrowserMainPartsMac::PreMainMessageLoopStart() {
void ChromeBrowserMainPartsMac::PreProfileInit() { void ChromeBrowserMainPartsMac::PreProfileInit() {
removable_device_notifications_mac_ = removable_device_notifications_mac_ =
new chrome::RemovableDeviceNotificationsMac(); new chrome::RemovableDeviceNotificationsMac();
image_capture_device_manager_.reset(new chrome::ImageCaptureDeviceManager);
ChromeBrowserMainPartsPosix::PreProfileInit(); ChromeBrowserMainPartsPosix::PreProfileInit();
} }
......
...@@ -22,11 +22,6 @@ class DiskInfoMac { ...@@ -22,11 +22,6 @@ class DiskInfoMac {
// dictionary. This function must be called on the file thread. // dictionary. This function must be called on the file thread.
static DiskInfoMac BuildDiskInfoOnFileThread(CFDictionaryRef dict); static DiskInfoMac BuildDiskInfoOnFileThread(CFDictionaryRef dict);
// Construct a disk info object from info from an ImageCature device.
static DiskInfoMac BuildDiskInfoFromICDevice(std::string device_id,
string16 device_name,
FilePath mount_point);
const std::string& bsd_name() const { return bsd_name_; } const std::string& bsd_name() const { return bsd_name_; }
const std::string& device_id() const { return device_id_; } const std::string& device_id() const { return device_id_; }
const std::string& model_name() const { return model_name_; } const std::string& model_name() const { return model_name_; }
......
...@@ -114,17 +114,4 @@ DiskInfoMac DiskInfoMac::BuildDiskInfoOnFileThread(CFDictionaryRef dict) { ...@@ -114,17 +114,4 @@ DiskInfoMac DiskInfoMac::BuildDiskInfoOnFileThread(CFDictionaryRef dict) {
return info; return info;
} }
// TODO(gbillock): Make sure this gets test coverage.
// static
DiskInfoMac DiskInfoMac::BuildDiskInfoFromICDevice(std::string device_id,
string16 device_name,
FilePath mount_point) {
DiskInfoMac info;
info.device_id_ = device_id;
info.device_name_ = device_name;
info.mount_point_ = mount_point;
info.type_ = MediaStorageUtil::MAC_IMAGE_CAPTURE;
return info;
}
} // namesapce chrome } // namesapce chrome
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_SYSTEM_MONITOR_IMAGE_CAPTURE_DEVICE_H_
#define CHROME_BROWSER_SYSTEM_MONITOR_IMAGE_CAPTURE_DEVICE_H_
#import <Foundation/Foundation.h>
#import <ImageCaptureCore/ImageCaptureCore.h>
#include "base/file_path.h"
#include "base/mac/cocoa_protocols.h"
#include "base/mac/foundation_util.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_nsobject.h"
#include "base/memory/weak_ptr.h"
#include "base/platform_file.h"
#include "base/string_util.h"
#include "base/synchronization/lock.h"
#include "base/sys_string_conversions.h"
// Clients use this listener interface to get notifications about
// events happening as a particular ImageCapture device is interacted with.
// Clients drive the interaction through the ImageCaptureDeviceManager
// and the ImageCaptureDevice classes, and get notifications of
// events through this interface.
class ImageCaptureDeviceListener {
public:
virtual ~ImageCaptureDeviceListener() {}
// Get a notification that a particular item has been found on the device.
// These calls will come automatically after a new device is initialized.
virtual void ItemAdded(const std::string& name,
const base::PlatformFileInfo& info) = 0;
// Called when there are no more items to retrieve.
virtual void NoMoreItems() = 0;
// Called upon completion of a file download request. The |path| is the
// requested download file. Note: in NOT_FOUND error case, can be called
// inline with the download request.
virtual void DownloadedFile(const std::string& name,
base::PlatformFileError error) = 0;
// Called to let the client know the device is removed. The client should
// set the ImageCaptureDevice listener to null upon receiving this call.
virtual void DeviceRemoved() = 0;
};
// Interface to a camera device found by ImageCaptureCore. This class manages a
// session to the camera and provides the backing interactions to present the
// media files on it to the filesystem delegate. FilePaths will be artificial,
// like "/$device_id/" + name.
// Note that all interactions with this class must happen on the UI thread.
@interface ImageCaptureDevice
: NSObject<ICCameraDeviceDelegate, ICCameraDeviceDownloadDelegate> {
@private
scoped_nsobject<ICCameraDevice> camera_;
base::WeakPtr<ImageCaptureDeviceListener> listener_;
}
- (id)initWithCameraDevice:(ICCameraDevice*)cameraDevice;
- (void)setListener:(base::WeakPtr<ImageCaptureDeviceListener>)listener;
- (void)open;
- (void)close;
// Download the given |file| to the provided |local_path|. Completion notice
// will be sent to the listener's DownloadedFile method.
- (void)downloadFile:(const std::string&)name
localPath:(const FilePath&)localPath;
@end
#endif // CHROME_BROWSER_SYSTEM_MONITOR_IMAGE_CAPTURE_DEVICE_H_
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/browser/system_monitor/image_capture_device.h"
#include "base/file_util.h"
#include "base/system_monitor/system_monitor.h"
#include "chrome/browser/system_monitor/media_storage_util.h"
#include "content/public/browser/browser_thread.h"
namespace {
void RenameFile(const FilePath& downloaded_filename,
const FilePath& desired_filename,
base::PlatformFileError* result) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
bool success = file_util::ReplaceFile(downloaded_filename, desired_filename);
*result = success ? base::PLATFORM_FILE_OK
: base::PLATFORM_FILE_ERROR_NOT_FOUND;
}
void ReturnRenameResultToListener(
base::WeakPtr<ImageCaptureDeviceListener> listener,
const std::string& name,
base::PlatformFileError* result) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
scoped_ptr<base::PlatformFileError> result_deleter(result);
if (listener)
listener->DownloadedFile(name, *result);
}
base::Time NSDateToBaseTime(NSDate* date) {
return base::Time::FromDoubleT([date timeIntervalSince1970]);
}
} // namespace
@implementation ImageCaptureDevice
- (id)initWithCameraDevice:(ICCameraDevice*)cameraDevice {
if ((self = [super init])) {
camera_.reset([cameraDevice retain]);
[camera_ setDelegate:self];
}
return self;
}
- (void)dealloc {
// Make sure the session was closed and listener set to null
// before destruction.
DCHECK(![camera_ delegate]);
DCHECK(!listener_);
[super dealloc];
}
- (void)setListener:(base::WeakPtr<ImageCaptureDeviceListener>)listener {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
listener_ = listener;
}
- (void)open {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
DCHECK(listener_);
[camera_ requestOpenSession];
}
- (void)close {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
[camera_ requestCloseSession];
[camera_ setDelegate:nil];
listener_.reset();
}
- (void)downloadFile:(const std::string&)name
localPath:(const FilePath&)localPath {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
// Find the file with that name and start download.
for (ICCameraItem* item in [camera_ mediaFiles]) {
std::string itemName = base::SysNSStringToUTF8([item name]);
if (itemName == name) {
// To create save options for ImageCapture, we need to
// split the target filename into directory/name
// and encode the directory as a URL.
NSString* saveDirectory =
base::mac::FilePathToNSString(localPath.DirName());
NSString* saveFilename =
base::mac::FilePathToNSString(localPath.BaseName());
NSMutableDictionary* options =
[NSMutableDictionary dictionaryWithCapacity:3];
[options setObject:[NSURL fileURLWithPath:saveDirectory isDirectory:YES]
forKey:ICDownloadsDirectoryURL];
[options setObject:saveFilename forKey:ICSaveAsFilename];
[options setObject:[NSNumber numberWithBool:YES] forKey:ICOverwrite];
[camera_ requestDownloadFile:base::mac::ObjCCastStrict<ICCameraFile>(item)
options:options
downloadDelegate:self
didDownloadSelector:
@selector(didDownloadFile:error:options:contextInfo:)
contextInfo:NULL];
return;
}
}
if (listener_)
listener_->DownloadedFile(name, base::PLATFORM_FILE_ERROR_NOT_FOUND);
}
- (void)cameraDevice:(ICCameraDevice*)camera didAddItem:(ICCameraItem*)item {
std::string name = base::SysNSStringToUTF8([item name]);
base::PlatformFileInfo info;
if ([[item UTI] isEqualToString:base::mac::CFToNSCast(kUTTypeFolder)])
info.is_directory = true;
else
info.size = [base::mac::ObjCCastStrict<ICCameraFile>(item) fileSize];
info.last_modified = NSDateToBaseTime([item modificationDate]);
info.creation_time = NSDateToBaseTime([item creationDate]);
info.last_accessed = info.last_modified;
if (listener_)
listener_->ItemAdded(name, info);
}
- (void)cameraDevice:(ICCameraDevice*)camera didAddItems:(NSArray*)items {
for (ICCameraItem* item in items)
[self cameraDevice:camera didAddItem:item];
}
- (void)didRemoveDevice:(ICDevice*)device {
device.delegate = NULL;
if (listener_)
listener_->DeviceRemoved();
}
// Notifies that a session was opened with the given device; potentially
// with an error.
- (void)device:(ICDevice*)device didOpenSessionWithError:(NSError*)error {
if (error)
[self didRemoveDevice:camera_];
}
- (void)device:(ICDevice*)device didEncounterError:(NSError*)error {
if (error && listener_)
listener_->DeviceRemoved();
}
// When this message is received, all media metadata is now loaded.
- (void)deviceDidBecomeReadyWithCompleteContentCatalog:(ICDevice*)device {
if (listener_)
listener_->NoMoreItems();
}
- (void)didDownloadFile:(ICCameraFile*)file
error:(NSError*)error
options:(NSDictionary*)options
contextInfo:(void*)contextInfo {
std::string name = base::SysNSStringToUTF8([file name]);
if (error) {
if (listener_)
listener_->DownloadedFile(name, base::PLATFORM_FILE_ERROR_FAILED);
return;
}
std::string savedFilename =
base::SysNSStringToUTF8([options objectForKey:ICSavedFilename]);
std::string saveAsFilename =
base::SysNSStringToUTF8([options objectForKey:ICSaveAsFilename]);
if (savedFilename == saveAsFilename) {
if (listener_)
listener_->DownloadedFile(name, base::PLATFORM_FILE_OK);
return;
}
// ImageCapture did not save the file into the name we gave it in the
// options. It picks a new name according to its best lights, so we need
// to rename the file.
FilePath saveDir(base::SysNSStringToUTF8(
[[options objectForKey:ICDownloadsDirectoryURL] path]));
FilePath saveAsPath = saveDir.Append(saveAsFilename);
FilePath savedPath = saveDir.Append(savedFilename);
// Shared result value from file-copy closure to tell-listener closure.
base::PlatformFileError* copyResult = new base::PlatformFileError();
content::BrowserThread::PostTaskAndReply(
content::BrowserThread::FILE,
FROM_HERE,
base::Bind(&RenameFile, savedPath, saveAsPath, copyResult),
base::Bind(&ReturnRenameResultToListener, listener_, name, copyResult));
}
@end // ImageCaptureDevice
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_SYSTEM_MONITOR_IMAGE_CAPTURE_DEVICE_MANAGER_H_
#define CHROME_BROWSER_SYSTEM_MONITOR_IMAGE_CAPTURE_DEVICE_MANAGER_H_
#import <Foundation/Foundation.h>
#include <string>
#include "base/memory/scoped_nsobject.h"
@protocol ICDeviceBrowserDelegate;
@class ImageCaptureDevice;
@class ImageCaptureDeviceManagerImpl;
namespace chrome {
// Upon creation, begins monitoring for any attached devices using the
// ImageCapture API. Notifies clients of the presence of such devices
// (i.e. cameras, USB cards) using the SystemMonitor and makes them
// available using |deviceForUUID|.
class ImageCaptureDeviceManager {
public:
ImageCaptureDeviceManager();
~ImageCaptureDeviceManager();
// The UUIDs passed here are available in the device attach notifications
// given through SystemMonitor. They're gotten by cracking the device ID
// and taking the unique ID output.
static ImageCaptureDevice* deviceForUUID(const std::string& uuid);
// Returns a weak pointer to the internal ImageCapture interface protocol.
id<ICDeviceBrowserDelegate> device_browser();
private:
scoped_nsobject<ImageCaptureDeviceManagerImpl> device_browser_;
};
} // namespace chrome
#endif // CHROME_BROWSER_SYSTEM_MONITOR_IMAGE_CAPTURE_DEVICE_MANAGER_H_
// Copyright (c) 2012 The Chromium 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 "chrome/browser/system_monitor/image_capture_device_manager.h"
#import <ImageCaptureCore/ImageCaptureCore.h>
#include "base/file_util.h"
#include "base/system_monitor/system_monitor.h"
#include "chrome/browser/system_monitor/disk_info_mac.h"
#import "chrome/browser/system_monitor/image_capture_device.h"
#include "chrome/browser/system_monitor/media_storage_util.h"
#include "content/public/browser/browser_thread.h"
namespace {
chrome::ImageCaptureDeviceManager* g_image_capture_device_manager = NULL;
} // namespace
// This class is the surface for the Mac ICDeviceBrowser ImageCaptureCore API.
// Owned by the ChromeBrowserParts and has browser process lifetime. Upon
// creation, it gets a list of attached media volumes (asynchronously) which
// it will eventually forward to the SystemMonitor as removable storage
// notifications. It will also set up an ImageCaptureCore listener to be
// told when new devices/volumes are discovered and existing ones are removed.
@interface ImageCaptureDeviceManagerImpl
: NSObject<ICDeviceBrowserDelegate> {
@private
scoped_nsobject<ICDeviceBrowser> deviceBrowser_;
scoped_nsobject<NSMutableArray> cameras_;
}
- (void)close;
// The UUIDs passed here are available in the device attach notifications
// given through SystemMonitor. They're gotten by cracking the device ID
// and taking the unique ID output.
- (ImageCaptureDevice*)deviceForUUID:(const std::string&)uuid;
@end
@implementation ImageCaptureDeviceManagerImpl
- (id)init {
if ((self = [super init])) {
cameras_.reset([[NSMutableArray alloc] init]);
deviceBrowser_.reset([[ICDeviceBrowser alloc] init]);
[deviceBrowser_ setDelegate:self];
[deviceBrowser_ setBrowsedDeviceTypeMask:
[deviceBrowser_ browsedDeviceTypeMask] |
ICDeviceTypeMaskCamera | ICDeviceLocationTypeMaskLocal];
[deviceBrowser_ start];
}
return self;
}
- (void)close {
[deviceBrowser_ setDelegate:nil];
[deviceBrowser_ stop];
deviceBrowser_.reset();
cameras_.reset();
}
- (ImageCaptureDevice*) deviceForUUID:(const std::string&)uuid {
for (ICCameraDevice* camera in cameras_.get()) {
NSString* camera_id = [camera UUIDString];
if (base::SysNSStringToUTF8(camera_id) == uuid) {
return [[[ImageCaptureDevice alloc]
initWithCameraDevice:camera] autorelease];
}
}
return nil;
}
- (void)deviceBrowser:(ICDeviceBrowser*)browser
didAddDevice:(ICDevice*)addedDevice
moreComing:(BOOL)moreComing {
if (!(addedDevice.type & ICDeviceTypeCamera))
return;
ICCameraDevice* cameraDevice =
base::mac::ObjCCastStrict<ICCameraDevice>(addedDevice);
[cameras_ addObject:addedDevice];
base::SystemMonitor::Get()->ProcessRemovableStorageAttached(
chrome::MediaStorageUtil::MakeDeviceId(
chrome::MediaStorageUtil::MAC_IMAGE_CAPTURE,
base::SysNSStringToUTF8([cameraDevice UUIDString])),
base::SysNSStringToUTF16([cameraDevice name]),
base::SysNSStringToUTF8([cameraDevice mountPoint]));
}
- (void)deviceBrowser:(ICDeviceBrowser*)browser
didRemoveDevice:(ICDevice*)device
moreGoing:(BOOL)moreGoing {
if (!(device.type & ICDeviceTypeCamera))
return;
std::string uuid = base::SysNSStringToUTF8([device UUIDString]);
// May delete |device|.
[cameras_ removeObject:device];
base::SystemMonitor::Get()->ProcessRemovableStorageDetached(
chrome::MediaStorageUtil::MakeDeviceId(
chrome::MediaStorageUtil::MAC_IMAGE_CAPTURE, uuid));
}
@end // ImageCaptureDeviceManagerImpl
namespace chrome {
ImageCaptureDeviceManager::ImageCaptureDeviceManager() {
device_browser_.reset([[ImageCaptureDeviceManagerImpl alloc] init]);
g_image_capture_device_manager = this;
}
ImageCaptureDeviceManager::~ImageCaptureDeviceManager() {
g_image_capture_device_manager = NULL;
[device_browser_ close];
}
// static
ImageCaptureDevice* ImageCaptureDeviceManager::deviceForUUID(
const std::string& uuid) {
ImageCaptureDeviceManagerImpl* manager =
g_image_capture_device_manager->device_browser_;
return [manager deviceForUUID:uuid];
}
id<ICDeviceBrowserDelegate> ImageCaptureDeviceManager::device_browser() {
return device_browser_.get();
}
} // namespace chrome
...@@ -2044,6 +2044,10 @@ ...@@ -2044,6 +2044,10 @@
'browser/sync_file_system/sync_file_system_service.h', 'browser/sync_file_system/sync_file_system_service.h',
'browser/system_monitor/disk_info_mac.h', 'browser/system_monitor/disk_info_mac.h',
'browser/system_monitor/disk_info_mac.mm', 'browser/system_monitor/disk_info_mac.mm',
'browser/system_monitor/image_capture_device.h',
'browser/system_monitor/image_capture_device.mm',
'browser/system_monitor/image_capture_device_manager.h',
'browser/system_monitor/image_capture_device_manager.mm',
'browser/system_monitor/media_device_notifications_utils.cc', 'browser/system_monitor/media_device_notifications_utils.cc',
'browser/system_monitor/media_device_notifications_utils.h', 'browser/system_monitor/media_device_notifications_utils.h',
'browser/system_monitor/media_storage_util.cc', 'browser/system_monitor/media_storage_util.cc',
...@@ -2830,6 +2834,7 @@ ...@@ -2830,6 +2834,7 @@
'$(SDKROOT)/System/Library/Frameworks/AudioUnit.framework', '$(SDKROOT)/System/Library/Frameworks/AudioUnit.framework',
'$(SDKROOT)/System/Library/Frameworks/DiskArbitration.framework', '$(SDKROOT)/System/Library/Frameworks/DiskArbitration.framework',
'$(SDKROOT)/System/Library/Frameworks/IOKit.framework', '$(SDKROOT)/System/Library/Frameworks/IOKit.framework',
'$(SDKROOT)/System/Library/Frameworks/ImageCaptureCore.framework',
'$(SDKROOT)/System/Library/Frameworks/OpenGL.framework', '$(SDKROOT)/System/Library/Frameworks/OpenGL.framework',
'$(SDKROOT)/System/Library/Frameworks/QuartzCore.framework', '$(SDKROOT)/System/Library/Frameworks/QuartzCore.framework',
'$(SDKROOT)/System/Library/Frameworks/SecurityInterface.framework', '$(SDKROOT)/System/Library/Frameworks/SecurityInterface.framework',
......
...@@ -1140,6 +1140,7 @@ ...@@ -1140,6 +1140,7 @@
'browser/sync_file_system/sync_file_system_service_unittest.cc', 'browser/sync_file_system/sync_file_system_service_unittest.cc',
'browser/sync_file_system/sync_file_system_test_util.cc', 'browser/sync_file_system/sync_file_system_test_util.cc',
'browser/sync_file_system/sync_file_system_test_util.h', 'browser/sync_file_system/sync_file_system_test_util.h',
'browser/system_monitor/image_capture_device_manager_unittest.mm',
'browser/system_monitor/media_device_notifications_utils_unittest.cc', 'browser/system_monitor/media_device_notifications_utils_unittest.cc',
'browser/system_monitor/media_storage_util_unittest.cc', 'browser/system_monitor/media_storage_util_unittest.cc',
'browser/system_monitor/media_transfer_protocol_device_observer_linux_unittest.cc', 'browser/system_monitor/media_transfer_protocol_device_observer_linux_unittest.cc',
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment