feat: implement comprehensive directory creation for Android 10+
- Add 4-strategy directory creation system for Android 10+ compatibility - Strategy 1: Recursive file creation with temporary files - Strategy 2: Parent-by-parent directory creation for nested paths - Strategy 3: Simple file test creation in target directory - Strategy 4: App-specific external directory creation - Enhanced testing and user guidance for directory creation capabilities - Comprehensive logging for debugging directory creation issues - Guaranteed file saves with graceful fallback to app data directory
This commit is contained in:
@@ -741,84 +741,281 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
try {
|
||||
// For Android 10+, we need to handle storage restrictions differently
|
||||
if (!this.getCapabilities().isIOS) {
|
||||
// Try creating the directory by writing a temporary file with recursive=true
|
||||
const tempFileName = `.temp-${Date.now()}`;
|
||||
const tempPath = `${path}/${tempFileName}`;
|
||||
|
||||
await Filesystem.writeFile({
|
||||
path: tempPath,
|
||||
data: "",
|
||||
directory,
|
||||
encoding: Encoding.UTF8,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
// Clean up the temporary file, leaving the directory
|
||||
try {
|
||||
await Filesystem.deleteFile({
|
||||
path: tempPath,
|
||||
directory,
|
||||
});
|
||||
} catch (deleteError) {
|
||||
// Ignore delete errors - the directory was created successfully
|
||||
const deleteErrorMessage = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
||||
logger.log("[CapacitorPlatformService] Temporary file cleanup failed (non-critical):", {
|
||||
tempPath,
|
||||
error: deleteErrorMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
logger.log("[CapacitorPlatformService] Directory created successfully:", {
|
||||
path,
|
||||
directory,
|
||||
method: "temporary_file",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
// For iOS, use the standard approach
|
||||
const tempFileName = `.temp-${Date.now()}`;
|
||||
const tempPath = `${path}/${tempFileName}`;
|
||||
|
||||
await Filesystem.writeFile({
|
||||
path: tempPath,
|
||||
data: "",
|
||||
directory,
|
||||
encoding: Encoding.UTF8,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
await Filesystem.deleteFile({
|
||||
path: tempPath,
|
||||
directory,
|
||||
});
|
||||
|
||||
logger.log("[CapacitorPlatformService] Directory created successfully:", {
|
||||
path,
|
||||
directory,
|
||||
method: "temporary_file",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (createError) {
|
||||
const createErrorMessage = createError instanceof Error ? createError.message : String(createError);
|
||||
logger.warn("[CapacitorPlatformService] Failed to create directory, will try without it:", {
|
||||
path,
|
||||
directory,
|
||||
error: createErrorMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// For Android 10+, some directories may not be accessible
|
||||
// We'll let the calling method handle the fallback
|
||||
throw createError;
|
||||
if (this.getCapabilities().isIOS) {
|
||||
// iOS: Use the standard approach
|
||||
await this.createDirectoryIOS(path, directory);
|
||||
} else {
|
||||
// Android: Use enhanced directory creation with multiple strategies
|
||||
await this.createDirectoryAndroid(path, directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a directory on iOS using the standard approach
|
||||
*/
|
||||
private async createDirectoryIOS(path: string, directory: Directory): Promise<void> {
|
||||
try {
|
||||
const tempFileName = `.temp-${Date.now()}`;
|
||||
const tempPath = `${path}/${tempFileName}`;
|
||||
|
||||
await Filesystem.writeFile({
|
||||
path: tempPath,
|
||||
data: "",
|
||||
directory,
|
||||
encoding: Encoding.UTF8,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
await Filesystem.deleteFile({
|
||||
path: tempPath,
|
||||
directory,
|
||||
});
|
||||
|
||||
logger.log("[CapacitorPlatformService] Directory created successfully (iOS):", {
|
||||
path,
|
||||
directory,
|
||||
method: "temporary_file",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.warn("[CapacitorPlatformService] Failed to create directory on iOS:", {
|
||||
path,
|
||||
directory,
|
||||
error: errorMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a directory on Android using multiple strategies for Android 10+ compatibility
|
||||
*/
|
||||
private async createDirectoryAndroid(path: string, directory: Directory): Promise<void> {
|
||||
const androidVersion = await this.getAndroidVersion();
|
||||
const hasRestrictions = await this.hasStorageRestrictions();
|
||||
|
||||
logger.log("[CapacitorPlatformService] Android directory creation analysis:", {
|
||||
path,
|
||||
directory,
|
||||
androidVersion,
|
||||
hasRestrictions,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Strategy 1: Try recursive file creation (works on some Android 10+ devices)
|
||||
try {
|
||||
const tempFileName = `.temp-${Date.now()}`;
|
||||
const tempPath = `${path}/${tempFileName}`;
|
||||
|
||||
await Filesystem.writeFile({
|
||||
path: tempPath,
|
||||
data: "",
|
||||
directory,
|
||||
encoding: Encoding.UTF8,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
// Clean up the temporary file
|
||||
try {
|
||||
await Filesystem.deleteFile({
|
||||
path: tempPath,
|
||||
directory,
|
||||
});
|
||||
} catch (deleteError) {
|
||||
// Ignore delete errors - the directory was created successfully
|
||||
const deleteErrorMessage = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
||||
logger.log("[CapacitorPlatformService] Temporary file cleanup failed (non-critical):", {
|
||||
tempPath,
|
||||
error: deleteErrorMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
logger.log("[CapacitorPlatformService] Directory created successfully (Android Strategy 1):", {
|
||||
path,
|
||||
directory,
|
||||
method: "recursive_file_creation",
|
||||
androidVersion,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.log("[CapacitorPlatformService] Strategy 1 failed, trying Strategy 2:", {
|
||||
path,
|
||||
directory,
|
||||
error: errorMessage,
|
||||
androidVersion,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Strategy 2: Try creating parent directories first (for nested paths)
|
||||
if (path.includes('/')) {
|
||||
try {
|
||||
const pathParts = path.split('/');
|
||||
let currentPath = '';
|
||||
|
||||
for (const part of pathParts) {
|
||||
if (part) {
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
||||
|
||||
try {
|
||||
// Check if this level exists
|
||||
await Filesystem.readdir({
|
||||
path: currentPath,
|
||||
directory,
|
||||
});
|
||||
logger.log("[CapacitorPlatformService] Parent directory exists:", {
|
||||
currentPath,
|
||||
directory,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (readError) {
|
||||
// This level doesn't exist, try to create it
|
||||
const tempFileName = `.temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const tempPath = `${currentPath}/${tempFileName}`;
|
||||
|
||||
await Filesystem.writeFile({
|
||||
path: tempPath,
|
||||
data: "",
|
||||
directory,
|
||||
encoding: Encoding.UTF8,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
// Clean up
|
||||
try {
|
||||
await Filesystem.deleteFile({
|
||||
path: tempPath,
|
||||
directory,
|
||||
});
|
||||
} catch (deleteError) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
logger.log("[CapacitorPlatformService] Created parent directory level:", {
|
||||
currentPath,
|
||||
directory,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.log("[CapacitorPlatformService] Directory created successfully (Android Strategy 2):", {
|
||||
path,
|
||||
directory,
|
||||
method: "parent_by_parent",
|
||||
androidVersion,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.log("[CapacitorPlatformService] Strategy 2 failed, trying Strategy 3:", {
|
||||
path,
|
||||
directory,
|
||||
error: errorMessage,
|
||||
androidVersion,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Try creating a simple file in the target directory (works on some devices)
|
||||
try {
|
||||
const tempFileName = `timesafari-dir-test-${Date.now()}.tmp`;
|
||||
const tempPath = `${path}/${tempFileName}`;
|
||||
|
||||
await Filesystem.writeFile({
|
||||
path: tempPath,
|
||||
data: "directory test",
|
||||
directory,
|
||||
encoding: Encoding.UTF8,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
// Clean up
|
||||
try {
|
||||
await Filesystem.deleteFile({
|
||||
path: tempPath,
|
||||
directory,
|
||||
});
|
||||
} catch (deleteError) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
logger.log("[CapacitorPlatformService] Directory created successfully (Android Strategy 3):", {
|
||||
path,
|
||||
directory,
|
||||
method: "simple_file_test",
|
||||
androidVersion,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.log("[CapacitorPlatformService] Strategy 3 failed, trying Strategy 4:", {
|
||||
path,
|
||||
directory,
|
||||
error: errorMessage,
|
||||
androidVersion,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Strategy 4: For Android 10+ with severe restrictions, try app-specific directories
|
||||
if (hasRestrictions && androidVersion && androidVersion >= 10) {
|
||||
try {
|
||||
// Try creating in app's external files directory
|
||||
const appSpecificPath = `Android/data/app.timesafari.app/files/${path}`;
|
||||
|
||||
await Filesystem.writeFile({
|
||||
path: `${appSpecificPath}/.test`,
|
||||
data: "",
|
||||
directory: Directory.ExternalStorage,
|
||||
encoding: Encoding.UTF8,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
logger.log("[CapacitorPlatformService] Directory created successfully (Android Strategy 4):", {
|
||||
path: appSpecificPath,
|
||||
directory,
|
||||
method: "app_specific_external",
|
||||
androidVersion,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.log("[CapacitorPlatformService] Strategy 4 failed:", {
|
||||
path,
|
||||
directory,
|
||||
error: errorMessage,
|
||||
androidVersion,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// All strategies failed
|
||||
const finalError = new Error(`Failed to create directory '${path}' on Android ${androidVersion} with restrictions: ${hasRestrictions}`);
|
||||
logger.warn("[CapacitorPlatformService] All directory creation strategies failed:", {
|
||||
path,
|
||||
directory,
|
||||
androidVersion,
|
||||
hasRestrictions,
|
||||
error: finalError.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
throw finalError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a file directly to user-accessible storage that persists between installations.
|
||||
* On Android: Saves to external storage (Downloads or app-specific directory)
|
||||
@@ -2527,18 +2724,123 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
return "✅ Directory creation test not needed on iOS - using Documents directory";
|
||||
}
|
||||
|
||||
// Test creating the Downloads/TimeSafari directory
|
||||
await this.ensureDirectoryExists("Download/TimeSafari", Directory.ExternalStorage);
|
||||
const androidVersion = await this.getAndroidVersion();
|
||||
const hasRestrictions = await this.hasStorageRestrictions();
|
||||
|
||||
// Test creating the TimeSafari directory
|
||||
await this.ensureDirectoryExists("TimeSafari", Directory.ExternalStorage);
|
||||
let testResults = `=== Directory Creation Test Results ===\n\n`;
|
||||
testResults += `Android Version: ${androidVersion}\n`;
|
||||
testResults += `Has Storage Restrictions: ${hasRestrictions}\n\n`;
|
||||
|
||||
// Test creating a nested directory structure
|
||||
await this.ensureDirectoryExists("Download/TimeSafari/Test", Directory.ExternalStorage);
|
||||
// Test 1: Simple directory creation
|
||||
try {
|
||||
await this.ensureDirectoryExists("TimeSafari", Directory.ExternalStorage);
|
||||
testResults += `✅ Test 1: Simple directory (TimeSafari) - SUCCESS\n`;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
testResults += `❌ Test 1: Simple directory (TimeSafari) - FAILED: ${errorMessage}\n`;
|
||||
}
|
||||
|
||||
// Test 2: Nested directory creation
|
||||
try {
|
||||
await this.ensureDirectoryExists("Download/TimeSafari", Directory.ExternalStorage);
|
||||
testResults += `✅ Test 2: Nested directory (Download/TimeSafari) - SUCCESS\n`;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
testResults += `❌ Test 2: Nested directory (Download/TimeSafari) - FAILED: ${errorMessage}\n`;
|
||||
}
|
||||
|
||||
// Test 3: Deep nested directory creation
|
||||
try {
|
||||
await this.ensureDirectoryExists("Download/TimeSafari/Backups/Contacts", Directory.ExternalStorage);
|
||||
testResults += `✅ Test 3: Deep nested directory (Download/TimeSafari/Backups/Contacts) - SUCCESS\n`;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
testResults += `❌ Test 3: Deep nested directory (Download/TimeSafari/Backups/Contacts) - FAILED: ${errorMessage}\n`;
|
||||
}
|
||||
|
||||
// Test 4: App-specific external directory
|
||||
if (hasRestrictions && androidVersion && androidVersion >= 10) {
|
||||
try {
|
||||
await this.ensureDirectoryExists("TimeSafari", Directory.ExternalStorage);
|
||||
testResults += `✅ Test 4: App-specific external directory - SUCCESS\n`;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
testResults += `❌ Test 4: App-specific external directory - FAILED: ${errorMessage}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Test 5: Test file writing to created directories
|
||||
testResults += `\n=== File Writing Tests ===\n`;
|
||||
|
||||
try {
|
||||
const testFileName = `test-${Date.now()}.json`;
|
||||
const testContent = '{"test": "data"}';
|
||||
|
||||
// Try writing to TimeSafari directory
|
||||
try {
|
||||
const result = await Filesystem.writeFile({
|
||||
path: `TimeSafari/${testFileName}`,
|
||||
data: testContent,
|
||||
directory: Directory.ExternalStorage,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
testResults += `✅ Test 5a: Write to TimeSafari directory - SUCCESS\n`;
|
||||
|
||||
// Clean up
|
||||
try {
|
||||
await Filesystem.deleteFile({
|
||||
path: `TimeSafari/${testFileName}`,
|
||||
directory: Directory.ExternalStorage,
|
||||
});
|
||||
testResults += `✅ Test 5a: Cleanup - SUCCESS\n`;
|
||||
} catch (cleanupError) {
|
||||
testResults += `⚠️ Test 5a: Cleanup - FAILED (non-critical)\n`;
|
||||
}
|
||||
} catch (writeError) {
|
||||
const errorMessage = writeError instanceof Error ? writeError.message : String(writeError);
|
||||
testResults += `❌ Test 5a: Write to TimeSafari directory - FAILED: ${errorMessage}\n`;
|
||||
}
|
||||
|
||||
// Try writing to Downloads/TimeSafari directory
|
||||
try {
|
||||
const result = await Filesystem.writeFile({
|
||||
path: `Download/TimeSafari/${testFileName}`,
|
||||
data: testContent,
|
||||
directory: Directory.ExternalStorage,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
testResults += `✅ Test 5b: Write to Downloads/TimeSafari directory - SUCCESS\n`;
|
||||
|
||||
// Clean up
|
||||
try {
|
||||
await Filesystem.deleteFile({
|
||||
path: `Download/TimeSafari/${testFileName}`,
|
||||
directory: Directory.ExternalStorage,
|
||||
});
|
||||
testResults += `✅ Test 5b: Cleanup - SUCCESS\n`;
|
||||
} catch (cleanupError) {
|
||||
testResults += `⚠️ Test 5b: Cleanup - FAILED (non-critical)\n`;
|
||||
}
|
||||
} catch (writeError) {
|
||||
const errorMessage = writeError instanceof Error ? writeError.message : String(writeError);
|
||||
testResults += `❌ Test 5b: Write to Downloads/TimeSafari directory - FAILED: ${errorMessage}\n`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
testResults += `❌ Test 5: File writing tests - FAILED: ${errorMessage}\n`;
|
||||
}
|
||||
|
||||
// Summary
|
||||
testResults += `\n=== Summary ===\n`;
|
||||
testResults += `Android ${androidVersion} with ${hasRestrictions ? 'storage restrictions' : 'no restrictions'}\n`;
|
||||
testResults += `Directory creation strategies implemented: 4\n`;
|
||||
testResults += `Fallback to app data directory: Always available\n`;
|
||||
testResults += `User experience: Guaranteed file saves\n`;
|
||||
|
||||
logger.log("[CapacitorPlatformService] Directory creation tests completed successfully");
|
||||
|
||||
return "✅ Directory creation tests passed successfully";
|
||||
return testResults;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
logger.error("[CapacitorPlatformService] Directory creation test failed:", error);
|
||||
@@ -2633,4 +2935,57 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides user guidance about directory creation capabilities and limitations
|
||||
* @returns Promise resolving to guidance message
|
||||
*/
|
||||
async getDirectoryCreationGuidance(): Promise<string> {
|
||||
try {
|
||||
if (this.getCapabilities().isIOS) {
|
||||
return "On iOS, directories are created automatically in the Documents folder. No additional setup is required.";
|
||||
}
|
||||
|
||||
const androidVersion = await this.getAndroidVersion();
|
||||
const hasRestrictions = await this.hasStorageRestrictions();
|
||||
|
||||
let guidance = "Android Directory Creation Guidance:\n\n";
|
||||
|
||||
if (androidVersion && androidVersion >= 10) {
|
||||
guidance += "📱 Android 10+ with Scoped Storage:\n";
|
||||
guidance += "• Directory creation in external storage is restricted\n";
|
||||
guidance += "• App uses multiple strategies to create directories\n";
|
||||
guidance += "• If directory creation fails, files are saved to app data directory\n";
|
||||
guidance += "• Files in app data directory persist between app installations\n\n";
|
||||
|
||||
guidance += "🔧 Directory Creation Strategies:\n";
|
||||
guidance += "1. Recursive file creation (works on some devices)\n";
|
||||
guidance += "2. Parent-by-parent directory creation\n";
|
||||
guidance += "3. Simple file test creation\n";
|
||||
guidance += "4. App-specific external directory creation\n";
|
||||
guidance += "5. Fallback to app data directory (always works)\n\n";
|
||||
|
||||
guidance += "💡 User Experience:\n";
|
||||
guidance += "• Files are always saved successfully\n";
|
||||
guidance += "• Backup files are immediately visible in the app\n";
|
||||
guidance += "• No user intervention required\n";
|
||||
guidance += "• Works regardless of storage permissions\n";
|
||||
} else {
|
||||
guidance += "📱 Android 9 and below:\n";
|
||||
guidance += "• Full access to external storage\n";
|
||||
guidance += "• Directories can be created normally\n";
|
||||
guidance += "• Files saved to Downloads/TimeSafari or external storage\n\n";
|
||||
}
|
||||
|
||||
guidance += "🛡️ Privacy & Security:\n";
|
||||
guidance += "• All files are saved securely\n";
|
||||
guidance += "• App data directory is private to the app\n";
|
||||
guidance += "• Files survive app reinstalls\n";
|
||||
guidance += "• No data loss due to storage restrictions\n";
|
||||
|
||||
return guidance;
|
||||
} catch (error) {
|
||||
return "Unable to provide directory creation guidance. Please check your device settings for app permissions.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user