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 #import <Foundation/Foundation.h> 6 #import <ImageCaptureCore/ImageCaptureCore.h> 7 8 #include "base/file_util.h" 9 #include "base/files/file.h" 10 #include "base/files/scoped_temp_dir.h" 11 #include "base/mac/cocoa_protocols.h" 12 #include "base/mac/foundation_util.h" 13 #include "base/mac/scoped_nsobject.h" 14 #include "base/mac/sdk_forward_declarations.h" 15 #include "base/message_loop/message_loop.h" 16 #include "base/run_loop.h" 17 #include "base/strings/sys_string_conversions.h" 18 #include "base/synchronization/waitable_event.h" 19 #include "base/test/sequenced_worker_pool_owner.h" 20 #include "base/threading/sequenced_worker_pool.h" 21 #include "chrome/browser/media_galleries/mac/mtp_device_delegate_impl_mac.h" 22 #include "components/storage_monitor/image_capture_device_manager.h" 23 #include "components/storage_monitor/test_storage_monitor.h" 24 #include "content/public/browser/browser_thread.h" 25 #include "content/public/test/test_browser_thread.h" 26 #include "testing/gtest/include/gtest/gtest.h" 27 28 namespace { 29 30 const char kDeviceId[] = "id"; 31 const char kDevicePath[] = "/ic:id"; 32 const char kTestFileContents[] = "test"; 33 34 } // namespace 35 36 @interface MockMTPICCameraDevice : ICCameraDevice { 37 @private 38 base::scoped_nsobject<NSMutableArray> allMediaFiles_; 39 } 40 41 - (void)addMediaFile:(ICCameraFile*)file; 42 43 @end 44 45 @implementation MockMTPICCameraDevice 46 47 - (NSString*)mountPoint { 48 return @"mountPoint"; 49 } 50 51 - (NSString*)name { 52 return @"name"; 53 } 54 55 - (NSString*)UUIDString { 56 return base::SysUTF8ToNSString(kDeviceId); 57 } 58 59 - (ICDeviceType)type { 60 return ICDeviceTypeCamera; 61 } 62 63 - (void)requestOpenSession { 64 } 65 66 - (void)requestCloseSession { 67 } 68 69 - (NSArray*)mediaFiles { 70 return allMediaFiles_; 71 } 72 73 - (void)addMediaFile:(ICCameraFile*)file { 74 if (!allMediaFiles_.get()) 75 allMediaFiles_.reset([[NSMutableArray alloc] init]); 76 [allMediaFiles_ addObject:file]; 77 } 78 79 - (void)requestDownloadFile:(ICCameraFile*)file 80 options:(NSDictionary*)options 81 downloadDelegate:(id<ICCameraDeviceDownloadDelegate>)downloadDelegate 82 didDownloadSelector:(SEL)selector 83 contextInfo:(void*)contextInfo { 84 base::FilePath saveDir(base::SysNSStringToUTF8( 85 [[options objectForKey:ICDownloadsDirectoryURL] path])); 86 std::string saveAsFilename = 87 base::SysNSStringToUTF8([options objectForKey:ICSaveAsFilename]); 88 // It appears that the ImageCapture library adds an extension to the requested 89 // filename. Do that here to require a rename. 90 saveAsFilename += ".jpg"; 91 base::FilePath toBeSaved = saveDir.Append(saveAsFilename); 92 ASSERT_EQ(static_cast<int>(strlen(kTestFileContents)), 93 base::WriteFile(toBeSaved, kTestFileContents, 94 strlen(kTestFileContents))); 95 96 NSMutableDictionary* returnOptions = 97 [NSMutableDictionary dictionaryWithDictionary:options]; 98 [returnOptions setObject:base::SysUTF8ToNSString(saveAsFilename) 99 forKey:ICSavedFilename]; 100 101 [static_cast<NSObject<ICCameraDeviceDownloadDelegate>*>(downloadDelegate) 102 didDownloadFile:file 103 error:nil 104 options:returnOptions 105 contextInfo:contextInfo]; 106 } 107 108 @end 109 110 @interface MockMTPICCameraFile : ICCameraFile { 111 @private 112 base::scoped_nsobject<NSString> name_; 113 base::scoped_nsobject<NSDate> date_; 114 } 115 116 - (id)init:(NSString*)name; 117 118 @end 119 120 @implementation MockMTPICCameraFile 121 122 - (id)init:(NSString*)name { 123 if ((self = [super init])) { 124 name_.reset([name retain]); 125 date_.reset([[NSDate dateWithNaturalLanguageString:@"12/12/12"] retain]); 126 } 127 return self; 128 } 129 130 - (NSString*)name { 131 return name_.get(); 132 } 133 134 - (NSString*)UTI { 135 return base::mac::CFToNSCast(kUTTypeImage); 136 } 137 138 - (NSDate*)modificationDate { 139 return date_.get(); 140 } 141 142 - (NSDate*)creationDate { 143 return date_.get(); 144 } 145 146 - (off_t)fileSize { 147 return 1000; 148 } 149 150 @end 151 152 class MTPDeviceDelegateImplMacTest : public testing::Test { 153 public: 154 MTPDeviceDelegateImplMacTest() : camera_(NULL), delegate_(NULL) {} 155 156 virtual void SetUp() OVERRIDE { 157 ui_thread_.reset(new content::TestBrowserThread( 158 content::BrowserThread::UI, &message_loop_)); 159 file_thread_.reset(new content::TestBrowserThread( 160 content::BrowserThread::FILE, &message_loop_)); 161 io_thread_.reset(new content::TestBrowserThread( 162 content::BrowserThread::IO)); 163 ASSERT_TRUE(io_thread_->Start()); 164 165 storage_monitor::TestStorageMonitor* monitor = 166 storage_monitor::TestStorageMonitor::CreateAndInstall(); 167 manager_.SetNotifications(monitor->receiver()); 168 169 camera_ = [MockMTPICCameraDevice alloc]; 170 id<ICDeviceBrowserDelegate> delegate = manager_.device_browser(); 171 [delegate deviceBrowser:nil didAddDevice:camera_ moreComing:NO]; 172 173 delegate_ = new MTPDeviceDelegateImplMac(kDeviceId, kDevicePath); 174 } 175 176 virtual void TearDown() OVERRIDE { 177 id<ICDeviceBrowserDelegate> delegate = manager_.device_browser(); 178 [delegate deviceBrowser:nil didRemoveDevice:camera_ moreGoing:NO]; 179 180 delegate_->CancelPendingTasksAndDeleteDelegate(); 181 182 storage_monitor::TestStorageMonitor::Destroy(); 183 184 io_thread_->Stop(); 185 } 186 187 void OnError(base::WaitableEvent* event, base::File::Error error) { 188 error_ = error; 189 event->Signal(); 190 } 191 192 void OverlappedOnError(base::WaitableEvent* event, 193 base::File::Error error) { 194 overlapped_error_ = error; 195 event->Signal(); 196 } 197 198 void OnFileInfo(base::WaitableEvent* event, 199 const base::File::Info& info) { 200 error_ = base::File::FILE_OK; 201 info_ = info; 202 event->Signal(); 203 } 204 205 void OnReadDir(base::WaitableEvent* event, 206 const fileapi::AsyncFileUtil::EntryList& files, 207 bool has_more) { 208 error_ = base::File::FILE_OK; 209 ASSERT_FALSE(has_more); 210 file_list_ = files; 211 event->Signal(); 212 } 213 214 void OverlappedOnReadDir(base::WaitableEvent* event, 215 const fileapi::AsyncFileUtil::EntryList& files, 216 bool has_more) { 217 overlapped_error_ = base::File::FILE_OK; 218 ASSERT_FALSE(has_more); 219 overlapped_file_list_ = files; 220 event->Signal(); 221 } 222 223 void OnDownload(base::WaitableEvent* event, 224 const base::File::Info& file_info, 225 const base::FilePath& local_path) { 226 error_ = base::File::FILE_OK; 227 event->Signal(); 228 } 229 230 base::File::Error GetFileInfo(const base::FilePath& path, 231 base::File::Info* info) { 232 base::WaitableEvent wait(true, false); 233 delegate_->GetFileInfo( 234 path, 235 base::Bind(&MTPDeviceDelegateImplMacTest::OnFileInfo, 236 base::Unretained(this), 237 &wait), 238 base::Bind(&MTPDeviceDelegateImplMacTest::OnError, 239 base::Unretained(this), 240 &wait)); 241 base::RunLoop loop; 242 loop.RunUntilIdle(); 243 EXPECT_TRUE(wait.IsSignaled()); 244 *info = info_; 245 return error_; 246 } 247 248 base::File::Error ReadDir(const base::FilePath& path) { 249 base::WaitableEvent wait(true, false); 250 delegate_->ReadDirectory( 251 path, 252 base::Bind(&MTPDeviceDelegateImplMacTest::OnReadDir, 253 base::Unretained(this), 254 &wait), 255 base::Bind(&MTPDeviceDelegateImplMacTest::OnError, 256 base::Unretained(this), 257 &wait)); 258 base::RunLoop loop; 259 loop.RunUntilIdle(); 260 wait.Wait(); 261 return error_; 262 } 263 264 base::File::Error DownloadFile( 265 const base::FilePath& path, 266 const base::FilePath& local_path) { 267 base::WaitableEvent wait(true, false); 268 delegate_->CreateSnapshotFile( 269 path, local_path, 270 base::Bind(&MTPDeviceDelegateImplMacTest::OnDownload, 271 base::Unretained(this), 272 &wait), 273 base::Bind(&MTPDeviceDelegateImplMacTest::OnError, 274 base::Unretained(this), 275 &wait)); 276 base::RunLoop loop; 277 loop.RunUntilIdle(); 278 wait.Wait(); 279 return error_; 280 } 281 282 protected: 283 base::MessageLoopForUI message_loop_; 284 // Note: threads must be made in this order: UI > FILE > IO 285 scoped_ptr<content::TestBrowserThread> ui_thread_; 286 scoped_ptr<content::TestBrowserThread> file_thread_; 287 scoped_ptr<content::TestBrowserThread> io_thread_; 288 base::ScopedTempDir temp_dir_; 289 storage_monitor::ImageCaptureDeviceManager manager_; 290 MockMTPICCameraDevice* camera_; 291 292 // This object needs special deletion inside the above |task_runner_|. 293 MTPDeviceDelegateImplMac* delegate_; 294 295 base::File::Error error_; 296 base::File::Info info_; 297 fileapi::AsyncFileUtil::EntryList file_list_; 298 299 base::File::Error overlapped_error_; 300 fileapi::AsyncFileUtil::EntryList overlapped_file_list_; 301 302 private: 303 DISALLOW_COPY_AND_ASSIGN(MTPDeviceDelegateImplMacTest); 304 }; 305 306 TEST_F(MTPDeviceDelegateImplMacTest, TestGetRootFileInfo) { 307 base::File::Info info; 308 // Making a fresh delegate should have a single file entry for the synthetic 309 // root directory, with the name equal to the device id string. 310 EXPECT_EQ(base::File::FILE_OK, 311 GetFileInfo(base::FilePath(kDevicePath), &info)); 312 EXPECT_TRUE(info.is_directory); 313 EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, 314 GetFileInfo(base::FilePath("/nonexistent"), &info)); 315 316 // Signal the delegate that no files are coming. 317 delegate_->NoMoreItems(); 318 319 EXPECT_EQ(base::File::FILE_OK, ReadDir(base::FilePath(kDevicePath))); 320 EXPECT_EQ(0U, file_list_.size()); 321 } 322 323 TEST_F(MTPDeviceDelegateImplMacTest, TestOverlappedReadDir) { 324 base::Time time1 = base::Time::Now(); 325 base::File::Info info1; 326 info1.size = 1; 327 info1.is_directory = false; 328 info1.is_symbolic_link = false; 329 info1.last_modified = time1; 330 info1.last_accessed = time1; 331 info1.creation_time = time1; 332 delegate_->ItemAdded("name1", info1); 333 334 base::WaitableEvent wait(true, false); 335 336 delegate_->ReadDirectory( 337 base::FilePath(kDevicePath), 338 base::Bind(&MTPDeviceDelegateImplMacTest::OnReadDir, 339 base::Unretained(this), 340 &wait), 341 base::Bind(&MTPDeviceDelegateImplMacTest::OnError, 342 base::Unretained(this), 343 &wait)); 344 345 delegate_->ReadDirectory( 346 base::FilePath(kDevicePath), 347 base::Bind(&MTPDeviceDelegateImplMacTest::OverlappedOnReadDir, 348 base::Unretained(this), 349 &wait), 350 base::Bind(&MTPDeviceDelegateImplMacTest::OverlappedOnError, 351 base::Unretained(this), 352 &wait)); 353 354 355 // Signal the delegate that no files are coming. 356 delegate_->NoMoreItems(); 357 358 base::RunLoop loop; 359 loop.RunUntilIdle(); 360 wait.Wait(); 361 362 EXPECT_EQ(base::File::FILE_OK, error_); 363 EXPECT_EQ(1U, file_list_.size()); 364 EXPECT_EQ(base::File::FILE_OK, overlapped_error_); 365 EXPECT_EQ(1U, overlapped_file_list_.size()); 366 } 367 368 TEST_F(MTPDeviceDelegateImplMacTest, TestGetFileInfo) { 369 base::Time time1 = base::Time::Now(); 370 base::File::Info info1; 371 info1.size = 1; 372 info1.is_directory = false; 373 info1.is_symbolic_link = false; 374 info1.last_modified = time1; 375 info1.last_accessed = time1; 376 info1.creation_time = time1; 377 delegate_->ItemAdded("name1", info1); 378 379 base::File::Info info; 380 EXPECT_EQ(base::File::FILE_OK, 381 GetFileInfo(base::FilePath("/ic:id/name1"), &info)); 382 EXPECT_EQ(info1.size, info.size); 383 EXPECT_EQ(info1.is_directory, info.is_directory); 384 EXPECT_EQ(info1.last_modified, info.last_modified); 385 EXPECT_EQ(info1.last_accessed, info.last_accessed); 386 EXPECT_EQ(info1.creation_time, info.creation_time); 387 388 info1.size = 2; 389 delegate_->ItemAdded("name2", info1); 390 delegate_->NoMoreItems(); 391 392 EXPECT_EQ(base::File::FILE_OK, 393 GetFileInfo(base::FilePath("/ic:id/name2"), &info)); 394 EXPECT_EQ(info1.size, info.size); 395 396 EXPECT_EQ(base::File::FILE_OK, ReadDir(base::FilePath(kDevicePath))); 397 398 ASSERT_EQ(2U, file_list_.size()); 399 EXPECT_EQ(time1, file_list_[0].last_modified_time); 400 EXPECT_FALSE(file_list_[0].is_directory); 401 EXPECT_EQ("name1", file_list_[0].name); 402 403 EXPECT_EQ(time1, file_list_[1].last_modified_time); 404 EXPECT_FALSE(file_list_[1].is_directory); 405 EXPECT_EQ("name2", file_list_[1].name); 406 } 407 408 TEST_F(MTPDeviceDelegateImplMacTest, TestDirectoriesAndSorting) { 409 base::Time time1 = base::Time::Now(); 410 base::File::Info info1; 411 info1.size = 1; 412 info1.is_directory = false; 413 info1.is_symbolic_link = false; 414 info1.last_modified = time1; 415 info1.last_accessed = time1; 416 info1.creation_time = time1; 417 delegate_->ItemAdded("name2", info1); 418 419 info1.is_directory = true; 420 delegate_->ItemAdded("dir2", info1); 421 delegate_->ItemAdded("dir1", info1); 422 423 info1.is_directory = false; 424 delegate_->ItemAdded("name1", info1); 425 delegate_->NoMoreItems(); 426 427 EXPECT_EQ(base::File::FILE_OK, ReadDir(base::FilePath(kDevicePath))); 428 429 ASSERT_EQ(4U, file_list_.size()); 430 EXPECT_EQ("dir1", file_list_[0].name); 431 EXPECT_EQ("dir2", file_list_[1].name); 432 EXPECT_EQ(time1, file_list_[2].last_modified_time); 433 EXPECT_FALSE(file_list_[2].is_directory); 434 EXPECT_EQ("name1", file_list_[2].name); 435 436 EXPECT_EQ(time1, file_list_[3].last_modified_time); 437 EXPECT_FALSE(file_list_[3].is_directory); 438 EXPECT_EQ("name2", file_list_[3].name); 439 } 440 441 TEST_F(MTPDeviceDelegateImplMacTest, SubDirectories) { 442 base::Time time1 = base::Time::Now(); 443 base::File::Info info1; 444 info1.size = 0; 445 info1.is_directory = true; 446 info1.is_symbolic_link = false; 447 info1.last_modified = time1; 448 info1.last_accessed = time1; 449 info1.creation_time = time1; 450 delegate_->ItemAdded("dir1", info1); 451 452 info1.size = 1; 453 info1.is_directory = false; 454 info1.is_symbolic_link = false; 455 info1.last_modified = time1; 456 info1.last_accessed = time1; 457 info1.creation_time = time1; 458 delegate_->ItemAdded("dir1/name1", info1); 459 460 info1.is_directory = true; 461 info1.size = 0; 462 delegate_->ItemAdded("dir2", info1); 463 464 info1.is_directory = false; 465 info1.size = 1; 466 delegate_->ItemAdded("dir2/name2", info1); 467 468 info1.is_directory = true; 469 info1.size = 0; 470 delegate_->ItemAdded("dir2/subdir", info1); 471 472 info1.is_directory = false; 473 info1.size = 1; 474 delegate_->ItemAdded("dir2/subdir/name3", info1); 475 delegate_->ItemAdded("name4", info1); 476 477 delegate_->NoMoreItems(); 478 479 EXPECT_EQ(base::File::FILE_OK, ReadDir(base::FilePath(kDevicePath))); 480 ASSERT_EQ(3U, file_list_.size()); 481 EXPECT_TRUE(file_list_[0].is_directory); 482 EXPECT_EQ("dir1", file_list_[0].name); 483 EXPECT_TRUE(file_list_[1].is_directory); 484 EXPECT_EQ("dir2", file_list_[1].name); 485 EXPECT_FALSE(file_list_[2].is_directory); 486 EXPECT_EQ("name4", file_list_[2].name); 487 488 EXPECT_EQ(base::File::FILE_OK, 489 ReadDir(base::FilePath(kDevicePath).Append("dir1"))); 490 ASSERT_EQ(1U, file_list_.size()); 491 EXPECT_FALSE(file_list_[0].is_directory); 492 EXPECT_EQ("name1", file_list_[0].name); 493 494 EXPECT_EQ(base::File::FILE_OK, 495 ReadDir(base::FilePath(kDevicePath).Append("dir2"))); 496 ASSERT_EQ(2U, file_list_.size()); 497 EXPECT_FALSE(file_list_[0].is_directory); 498 EXPECT_EQ("name2", file_list_[0].name); 499 EXPECT_TRUE(file_list_[1].is_directory); 500 EXPECT_EQ("subdir", file_list_[1].name); 501 502 EXPECT_EQ(base::File::FILE_OK, 503 ReadDir(base::FilePath(kDevicePath) 504 .Append("dir2").Append("subdir"))); 505 ASSERT_EQ(1U, file_list_.size()); 506 EXPECT_FALSE(file_list_[0].is_directory); 507 EXPECT_EQ("name3", file_list_[0].name); 508 509 EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, 510 ReadDir(base::FilePath(kDevicePath) 511 .Append("dir2").Append("subdir").Append("subdir"))); 512 EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, 513 ReadDir(base::FilePath(kDevicePath) 514 .Append("dir3").Append("subdir"))); 515 EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, 516 ReadDir(base::FilePath(kDevicePath).Append("dir3"))); 517 } 518 519 TEST_F(MTPDeviceDelegateImplMacTest, TestDownload) { 520 ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); 521 base::Time t1 = base::Time::Now(); 522 base::File::Info info; 523 info.size = 4; 524 info.is_directory = false; 525 info.is_symbolic_link = false; 526 info.last_modified = t1; 527 info.last_accessed = t1; 528 info.creation_time = t1; 529 std::string kTestFileName("filename"); 530 base::scoped_nsobject<MockMTPICCameraFile> picture1( 531 [[MockMTPICCameraFile alloc] 532 init:base::SysUTF8ToNSString(kTestFileName)]); 533 [camera_ addMediaFile:picture1]; 534 delegate_->ItemAdded(kTestFileName, info); 535 delegate_->NoMoreItems(); 536 537 EXPECT_EQ(base::File::FILE_OK, ReadDir(base::FilePath(kDevicePath))); 538 ASSERT_EQ(1U, file_list_.size()); 539 ASSERT_EQ("filename", file_list_[0].name); 540 541 EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, 542 DownloadFile(base::FilePath("/ic:id/nonexist"), 543 temp_dir_.path().Append("target"))); 544 545 EXPECT_EQ(base::File::FILE_OK, 546 DownloadFile(base::FilePath("/ic:id/filename"), 547 temp_dir_.path().Append("target"))); 548 std::string contents; 549 EXPECT_TRUE(base::ReadFileToString(temp_dir_.path().Append("target"), 550 &contents)); 551 EXPECT_EQ(kTestFileContents, contents); 552 } 553