ReadViewSDK/Doc/WXRead/decompiled/WRPreloadBookManager.m

548 lines
18 KiB
Objective-C

//
// WRPreloadBookManager.m
// WeRead (读书)
// Reverse-engineered implementation reconstruction
//
// Detailed pseudo-code based on binary analysis, ivar types (NSMutableDictionary),
// 23 known methods, and contextual knowledge of WeRead's preload strategy.
//
// Architecture notes:
// - Class methods handle key/filename storage (shared state via NSUserDefaults
// or a static dictionary).
// - Instance methods handle the actual preloading logic.
// - Preloading decisions are based on book ranking, reading patterns,
// and network conditions (WiFi vs. cellular).
// - Preloaded content is encrypted and stored in a separate cache directory.
//
#import "WRPreloadBookManager.h"
#import "WRBookNetwork.h"
#import "WREncryptedFileManager.h"
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
static NSString *const kPreloadEncryptKeyPrefix = @"preload_key_";
static NSString *const kPreloadFileNamePrefix = @"preload_fn_";
static NSString *const kPreloadCacheDir = @"preload_cache";
// Preload limits
static const NSUInteger kMaxPreloadChaptersOnWiFi = 50;
static const NSUInteger kMaxPreloadChaptersOnCellular = 5;
static const NSUInteger kMaxPreloadBooksOnShelf = 3;
#pragma mark - Private Interface
@interface WRPreloadBookManager ()
@property (nonatomic, strong, readwrite) NSMutableDictionary *preloadState;
@end
#pragma mark - Implementation
@implementation WRPreloadBookManager
{
// Ivar from binary analysis:
NSMutableDictionary *_preloadState;
}
#pragma mark - Lifecycle
- (instancetype)init
{
self = [super init];
if (self) {
_preloadState = [NSMutableDictionary dictionary];
// Register for network change notifications to adjust preload behavior
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(_networkStatusChanged:)
name:@"WRNetworkStatusChangedNotification"
object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Class Methods: Encryption Key Storage
+ (void)saveEncryptKey:(NSString *)key
forPath:(NSString *)path
bookId:(NSString *)bookId
{
if (!key || !bookId) return;
// Store in NSUserDefaults with a composite key
NSString *storageKey = [NSString stringWithFormat:@"%@%@_%@",
kPreloadEncryptKeyPrefix, bookId,
[path lastPathComponent]];
[[NSUserDefaults standardUserDefaults] setObject:key forKey:storageKey];
[[NSUserDefaults standardUserDefaults] synchronize];
// Also save to the keychain via WREncryptedFileManager for persistence
NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding];
[WREncryptedFileManager saveKey:keyData forBookId:bookId];
}
+ (nullable NSString *)encryptKeyForPath:(NSString *)path
bookId:(NSString *)bookId
{
NSString *storageKey = [NSString stringWithFormat:@"%@%@_%@",
kPreloadEncryptKeyPrefix, bookId,
[path lastPathComponent]];
NSString *key = [[NSUserDefaults standardUserDefaults] stringForKey:storageKey];
// Fallback: try the keychain
if (!key) {
NSData *keyData = [WREncryptedFileManager keyForBookId:bookId];
if (keyData) {
key = [[NSString alloc] initWithData:keyData encoding:NSUTF8StringEncoding];
}
}
return key;
}
+ (void)removeEncryptKeyForPath:(NSString *)path
bookId:(NSString *)bookId
{
NSString *storageKey = [NSString stringWithFormat:@"%@%@_%@",
kPreloadEncryptKeyPrefix, bookId,
[path lastPathComponent]];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:storageKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
#pragma mark - Class Methods: Filename Dictionary
+ (void)saveFileNameDict:(NSDictionary<NSString *, NSString *> *)dict
bookId:(NSString *)bookId
{
if (!dict || !bookId) return;
NSString *storageKey = [NSString stringWithFormat:@"%@%@", kPreloadFileNamePrefix, bookId];
[[NSUserDefaults standardUserDefaults] setObject:dict forKey:storageKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
+ (nullable NSString *)fileNameForKey:(NSString *)key
bookId:(NSString *)bookId
{
NSString *storageKey = [NSString stringWithFormat:@"%@%@", kPreloadFileNamePrefix, bookId];
NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:storageKey];
return dict[key];
}
+ (void)removeFileNameForKey:(NSString *)key
bookId:(NSString *)bookId
{
NSString *storageKey = [NSString stringWithFormat:@"%@%@", kPreloadFileNamePrefix, bookId];
NSMutableDictionary *dict = [[[NSUserDefaults standardUserDefaults]
dictionaryForKey:storageKey] mutableCopy];
if (dict) {
[dict removeObjectForKey:key];
[[NSUserDefaults standardUserDefaults] setObject:dict forKey:storageKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
#pragma mark - Class Methods: Cache Management
+ (void)clearKV
{
// Remove all preload-related keys from NSUserDefaults
NSDictionary *defaults = [[NSUserDefaults standardUserDefaults] dictionaryRepresentation];
NSArray *prefixes = @[kPreloadEncryptKeyPrefix, kPreloadFileNamePrefix];
for (NSString *key in defaults) {
for (NSString *prefix in prefixes) {
if ([key hasPrefix:prefix]) {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
}
}
}
[[NSUserDefaults standardUserDefaults] synchronize];
}
+ (void)removeKVWithBookId:(NSString *)bookId
{
if (!bookId) return;
NSDictionary *defaults = [[NSUserDefaults standardUserDefaults] dictionaryRepresentation];
NSString *keyPrefix1 = [NSString stringWithFormat:@"%@%@", kPreloadEncryptKeyPrefix, bookId];
NSString *keyPrefix2 = [NSString stringWithFormat:@"%@%@", kPreloadFileNamePrefix, bookId];
for (NSString *key in defaults) {
if ([key hasPrefix:keyPrefix1] || [key hasPrefix:keyPrefix2]) {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
}
}
[[NSUserDefaults standardUserDefaults] synchronize];
}
#pragma mark - Instance Methods: Preloading
- (void)_preloadWholeBook:(WRBook *)book scene:(WRPreloadScene)scene
{
// 1. Determine the maximum chapters to preload based on scene
NSUInteger maxChapters;
switch (scene) {
case WRPreloadSceneWiFi:
maxChapters = kMaxPreloadChaptersOnWiFi;
break;
case WRPreloadSceneShelf:
maxChapters = 20; // Moderate for shelf browsing
break;
case WRPreloadSceneReading:
maxChapters = 10; // Next few chapters while reading
break;
default:
maxChapters = kMaxPreloadChaptersOnCellular;
break;
}
// 2. Get the chapter list for the book
NSArray *chapters = book.chapters;
if (!chapters || chapters.count == 0) {
NSLog(@"[WRPreloadBookManager] No chapters for book %@", book.bookId);
return;
}
// 3. Select chapters to preload
NSArray *uidsToPreload = [self _selectChaptersForPreload:book
maxCount:maxChapters
scene:scene];
if (uidsToPreload.count == 0) return;
// 4. Update preload state
NSString *bookId = book.bookId;
_preloadState[bookId] = @{
@"status" : @"preloading",
@"scene" : @(scene),
@"totalCount" : @(uidsToPreload.count),
@"loadedCount" : @(0),
@"startTime" : @([[NSDate date] timeIntervalSince1970]),
};
// 5. Initiate download via WRBookNetwork
[WRBookNetwork loadTarForEpubBookId:bookId
chapters:uidsToPreload
isPreload:YES];
NSLog(@"[WRPreloadBookManager] Started preloading %lu chapters for book %@ (scene=%ld)",
(unsigned long)uidsToPreload.count, bookId, (long)scene);
}
- (void)preloadWithBook:(WRBook *)book fromChapterIdx:(NSInteger)fromChapterIdx
{
// Preload chapters starting from the given index
NSArray *chapters = book.chapters;
if (!chapters || fromChapterIdx >= (NSInteger)chapters.count) return;
// Determine how many chapters to preload ahead
NSUInteger preloadCount = 5; // Default: preload 5 chapters ahead
if ([self _isOnWiFi]) {
preloadCount = 10;
}
NSInteger startIndex = MAX(0, fromChapterIdx);
NSInteger endIndex = MIN(startIndex + (NSInteger)preloadCount,
(NSInteger)chapters.count);
NSMutableArray *uids = [NSMutableArray array];
for (NSInteger i = startIndex; i < endIndex; i++) {
NSDictionary *chapter = chapters[i];
NSString *uid = chapter[@"chapterUid"] ?: chapter[@"uid"];
if (uid) [uids addObject:uid];
}
if (uids.count > 0) {
[self downloadBook:book uids:uids];
}
}
- (void)downloadBook:(WRBook *)book uids:(NSArray<NSString *> *)uids
{
if (!book.bookId || uids.count == 0) return;
// Filter out already-downloaded chapters
NSMutableArray *pendingUIDs = [NSMutableArray array];
NSString *plainDir = [self _plainBookDirectoryForBookId:book.bookId];
for (NSString *uid in uids) {
NSString *chapterPath = [plainDir stringByAppendingPathComponent:
[NSString stringWithFormat:@"chapter_%@.xhtml", uid]];
if (![[NSFileManager defaultManager] fileExistsAtPath:chapterPath]) {
[pendingUIDs addObject:uid];
}
}
if (pendingUIDs.count == 0) return;
// Initiate download
[WRBookNetwork loadTarForEpubBookId:book.bookId
chapters:pendingUIDs
isPreload:YES];
}
- (void)cleanUpPreloadBook
{
// 1. Remove all preloaded book files
NSString *preloadDir = [self _preloadCacheDirectory];
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error = nil;
[fm removeItemAtPath:preloadDir error:&error];
if (error) {
NSLog(@"[WRPreloadBookManager] Cleanup failed: %@", error);
}
// 2. Clear all key-value caches
[[self class] clearKV];
// 3. Reset state
[_preloadState removeAllObjects];
// 4. Recreate the directory
[fm createDirectoryAtPath:preloadDir
withIntermediateDirectories:YES
attributes:nil
error:nil];
}
- (void)calcAndClearPreloadBookWithCompletion:(void (^)(NSUInteger totalSize))completion
onlyCalc:(BOOL)onlyCalc
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *preloadDir = [self _preloadCacheDirectory];
NSUInteger totalSize = [self _directorySizeAtPath:preloadDir];
if (!onlyCalc) {
// Clear the cache
[self cleanUpPreloadBook];
}
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(totalSize);
});
});
}
#pragma mark - Additional Methods (Reconstructed)
- (BOOL)isPreloadingBookId:(NSString *)bookId
{
NSDictionary *state = _preloadState[bookId];
return [state[@"status"] isEqualToString:@"preloading"];
}
- (float)preloadProgressForBookId:(NSString *)bookId
{
NSDictionary *state = _preloadState[bookId];
if (!state) return 0.0;
NSInteger total = [state[@"totalCount"] integerValue];
NSInteger loaded = [state[@"loadedCount"] integerValue];
if (total <= 0) return 0.0;
return (float)loaded / (float)total;
}
- (void)cancelPreloadForBookId:(NSString *)bookId
{
// Mark the preload as cancelled in state
_preloadState[bookId] = @{
@"status" : @"cancelled",
@"cancelTime": @([[NSDate date] timeIntervalSince1970]),
};
// Note: Actual network cancellation would need to be handled
// by WRBookNetwork's task management
NSLog(@"[WRPreloadBookManager] Cancelled preload for book %@", bookId);
}
- (NSArray<NSString *> *)predictedChapterUidsForBook:(WRBook *)book
{
// Predict which chapters the user is likely to read next.
//
// Strategy:
// 1. If the user is currently reading chapter N, predict N+1, N+2, ...
// 2. If the user has a pattern of jumping (e.g., to bookmarks), preload those
// 3. Consider book ranking: popular books get more aggressive preloading
NSMutableArray *predicted = [NSMutableArray array];
NSArray *chapters = book.chapters;
if (!chapters) return predicted;
// Find the current chapter index
NSInteger currentIndex = 0;
NSString *currentUid = book.currentChapterUid;
for (NSUInteger i = 0; i < chapters.count; i++) {
NSDictionary *chapter = chapters[i];
if ([chapter[@"chapterUid"] isEqualToString:currentUid]) {
currentIndex = (NSInteger)i;
break;
}
}
// Predict the next N chapters
NSUInteger predictCount = [self _isOnWiFi] ? 10 : 3;
for (NSInteger i = currentIndex + 1;
i < MIN(currentIndex + 1 + (NSInteger)predictCount, (NSInteger)chapters.count);
i++) {
NSDictionary *chapter = chapters[i];
NSString *uid = chapter[@"chapterUid"] ?: chapter[@"uid"];
if (uid) [predicted addObject:uid];
}
return [predicted copy];
}
#pragma mark - Private Helpers
- (NSArray<NSString *> *)_selectChaptersForPreload:(WRBook *)book
maxCount:(NSUInteger)maxCount
scene:(WRPreloadScene)scene
{
NSArray *chapters = book.chapters;
if (!chapters) return @[];
NSMutableArray *selected = [NSMutableArray array];
NSString *currentUid = book.currentChapterUid;
NSInteger currentIndex = 0;
// Find current chapter index
for (NSUInteger i = 0; i < chapters.count; i++) {
NSDictionary *chapter = chapters[i];
if ([chapter[@"chapterUid"] isEqualToString:currentUid]) {
currentIndex = (NSInteger)i;
break;
}
}
// Select chapters based on scene
switch (scene) {
case WRPreloadSceneReading:
// Preload next chapters from current position
for (NSInteger i = currentIndex;
i < MIN(currentIndex + (NSInteger)maxCount, (NSInteger)chapters.count);
i++) {
NSDictionary *chapter = chapters[i];
NSString *uid = chapter[@"chapterUid"] ?: chapter[@"uid"];
if (uid) [selected addObject:uid];
}
break;
case WRPreloadSceneShelf:
// Preload from the beginning (user might start reading)
for (NSUInteger i = 0; i < MIN(maxCount, chapters.count); i++) {
NSDictionary *chapter = chapters[i];
NSString *uid = chapter[@"chapterUid"] ?: chapter[@"uid"];
if (uid) [selected addObject:uid];
}
break;
case WRPreloadSceneWiFi:
// Aggressive: preload all chapters
for (NSDictionary *chapter in chapters) {
NSString *uid = chapter[@"chapterUid"] ?: chapter[@"uid"];
if (uid) [selected addObject:uid];
if (selected.count >= maxCount) break;
}
break;
default:
// Minimal: just the next chapter
if (currentIndex + 1 < (NSInteger)chapters.count) {
NSDictionary *chapter = chapters[currentIndex + 1];
NSString *uid = chapter[@"chapterUid"] ?: chapter[@"uid"];
if (uid) [selected addObject:uid];
}
break;
}
return [selected copy];
}
- (BOOL)_isOnWiFi
{
// Check network status
// In the actual app, this uses SCNetworkReachability or a wrapper
// For reconstruction, assume WiFi if not explicitly cellular
NSNumber *isWiFi = [[NSUserDefaults standardUserDefaults] objectForKey:@"WRIsOnWiFi"];
return isWiFi ? isWiFi.boolValue : YES; // Default to WiFi for safety
}
- (void)_networkStatusChanged:(NSNotification *)notification
{
// Adjust preload behavior when network changes
BOOL isWiFi = [notification.userInfo[@"isWiFi"] boolValue];
if (!isWiFi) {
// On cellular: cancel aggressive preloads
for (NSString *bookId in [_preloadState copy]) {
NSDictionary *state = _preloadState[bookId];
if ([state[@"scene"] integerValue] == WRPreloadSceneWiFi) {
[self cancelPreloadForBookId:bookId];
}
}
}
}
- (NSString *)_preloadCacheDirectory
{
NSString *caches = NSSearchPathForDirectoriesInDomains(
NSCachesDirectory, NSUserDomainMask, YES).firstObject;
return [caches stringByAppendingPathComponent:kPreloadCacheDir];
}
- (NSString *)_plainBookDirectoryForBookId:(NSString *)bookId
{
NSString *caches = NSSearchPathForDirectoriesInDomains(
NSCachesDirectory, NSUserDomainMask, YES).firstObject;
return [[caches stringByAppendingPathComponent:@"plain_books"]
stringByAppendingPathComponent:bookId];
}
- (NSUInteger)_directorySizeAtPath:(NSString *)path
{
NSFileManager *fm = [NSFileManager defaultManager];
BOOL isDir = NO;
if (![fm fileExistsAtPath:path isDirectory:&isDir]) return 0;
if (!isDir) {
NSDictionary *attrs = [fm attributesOfItemAtPath:path error:nil];
return [attrs fileSize];
}
NSUInteger totalSize = 0;
NSDirectoryEnumerator *enumerator = [fm enumeratorAtPath:path];
for (NSString *filename in enumerator) {
NSString *filePath = [path stringByAppendingPathComponent:filename];
NSDictionary *attrs = [fm attributesOfItemAtPath:filePath error:nil];
totalSize += [attrs fileSize];
}
return totalSize;
}
@end