浏览代码

Add 'smart' directory watcher (closes #8731)

Gleb Pinigin 12 年之前
父节点
当前提交
03998b94a0
共有 4 个文件被更改,包括 380 次插入2 次删除
  1. 73 0
      AspenProject/DirectoryWatcher.h
  2. 221 0
      AspenProject/DirectoryWatcher.m
  3. 80 2
      AspenProject/VLCAppDelegate.m
  4. 6 0
      VLC for iOS.xcodeproj/project.pbxproj

+ 73 - 0
AspenProject/DirectoryWatcher.h

@@ -0,0 +1,73 @@
+/*
+     File: DirectoryWatcher.h 
+ Abstract: 
+ Object used to monitor the contents of a given directory by using
+ "kqueue": a kernel event notification mechanism.
+  
+  Version: 1.5 
+  
+ Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple 
+ Inc. ("Apple") in consideration of your agreement to the following 
+ terms, and your use, installation, modification or redistribution of 
+ this Apple software constitutes acceptance of these terms.  If you do 
+ not agree with these terms, please do not use, install, modify or 
+ redistribute this Apple software. 
+  
+ In consideration of your agreement to abide by the following terms, and 
+ subject to these terms, Apple grants you a personal, non-exclusive 
+ license, under Apple's copyrights in this original Apple software (the 
+ "Apple Software"), to use, reproduce, modify and redistribute the Apple 
+ Software, with or without modifications, in source and/or binary forms; 
+ provided that if you redistribute the Apple Software in its entirety and 
+ without modifications, you must retain this notice and the following 
+ text and disclaimers in all such redistributions of the Apple Software. 
+ Neither the name, trademarks, service marks or logos of Apple Inc. may 
+ be used to endorse or promote products derived from the Apple Software 
+ without specific prior written permission from Apple.  Except as 
+ expressly stated in this notice, no other rights or licenses, express or 
+ implied, are granted by Apple herein, including but not limited to any 
+ patent rights that may be infringed by your derivative works or by other 
+ works in which the Apple Software may be incorporated. 
+  
+ The Apple Software is provided by Apple on an "AS IS" basis.  APPLE 
+ MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 
+ THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS 
+ FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND 
+ OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 
+  
+ IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL 
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
+ INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, 
+ MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED 
+ AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), 
+ STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE 
+ POSSIBILITY OF SUCH DAMAGE. 
+  
+ Copyright (C) 2013 Apple Inc. All Rights Reserved. 
+  
+ */
+
+#import <Foundation/Foundation.h>
+
+@class DirectoryWatcher;
+
+@protocol DirectoryWatcherDelegate <NSObject>
+@required
+- (void)directoryDidChange:(DirectoryWatcher *)folderWatcher;
+@end
+
+@interface DirectoryWatcher : NSObject 
+{
+	id <DirectoryWatcherDelegate> __weak delegate;
+    
+	int dirFD;
+    int kq;
+
+	CFFileDescriptorRef dirKQRef;
+}
+@property (nonatomic, weak) id <DirectoryWatcherDelegate> delegate;
+
++ (DirectoryWatcher *)watchFolderWithPath:(NSString *)watchPath delegate:(id<DirectoryWatcherDelegate>)watchDelegate;
+- (void)invalidate;
+@end

+ 221 - 0
AspenProject/DirectoryWatcher.m

@@ -0,0 +1,221 @@
+/*
+     File: DirectoryWatcher.m 
+ Abstract: 
+ Object used to monitor the contents of a given directory by using
+ "kqueue": a kernel event notification mechanism.
+  
+  Version: 1.5 
+  
+ Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple 
+ Inc. ("Apple") in consideration of your agreement to the following 
+ terms, and your use, installation, modification or redistribution of 
+ this Apple software constitutes acceptance of these terms.  If you do 
+ not agree with these terms, please do not use, install, modify or 
+ redistribute this Apple software. 
+  
+ In consideration of your agreement to abide by the following terms, and 
+ subject to these terms, Apple grants you a personal, non-exclusive 
+ license, under Apple's copyrights in this original Apple software (the 
+ "Apple Software"), to use, reproduce, modify and redistribute the Apple 
+ Software, with or without modifications, in source and/or binary forms; 
+ provided that if you redistribute the Apple Software in its entirety and 
+ without modifications, you must retain this notice and the following 
+ text and disclaimers in all such redistributions of the Apple Software. 
+ Neither the name, trademarks, service marks or logos of Apple Inc. may 
+ be used to endorse or promote products derived from the Apple Software 
+ without specific prior written permission from Apple.  Except as 
+ expressly stated in this notice, no other rights or licenses, express or 
+ implied, are granted by Apple herein, including but not limited to any 
+ patent rights that may be infringed by your derivative works or by other 
+ works in which the Apple Software may be incorporated. 
+  
+ The Apple Software is provided by Apple on an "AS IS" basis.  APPLE 
+ MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 
+ THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS 
+ FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND 
+ OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 
+  
+ IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL 
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
+ INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, 
+ MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED 
+ AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), 
+ STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE 
+ POSSIBILITY OF SUCH DAMAGE. 
+  
+ Copyright (C) 2013 Apple Inc. All Rights Reserved. 
+  
+ */ 
+
+#import "DirectoryWatcher.h"
+
+#include <sys/types.h>
+#include <sys/event.h>
+#include <sys/time.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#import <CoreFoundation/CoreFoundation.h>
+
+@interface DirectoryWatcher (DirectoryWatcherPrivate)
+- (BOOL)startMonitoringDirectory:(NSString *)dirPath;
+- (void)kqueueFired;
+@end
+
+
+#pragma mark -
+
+@implementation DirectoryWatcher
+
+@synthesize delegate;
+
+- (instancetype)init
+{
+	self = [super init];
+	delegate = NULL;
+
+	dirFD = -1;
+    kq = -1;
+	dirKQRef = NULL;
+	
+	return self;
+}
+
+- (void)dealloc
+{
+	[self invalidate];
+}
+
++ (DirectoryWatcher *)watchFolderWithPath:(NSString *)watchPath delegate:(id)watchDelegate
+{
+	DirectoryWatcher *retVal = NULL;
+	if ((watchDelegate != NULL) && (watchPath != NULL))
+	{
+		DirectoryWatcher *tempManager = [[DirectoryWatcher alloc] init];
+		tempManager.delegate = watchDelegate;		
+		if ([tempManager startMonitoringDirectory: watchPath])
+		{
+			// Everything appears to be in order, so return the DirectoryWatcher.  
+			// Otherwise we'll fall through and return NULL.
+			retVal = tempManager;
+		}
+	}
+	return retVal;
+}
+
+- (void)invalidate
+{
+	if (dirKQRef != NULL)
+	{
+		CFFileDescriptorInvalidate(dirKQRef);
+		CFRelease(dirKQRef);
+		dirKQRef = NULL;
+		// We don't need to close the kq, CFFileDescriptorInvalidate closed it instead.
+		// Change the value so no one thinks it's still live.
+		kq = -1;
+	}
+	
+	if(dirFD != -1)
+	{
+		close(dirFD);
+		dirFD = -1;
+	}
+}
+
+@end
+
+
+#pragma mark -
+
+@implementation DirectoryWatcher (DirectoryWatcherPrivate)
+
+- (void)kqueueFired
+{
+    assert(kq >= 0);
+
+    struct kevent   event;
+    struct timespec timeout = {0, 0};
+    int             eventCount;
+	
+    eventCount = kevent(kq, NULL, 0, &event, 1, &timeout);
+    assert((eventCount >= 0) && (eventCount < 2));
+    
+	// call our delegate of the directory change
+    [delegate directoryDidChange:self];
+
+    CFFileDescriptorEnableCallBacks(dirKQRef, kCFFileDescriptorReadCallBack);
+}
+
+static void KQCallback(CFFileDescriptorRef kqRef, CFOptionFlags callBackTypes, void *info)
+{
+    DirectoryWatcher *obj;
+	
+    obj = (__bridge DirectoryWatcher *)info;
+    assert([obj isKindOfClass:[DirectoryWatcher class]]);
+    assert(kqRef == obj->dirKQRef);
+    assert(callBackTypes == kCFFileDescriptorReadCallBack);
+	
+    [obj kqueueFired];
+}
+
+- (BOOL)startMonitoringDirectory:(NSString *)dirPath
+{
+	// Double initializing is not going to work...
+	if ((dirKQRef == NULL) && (dirFD == -1) && (kq == -1))
+	{
+		// Open the directory we're going to watch
+		dirFD = open([dirPath fileSystemRepresentation], O_EVTONLY);
+		if (dirFD >= 0)
+		{
+			// Create a kqueue for our event messages...
+			kq = kqueue();
+			if (kq >= 0)
+			{
+				struct kevent eventToAdd;
+				eventToAdd.ident  = dirFD;
+				eventToAdd.filter = EVFILT_VNODE;
+				eventToAdd.flags  = EV_ADD | EV_CLEAR;
+				eventToAdd.fflags = NOTE_WRITE;
+				eventToAdd.data   = 0;
+				eventToAdd.udata  = NULL;
+				
+				int errNum = kevent(kq, &eventToAdd, 1, NULL, 0, NULL);
+				if (errNum == 0)
+				{
+					CFFileDescriptorContext context = { 0, (__bridge void *)(self), NULL, NULL, NULL };
+					CFRunLoopSourceRef      rls;
+
+					// Passing true in the third argument so CFFileDescriptorInvalidate will close kq.
+					dirKQRef = CFFileDescriptorCreate(NULL, kq, true, KQCallback, &context);
+					if (dirKQRef != NULL)
+					{
+						rls = CFFileDescriptorCreateRunLoopSource(NULL, dirKQRef, 0);
+						if (rls != NULL)
+						{
+							CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
+							CFRelease(rls);
+							CFFileDescriptorEnableCallBacks(dirKQRef, kCFFileDescriptorReadCallBack);
+							
+							// If everything worked, return early and bypass shutting things down
+							return YES;
+						}
+						// Couldn't create a runloop source, invalidate and release the CFFileDescriptorRef
+						CFFileDescriptorInvalidate(dirKQRef);
+                        CFRelease(dirKQRef);
+						dirKQRef = NULL;
+					}
+				}
+				// kq is active, but something failed, close the handle...
+				close(kq);
+				kq = -1;
+			}
+			// file handle is open, but something failed, close the handle...
+			close(dirFD);
+			dirFD = -1;
+		}
+	}
+	return NO;
+}
+
+@end

+ 80 - 2
AspenProject/VLCAppDelegate.m

@@ -7,14 +7,19 @@
 //
 
 #import "VLCAppDelegate.h"
+#import "DirectoryWatcher.h"
 
 #import "VLCPlaylistViewController.h"
 #import "VLCMovieViewController.h"
 #import "PAPasscodeViewController.h"
 
-@interface VLCAppDelegate () <PAPasscodeViewControllerDelegate> {
+@interface VLCAppDelegate () <PAPasscodeViewControllerDelegate, DirectoryWatcherDelegate> {
     NSURL *_tempURL;
     PAPasscodeViewController *_passcodeLockController;
+
+    DirectoryWatcher *_directoryWatcher;
+    NSTimer *_addMediaTimer;
+    NSMutableDictionary *_addedFiles;
 }
 
 @property (nonatomic) BOOL passcodeValidated;
@@ -36,6 +41,8 @@
 {
     self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
 
+    _directoryWatcher = [DirectoryWatcher watchFolderWithPath:[self directoryPath] delegate:self];
+
     _playlistViewController = [[VLCPlaylistViewController alloc] init];
 
     self.navigationController = [[UINavigationController alloc] initWithRootViewController:_playlistViewController];
@@ -114,7 +121,71 @@
     // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
 }
 
-- (void)updateMediaList
+#pragma mark - directory watcher delegate
+
+- (void)addFileTimerFired
+{
+    NSArray *allKeys = [_addedFiles allKeys];
+    for (NSString *fileURL in allKeys) {
+        NSDictionary *attribs = [[NSFileManager defaultManager] attributesOfItemAtPath:fileURL error:nil];
+
+        NSNumber *prevFetchedSize = [_addedFiles objectForKey:fileURL];
+        NSNumber *updatedSize = [attribs objectForKey:NSFileSize];
+        if ([prevFetchedSize compare:updatedSize] == NSOrderedSame) {
+            [_addedFiles removeObjectForKey:fileURL];
+            [[MLMediaLibrary sharedMediaLibrary] addFilePaths:@[fileURL]];
+            [_playlistViewController updateViewContents];
+        } else {
+            [_addedFiles setObject:updatedSize forKey:fileURL];
+        }
+    }
+    
+    if (_addedFiles.count == 0) {
+        [_addMediaTimer invalidate];
+        _addMediaTimer = nil;
+    }
+}
+
+- (void)directoryDidChange:(DirectoryWatcher *)folderWatcher
+{
+    NSArray *foundFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:[self directoryPath] error:nil];
+    NSMutableArray *matchedFiles = [NSMutableArray arrayWithCapacity:foundFiles.count];
+    for (NSString *fileName in foundFiles) {
+        if ([fileName rangeOfString:kSupportedFileExtensions options:NSRegularExpressionSearch|NSCaseInsensitiveSearch].length != 0) {
+            [matchedFiles addObject:[[self directoryPath] stringByAppendingPathComponent:fileName]];
+        }
+    }
+
+    NSArray *mediaFiles = [MLFile allFiles];
+    NSMutableArray *mediaFilesPaths = [NSMutableArray arrayWithCapacity:mediaFiles.count];
+    for (MLFile *file in mediaFiles) {
+        [mediaFilesPaths addObject:file.url];
+    }
+
+    if (mediaFiles.count > matchedFiles.count) { // File was deleted
+        [self updateMediaList];
+
+    } else if (mediaFiles.count < matchedFiles.count) { // File was added
+        NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:@"not (self in %@)", mediaFilesPaths];
+        NSArray *addedFiles = [matchedFiles filteredArrayUsingPredicate:filterPredicate];
+
+        _addedFiles = [NSMutableDictionary dictionaryWithCapacity:[addedFiles count]];
+        for (NSString *fileURL in addedFiles) {
+            [_addedFiles setObject:@(0) forKey:fileURL];
+        }
+
+        _addMediaTimer = [NSTimer scheduledTimerWithTimeInterval:2. target:self
+                                                        selector:@selector(addFileTimerFired)
+                                                        userInfo:nil repeats:YES];
+
+    } else {
+        APLog(@"Directory content changes for undefined reason");
+    }
+}
+
+#pragma mark - media list methods
+
+- (NSString *)directoryPath
 {
 #define LOCAL_PLAYBACK_HACK 1
 #if LOCAL_PLAYBACK_HACK && TARGET_IPHONE_SIMULATOR
@@ -123,6 +194,13 @@
     NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
     NSString *directoryPath = searchPaths[0];
 #endif
+
+    return directoryPath;
+}
+
+- (void)updateMediaList
+{
+    NSString *directoryPath = [self directoryPath];
     NSArray *foundFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:directoryPath error:nil];
     NSMutableArray *filePaths = [NSMutableArray arrayWithCapacity:[foundFiles count]];
     NSURL *fileURL;

+ 6 - 0
VLC for iOS.xcodeproj/project.pbxproj

@@ -189,6 +189,7 @@
 		A7A0E9F9174BA66000162F25 /* papasscode_marker.png in Resources */ = {isa = PBXBuildFile; fileRef = A7A0E9F1174BA66000162F25 /* papasscode_marker.png */; };
 		A7A0E9FA174BA66000162F25 /* papasscode_marker@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = A7A0E9F2174BA66000162F25 /* papasscode_marker@2x.png */; };
 		A7A0E9FB174BA66000162F25 /* PAPasscodeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = A7A0E9F4174BA66000162F25 /* PAPasscodeViewController.m */; };
+		A7C30259175A3C7A00AD4388 /* DirectoryWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = A7C30258175A3C7A00AD4388 /* DirectoryWatcher.m */; };
 		A7CB0DB11716F72600050CF3 /* PlayingExternally@2x~iphone.png in Resources */ = {isa = PBXBuildFile; fileRef = A7CB0DAD1716F72600050CF3 /* PlayingExternally@2x~iphone.png */; };
 		A7CB0DB21716F72600050CF3 /* PlayingExternally~iphone.png in Resources */ = {isa = PBXBuildFile; fileRef = A7CB0DAE1716F72600050CF3 /* PlayingExternally~iphone.png */; };
 		A7CB0DB31716F72600050CF3 /* PlayingExternally~ipad.png in Resources */ = {isa = PBXBuildFile; fileRef = A7CB0DAF1716F72600050CF3 /* PlayingExternally~ipad.png */; };
@@ -481,6 +482,8 @@
 		A7A0E9F2174BA66000162F25 /* papasscode_marker@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "papasscode_marker@2x.png"; sourceTree = "<group>"; };
 		A7A0E9F3174BA66000162F25 /* PAPasscodeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PAPasscodeViewController.h; sourceTree = "<group>"; };
 		A7A0E9F4174BA66000162F25 /* PAPasscodeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PAPasscodeViewController.m; sourceTree = "<group>"; };
+		A7C30257175A3C7A00AD4388 /* DirectoryWatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DirectoryWatcher.h; sourceTree = "<group>"; };
+		A7C30258175A3C7A00AD4388 /* DirectoryWatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DirectoryWatcher.m; sourceTree = "<group>"; };
 		A7CB0DAD1716F72600050CF3 /* PlayingExternally@2x~iphone.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "PlayingExternally@2x~iphone.png"; sourceTree = "<group>"; };
 		A7CB0DAE1716F72600050CF3 /* PlayingExternally~iphone.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "PlayingExternally~iphone.png"; sourceTree = "<group>"; };
 		A7CB0DAF1716F72600050CF3 /* PlayingExternally~ipad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "PlayingExternally~ipad.png"; sourceTree = "<group>"; };
@@ -989,6 +992,8 @@
 		7D94FCE416DE7D1000F2623B /* AspenProject */ = {
 			isa = PBXGroup;
 			children = (
+				A7C30257175A3C7A00AD4388 /* DirectoryWatcher.h */,
+				A7C30258175A3C7A00AD4388 /* DirectoryWatcher.m */,
 				7D6B08BB174A72A900A05173 /* VLCConstants.h */,
 				7D94FCED16DE7D1000F2623B /* VLCAppDelegate.h */,
 				7D94FCEE16DE7D1000F2623B /* VLCAppDelegate.m */,
@@ -1377,6 +1382,7 @@
 				7D5E39CF174FCE04007DAFA1 /* VLCDropboxTableViewCell.m in Sources */,
 				7D5F7AC317529430006CCCFA /* VLCHorizontalSwipeGestureRecognizer.m in Sources */,
 				7D5F7AC61752943F006CCCFA /* VLCVerticalSwipeGestureRecognizer.m in Sources */,
+				A7C30259175A3C7A00AD4388 /* DirectoryWatcher.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};