1 // Copyright 2014 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 #import <Foundation/Foundation.h> 6 #import <ImageCaptureCore/ImageCaptureCore.h> 7 8 #include "base/file_util.h" 9 #include "base/files/file_path.h" 10 #include "base/files/scoped_temp_dir.h" 11 #include "base/mac/foundation_util.h" 12 #include "base/mac/sdk_forward_declarations.h" 13 #include "base/memory/weak_ptr.h" 14 #include "base/run_loop.h" 15 #include "components/storage_monitor/image_capture_device.h" 16 #include "components/storage_monitor/image_capture_device_manager.h" 17 #include "components/storage_monitor/test_storage_monitor.h" 18 #include "content/public/browser/browser_thread.h" 19 #include "content/public/test/test_browser_thread_bundle.h" 20 #include "testing/gtest/include/gtest/gtest.h" 21 22 namespace { 23 24 const char kDeviceId[] = "id"; 25 const char kTestFileContents[] = "test"; 26 27 } // namespace 28 29 // Private ICCameraDevice method needed to properly initialize the object. 30 @interface NSObject (PrivateAPIICCameraDevice) 31 - (id)initWithDictionary:(id)properties; 32 @end 33 34 @interface MockICCameraDevice : ICCameraDevice { 35 @private 36 base::scoped_nsobject<NSMutableArray> allMediaFiles_; 37 } 38 39 - (void)addMediaFile:(ICCameraFile*)file; 40 41 @end 42 43 @implementation MockICCameraDevice 44 45 - (id)init { 46 if ((self = [super initWithDictionary:[NSDictionary dictionary]])) { 47 } 48 return self; 49 } 50 51 - (NSString*)mountPoint { 52 return @"mountPoint"; 53 } 54 55 - (NSString*)name { 56 return @"name"; 57 } 58 59 - (NSString*)UUIDString { 60 return base::SysUTF8ToNSString(kDeviceId); 61 } 62 63 - (ICDeviceType)type { 64 return ICDeviceTypeCamera; 65 } 66 67 - (void)requestOpenSession { 68 } 69 70 - (void)requestCloseSession { 71 } 72 73 - (NSArray*)mediaFiles { 74 return allMediaFiles_; 75 } 76 77 - (void)addMediaFile:(ICCameraFile*)file { 78 if (!allMediaFiles_.get()) 79 allMediaFiles_.reset([[NSMutableArray alloc] init]); 80 [allMediaFiles_ addObject:file]; 81 } 82 83 // This method does approximately what the internal ImageCapture platform 84 // library is observed to do: take the download save-as filename and mangle 85 // it to attach an extension, then return that new filename to the caller 86 // in the options. 87 - (void)requestDownloadFile:(ICCameraFile*)file 88 options:(NSDictionary*)options 89 downloadDelegate:(id<ICCameraDeviceDownloadDelegate>)downloadDelegate 90 didDownloadSelector:(SEL)selector 91 contextInfo:(void*)contextInfo { 92 base::FilePath saveDir(base::SysNSStringToUTF8( 93 [[options objectForKey:ICDownloadsDirectoryURL] path])); 94 std::string saveAsFilename = 95 base::SysNSStringToUTF8([options objectForKey:ICSaveAsFilename]); 96 // It appears that the ImageCapture library adds an extension to the requested 97 // filename. Do that here to require a rename. 98 saveAsFilename += ".jpg"; 99 base::FilePath toBeSaved = saveDir.Append(saveAsFilename); 100 ASSERT_EQ(static_cast<int>(strlen(kTestFileContents)), 101 base::WriteFile(toBeSaved, kTestFileContents, 102 strlen(kTestFileContents))); 103 104 NSMutableDictionary* returnOptions = 105 [NSMutableDictionary dictionaryWithDictionary:options]; 106 [returnOptions setObject:base::SysUTF8ToNSString(saveAsFilename) 107 forKey:ICSavedFilename]; 108 109 [static_cast<NSObject<ICCameraDeviceDownloadDelegate>*>(downloadDelegate) 110 didDownloadFile:file 111 error:nil 112 options:returnOptions 113 contextInfo:contextInfo]; 114 } 115 116 @end 117 118 @interface MockICCameraFolder : ICCameraFolder { 119 @private 120 base::scoped_nsobject<NSString> name_; 121 } 122 123 - (id)initWithName:(NSString*)name; 124 125 @end 126 127 @implementation MockICCameraFolder 128 129 - (id)initWithName:(NSString*)name { 130 if ((self = [super init])) { 131 name_.reset([name retain]); 132 } 133 return self; 134 } 135 136 - (NSString*)name { 137 return name_; 138 } 139 140 - (ICCameraFolder*)parentFolder { 141 return nil; 142 } 143 144 @end 145 146 @interface MockICCameraFile : ICCameraFile { 147 @private 148 base::scoped_nsobject<NSString> name_; 149 base::scoped_nsobject<NSDate> date_; 150 base::scoped_nsobject<MockICCameraFolder> parent_; 151 } 152 153 - (id)init:(NSString*)name; 154 - (void)setParent:(NSString*)parent; 155 156 @end 157 158 @implementation MockICCameraFile 159 160 - (id)init:(NSString*)name { 161 if ((self = [super init])) { 162 name_.reset([name retain]); 163 date_.reset([[NSDate dateWithNaturalLanguageString:@"12/12/12"] retain]); 164 } 165 return self; 166 } 167 168 - (void)setParent:(NSString*)parent { 169 parent_.reset([[MockICCameraFolder alloc] initWithName:parent]); 170 } 171 172 - (ICCameraFolder*)parentFolder { 173 return parent_.get(); 174 } 175 176 - (NSString*)name { 177 return name_; 178 } 179 180 - (NSString*)UTI { 181 return base::mac::CFToNSCast(kUTTypeImage); 182 } 183 184 - (NSDate*)modificationDate { 185 return date_.get(); 186 } 187 188 - (NSDate*)creationDate { 189 return date_.get(); 190 } 191 192 - (off_t)fileSize { 193 return 1000; 194 } 195 196 @end 197 198 namespace storage_monitor { 199 200 class TestCameraListener 201 : public ImageCaptureDeviceListener, 202 public base::SupportsWeakPtr<TestCameraListener> { 203 public: 204 TestCameraListener() 205 : completed_(false), 206 removed_(false), 207 last_error_(base::File::FILE_ERROR_INVALID_URL) {} 208 virtual ~TestCameraListener() {} 209 210 virtual void ItemAdded(const std::string& name, 211 const base::File::Info& info) OVERRIDE { 212 items_.push_back(name); 213 } 214 215 virtual void NoMoreItems() OVERRIDE { 216 completed_ = true; 217 } 218 219 virtual void DownloadedFile(const std::string& name, 220 base::File::Error error) OVERRIDE { 221 EXPECT_TRUE(content::BrowserThread::CurrentlyOn( 222 content::BrowserThread::UI)); 223 downloads_.push_back(name); 224 last_error_ = error; 225 } 226 227 virtual void DeviceRemoved() OVERRIDE { 228 removed_ = true; 229 } 230 231 std::vector<std::string> items() const { return items_; } 232 std::vector<std::string> downloads() const { return downloads_; } 233 bool completed() const { return completed_; } 234 bool removed() const { return removed_; } 235 base::File::Error last_error() const { return last_error_; } 236 237 private: 238 std::vector<std::string> items_; 239 std::vector<std::string> downloads_; 240 bool completed_; 241 bool removed_; 242 base::File::Error last_error_; 243 }; 244 245 class ImageCaptureDeviceManagerTest : public testing::Test { 246 public: 247 virtual void SetUp() OVERRIDE { 248 monitor_ = TestStorageMonitor::CreateAndInstall(); 249 } 250 251 virtual void TearDown() OVERRIDE { 252 TestStorageMonitor::Destroy(); 253 } 254 255 MockICCameraDevice* AttachDevice(ImageCaptureDeviceManager* manager) { 256 // Ownership will be passed to the device browser delegate. 257 base::scoped_nsobject<MockICCameraDevice> device( 258 [[MockICCameraDevice alloc] init]); 259 id<ICDeviceBrowserDelegate> delegate = manager->device_browser(); 260 [delegate deviceBrowser:nil didAddDevice:device moreComing:NO]; 261 return device.autorelease(); 262 } 263 264 void DetachDevice(ImageCaptureDeviceManager* manager, 265 ICCameraDevice* device) { 266 id<ICDeviceBrowserDelegate> delegate = manager->device_browser(); 267 [delegate deviceBrowser:nil didRemoveDevice:device moreGoing:NO]; 268 } 269 270 protected: 271 content::TestBrowserThreadBundle thread_bundle_; 272 TestStorageMonitor* monitor_; 273 TestCameraListener listener_; 274 }; 275 276 TEST_F(ImageCaptureDeviceManagerTest, TestAttachDetach) { 277 ImageCaptureDeviceManager manager; 278 manager.SetNotifications(monitor_->receiver()); 279 ICCameraDevice* device = AttachDevice(&manager); 280 std::vector<StorageInfo> devices = monitor_->GetAllAvailableStorages(); 281 282 ASSERT_EQ(1U, devices.size()); 283 EXPECT_EQ(std::string("ic:") + kDeviceId, devices[0].device_id()); 284 285 DetachDevice(&manager, device); 286 devices = monitor_->GetAllAvailableStorages(); 287 ASSERT_EQ(0U, devices.size()); 288 }; 289 290 TEST_F(ImageCaptureDeviceManagerTest, OpenCamera) { 291 ImageCaptureDeviceManager manager; 292 manager.SetNotifications(monitor_->receiver()); 293 ICCameraDevice* device = AttachDevice(&manager); 294 295 EXPECT_FALSE(ImageCaptureDeviceManager::deviceForUUID( 296 "nonexistent")); 297 298 base::scoped_nsobject<ImageCaptureDevice> camera( 299 [ImageCaptureDeviceManager::deviceForUUID(kDeviceId) retain]); 300 301 [camera setListener:listener_.AsWeakPtr()]; 302 [camera open]; 303 304 base::scoped_nsobject<MockICCameraFile> picture1( 305 [[MockICCameraFile alloc] init:@"pic1"]); 306 [camera cameraDevice:nil didAddItem:picture1]; 307 base::scoped_nsobject<MockICCameraFile> picture2( 308 [[MockICCameraFile alloc] init:@"pic2"]); 309 [camera cameraDevice:nil didAddItem:picture2]; 310 ASSERT_EQ(2U, listener_.items().size()); 311 EXPECT_EQ("pic1", listener_.items()[0]); 312 EXPECT_EQ("pic2", listener_.items()[1]); 313 EXPECT_FALSE(listener_.completed()); 314 315 [camera deviceDidBecomeReadyWithCompleteContentCatalog:nil]; 316 317 ASSERT_EQ(2U, listener_.items().size()); 318 EXPECT_TRUE(listener_.completed()); 319 320 [camera close]; 321 DetachDevice(&manager, device); 322 EXPECT_FALSE(ImageCaptureDeviceManager::deviceForUUID(kDeviceId)); 323 } 324 325 TEST_F(ImageCaptureDeviceManagerTest, RemoveCamera) { 326 ImageCaptureDeviceManager manager; 327 manager.SetNotifications(monitor_->receiver()); 328 ICCameraDevice* device = AttachDevice(&manager); 329 330 base::scoped_nsobject<ImageCaptureDevice> camera( 331 [ImageCaptureDeviceManager::deviceForUUID(kDeviceId) retain]); 332 333 [camera setListener:listener_.AsWeakPtr()]; 334 [camera open]; 335 336 [camera didRemoveDevice:device]; 337 EXPECT_TRUE(listener_.removed()); 338 } 339 340 TEST_F(ImageCaptureDeviceManagerTest, DownloadFile) { 341 ImageCaptureDeviceManager manager; 342 manager.SetNotifications(monitor_->receiver()); 343 MockICCameraDevice* device = AttachDevice(&manager); 344 345 base::scoped_nsobject<ImageCaptureDevice> camera( 346 [ImageCaptureDeviceManager::deviceForUUID(kDeviceId) retain]); 347 348 [camera setListener:listener_.AsWeakPtr()]; 349 [camera open]; 350 351 std::string kTestFileName("pic1"); 352 353 base::scoped_nsobject<MockICCameraFile> picture1( 354 [[MockICCameraFile alloc] init:base::SysUTF8ToNSString(kTestFileName)]); 355 [device addMediaFile:picture1]; 356 [camera cameraDevice:nil didAddItem:picture1]; 357 358 base::ScopedTempDir temp_dir; 359 ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); 360 361 EXPECT_EQ(0U, listener_.downloads().size()); 362 363 // Test that a nonexistent file we ask to be downloaded will 364 // return us a not-found error. 365 base::FilePath temp_file = temp_dir.path().Append("tempfile"); 366 [camera downloadFile:std::string("nonexistent") localPath:temp_file]; 367 base::RunLoop().RunUntilIdle(); 368 ASSERT_EQ(1U, listener_.downloads().size()); 369 EXPECT_EQ("nonexistent", listener_.downloads()[0]); 370 EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, listener_.last_error()); 371 372 // Test that an existing file we ask to be downloaded will end up in 373 // the location we specify. The mock system will copy testing file 374 // contents to a separate filename, mimicking the ImageCaptureCore 375 // library behavior. Our code then renames the file onto the requested 376 // destination. 377 [camera downloadFile:kTestFileName localPath:temp_file]; 378 base::RunLoop().RunUntilIdle(); 379 380 ASSERT_EQ(2U, listener_.downloads().size()); 381 EXPECT_EQ(kTestFileName, listener_.downloads()[1]); 382 ASSERT_EQ(base::File::FILE_OK, listener_.last_error()); 383 char file_contents[5]; 384 ASSERT_EQ(4, base::ReadFile(temp_file, file_contents, 385 strlen(kTestFileContents))); 386 EXPECT_EQ(kTestFileContents, 387 std::string(file_contents, strlen(kTestFileContents))); 388 389 [camera didRemoveDevice:device]; 390 } 391 392 TEST_F(ImageCaptureDeviceManagerTest, TestSubdirectories) { 393 ImageCaptureDeviceManager manager; 394 manager.SetNotifications(monitor_->receiver()); 395 MockICCameraDevice* device = AttachDevice(&manager); 396 397 base::scoped_nsobject<ImageCaptureDevice> camera( 398 [ImageCaptureDeviceManager::deviceForUUID(kDeviceId) retain]); 399 400 [camera setListener:listener_.AsWeakPtr()]; 401 [camera open]; 402 403 std::string kTestFileName("pic1"); 404 base::scoped_nsobject<MockICCameraFile> picture1( 405 [[MockICCameraFile alloc] init:base::SysUTF8ToNSString(kTestFileName)]); 406 [picture1 setParent:base::SysUTF8ToNSString("dir")]; 407 [device addMediaFile:picture1]; 408 [camera cameraDevice:nil didAddItem:picture1]; 409 410 base::ScopedTempDir temp_dir; 411 ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); 412 base::FilePath temp_file = temp_dir.path().Append("tempfile"); 413 414 [camera downloadFile:("dir/" + kTestFileName) localPath:temp_file]; 415 base::RunLoop().RunUntilIdle(); 416 417 char file_contents[5]; 418 ASSERT_EQ(4, base::ReadFile(temp_file, file_contents, 419 strlen(kTestFileContents))); 420 EXPECT_EQ(kTestFileContents, 421 std::string(file_contents, strlen(kTestFileContents))); 422 423 [camera didRemoveDevice:device]; 424 } 425 426 } // namespace storage_monitor 427