@ -52,6 +52,11 @@ export class CapacitorPlatformService implements PlatformService {
private operationQueue : Array < QueuedOperation > = [ ] ;
private isProcessingQueue : boolean = false ;
/** Permission request lock to prevent concurrent requests */
private permissionRequestLock : Promise < void > | null = null ;
private permissionGranted : boolean = false ;
private permissionChecked : boolean = false ;
constructor ( ) {
this . sqlite = new SQLiteConnection ( CapacitorSQLite ) ;
}
@ -277,84 +282,88 @@ export class CapacitorPlatformService implements PlatformService {
return ;
}
// For Android, try to access external storage to check permissions
try {
// Try to list files in external storage to check permissions
await Filesystem . readdir ( {
path : "." ,
directory : Directory.External ,
// Check if we already have permissions
if ( this . permissionChecked && this . permissionGranted ) {
logger . log ( "External storage permissions already granted" , {
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
logger . log (
"External storage permissions already granted" ,
JSON . stringify ( { timestamp : new Date ( ) . toISOString ( ) } , null , 2 ) ,
) ;
return ;
}
// If a permission request is already in progress, wait for it
if ( this . permissionRequestLock ) {
logger . log ( "Permission request already in progress, waiting..." , {
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
await this . permissionRequestLock ;
return ;
}
// Create a new permission request lock
this . permissionRequestLock = this . _requestStoragePermissions ( ) ;
try {
await this . permissionRequestLock ;
this . permissionGranted = true ;
this . permissionChecked = true ;
logger . log ( "Storage permissions granted successfully" , {
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
} catch ( error ) {
const err = error as Error ;
const errorLogData = {
error : {
message : err.message ,
name : err.name ,
stack : err.stack ,
} ,
this . permissionGranted = false ;
this . permissionChecked = true ;
const errorMessage = error instanceof Error ? error.message : String ( error ) ;
logger . warn ( "Storage permissions denied, continuing with limited functionality" , {
error : errorMessage ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
} ) ;
// Don't throw error - allow app to continue with limited functionality
} finally {
this . permissionRequestLock = null ;
}
} catch ( error ) {
const errorMessage = error instanceof Error ? error.message : String ( error ) ;
logger . error ( "Error checking storage permissions:" , {
error : errorMessage ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
// Don't throw error - allow app to continue with limited functionality
}
}
// Check for actual permission errors
if (
err . message . includes ( "permission" ) ||
err . message . includes ( "access" ) ||
err . message . includes ( "denied" ) ||
err . message . includes ( "User denied storage permission" )
) {
logger . log (
"Permission check failed, requesting permissions" ,
JSON . stringify ( errorLogData , null , 2 ) ,
) ;
// The Filesystem plugin will automatically request permissions when needed
// We just need to try the operation again
try {
await Filesystem . readdir ( {
path : "." ,
directory : Directory.External ,
} ) ;
logger . log (
"External storage permissions granted after request" ,
JSON . stringify ( { timestamp : new Date ( ) . toISOString ( ) } , null , 2 ) ,
) ;
return ;
} catch ( retryError : unknown ) {
const retryErr = retryError as Error ;
// If permission is still denied, log it but don't throw an error
// This allows the app to continue with limited functionality
if ( retryErr . message . includes ( "User denied storage permission" ) ) {
logger . warn (
"External storage permissions denied by user, continuing with limited functionality" ,
JSON . stringify ( {
error : retryErr.message ,
timestamp : new Date ( ) . toISOString ( )
} , null , 2 ) ,
) ;
return ; // Don't throw error, just return
}
/ * *
* Internal method to request storage permissions
* @returns Promise that resolves when permissions are granted or denied
* /
private async _requestStoragePermissions ( ) : Promise < void > {
try {
// Try to read external storage to trigger permission request
await Filesystem . readdir ( {
path : "." ,
directory : Directory.ExternalStorage ,
} ) ;
throw new Error (
` Failed to obtain external storage permissions: ${ retryErr . message } ` ,
) ;
}
}
logger . log ( "External storage permissions already granted" , {
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
} catch ( error ) {
const errorMessage = error instanceof Error ? error.message : String ( error ) ;
// For any other error, log it but don't treat as permission error
logger . log (
"Unexpected error during permission check, proceeding anyway" ,
JSON . stringify ( errorLogData , null , 2 ) ,
) ;
return ;
// Check if this is a permission denial
if ( errorMessage . includes ( "user denied permission" ) ||
errorMessage . includes ( "permission request" ) ) {
logger . warn ( "User denied storage permission" , {
error : errorMessage ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
throw new Error ( "Storage permission denied by user" ) ;
}
} catch ( error ) {
logger . error ( "Error in checkStoragePermissions:" , error ) ;
// For other errors, log but don't treat as permission denial
logger . warn ( "Storage access error (not permission-related):" , {
error : errorMessage ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
throw error ;
}
}
@ -1966,213 +1975,172 @@ export class CapacitorPlatformService implements PlatformService {
}
/ * *
* Enhanced file discovery that searches multiple user - accessible locations
* This helps find files regardless of where users chose to save them
* Lists user - accessible files with enhanced discovery from multiple storage locations .
* This method tries multiple directories and handles permission denials gracefully .
* @returns Promise resolving to array of file information
* /
async listUserAccessibleFilesEnhanced ( ) : Promise < Array < { name : string , uri : string , size ? : number , path ? : string } > > {
const allFiles : Array < { name : string , uri : string , size? : number , path? : string } > = [ ] ;
try {
const allFiles : Array < { name : string , uri : string , size? : number , path? : string } > = [ ] ;
// Check permissions once at the beginning
const hasPermissions = await this . hasStoragePermissions ( ) ;
if ( this . getCapabilities ( ) . isIOS ) {
// iOS: Documents directory
const result = await Filesystem . readdir ( {
path : "." ,
directory : Directory.Documents ,
} ) ;
const files = result . files . map ( ( file ) = > ( {
name : typeof file === "string" ? file : file.name ,
uri : ` file:// ${ file . uri || file } ` ,
size : typeof file === "string" ? undefined : file . size ,
path : "Documents"
} ) ) ;
allFiles . push ( . . . files ) ;
} else {
// Android: Multiple locations with recursive search
// 1. App's external storage directory
// iOS: List files in Documents directory (persistent, accessible via Files app)
try {
const appSto rageR esult = await Filesystem . readdir ( {
path : "TimeSafari " ,
directory : Directory.ExternalStorage ,
const result = await Filesystem . readdir ( {
path : "." ,
directory : Directory.Documents ,
} ) ;
// Log full readdir output for TimeSafari
logger . log ( "[CapacitorPlatformService] Android TimeSafari readdir full result:" , {
path : "TimeSafari" ,
directory : "ExternalStorage" ,
files : appStorageResult.files ,
fileCount : appStorageResult.files.length ,
fileDetails : appStorageResult.files.map ( ( file , index ) = > ( {
index ,
name : typeof file === "string" ? file : file.name ,
type : typeof file === "string" ? "string" : "object" ,
hasUri : typeof file === "string" ? false : ! ! file . uri ,
hasSize : typeof file === "string" ? false : ! ! file . size ,
fullObject : file
} ) ) ,
const files = result . files
. filter ( ( file ) = > typeof file === "object" && file . type === "file" )
. map ( ( file ) = > {
const fileObj = file as any ;
return {
name : fileObj.name ,
uri : fileObj.uri ,
size : fileObj.size ,
path : "Documents" ,
} ;
} ) ;
allFiles . push ( . . . files ) ;
logger . log ( "[CapacitorPlatformService] iOS Documents files found:" , {
fileCount : files.length ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
const appStorageFiles = appStorageResult . files . map ( ( file ) = > ( {
name : typeof file === "string" ? file : file.name ,
uri : ` file:// ${ file . uri || file } ` ,
size : typeof file === "string" ? undefined : file . size ,
path : "TimeSafari"
} ) ) ;
allFiles . push ( . . . appStorageFiles ) ;
} catch ( error ) {
const err = error as Error ;
if ( err . message . includes ( "User denied storage permission" ) ) {
logger . warn ( "[CapacitorPlatformService] Storage permission denied for TimeSafari, skipping" ) ;
} else {
logger . warn ( "[CapacitorPlatformService] Could not read TimeSafari external storage:" , error ) ;
}
const errorMessage = error instanceof Error ? error.message : String ( error ) ;
logger . warn ( "[CapacitorPlatformService] Could not read iOS Documents:" , {
error : errorMessage ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
}
// 2. Common user-chosen locations (if accessible) with recursive search
const commonPaths = [
"Download" ,
"Documents" ,
"Backups" ,
"TimeSafari" ,
"Data"
] ;
for ( const path of commonPaths ) {
} else {
// Android: Try multiple storage locations
if ( hasPermissions ) {
// 1. App's external storage directory
try {
const result = await Filesystem . readdir ( {
path : path ,
const appStorageResult = await Filesystem . readdir ( {
path : "TimeSafari" ,
directory : Directory.ExternalStorage ,
} ) ;
// Log full readdir output for debugging
logger . log ( ` [CapacitorPlatformService] Android ${ path } readdir full result: ` , {
path : path ,
directory : "ExternalStorage" ,
files : result.files ,
fileCount : result.files.length ,
fileDetails : result.files.map ( ( file , index ) = > ( {
index ,
name : typeof file === "string" ? file : file.name ,
type : typeof file === "string" ? "string" : "object" ,
hasUri : typeof file === "string" ? false : ! ! file . uri ,
hasSize : typeof file === "string" ? false : ! ! file . size ,
fullObject : file
} ) ) ,
const appFiles = appStorageResult . files
. filter ( ( file ) = > typeof file === "object" && file . type === "file" )
. map ( ( file ) = > {
const fileObj = file as any ;
return {
name : fileObj.name ,
uri : fileObj.uri ,
size : fileObj.size ,
path : "TimeSafari" ,
} ;
} ) ;
allFiles . push ( . . . appFiles ) ;
logger . log ( "[CapacitorPlatformService] Android TimeSafari files found:" , {
fileCount : appFiles.length ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
} catch ( error ) {
const errorMessage = error instanceof Error ? error.message : String ( error ) ;
logger . warn ( "[CapacitorPlatformService] Could not read TimeSafari external storage:" , {
error : errorMessage ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
}
// Process each entry (file or directory)
const relevantFiles = [ ] ;
for ( const file of result . files ) {
const fileName = typeof file === "string" ? file : file.name ;
const name = fileName . toLowerCase ( ) ;
// Check if it's a directory by trying to get file stats
let isDirectory = false ;
try {
const stat = await Filesystem . stat ( {
path : ` ${ path } / ${ fileName } ` ,
directory : Directory.ExternalStorage
} ) ;
isDirectory = stat . type === 'directory' ;
} catch ( statError ) {
// If stat fails, assume it's a file
isDirectory = false ;
}
if ( isDirectory ) {
// RECURSIVELY SEARCH DIRECTORY for backup files
logger . log ( ` [CapacitorPlatformService] Recursively searching directory: ${ fileName } in ${ path } ` ) ;
try {
const subDirResult = await Filesystem . readdir ( {
path : ` ${ path } / ${ fileName } ` ,
directory : Directory.ExternalStorage ,
} ) ;
// Process files in subdirectory
for ( const subFile of subDirResult . files ) {
const subFileName = typeof subFile === "string" ? subFile : subFile.name ;
const subName = subFileName . toLowerCase ( ) ;
// Check if subfile matches backup criteria
const matchesBackupCriteria = subName . includes ( 'timesafari' ) ||
subName . includes ( 'backup' ) ||
subName . includes ( 'contacts' ) ||
subName . endsWith ( '.json' ) ;
if ( matchesBackupCriteria ) {
relevantFiles . push ( {
name : subFileName ,
uri : ` file:// ${ subFile . uri || subFile } ` ,
size : typeof subFile === "string" ? undefined : subFile . size ,
path : ` ${ path } / ${ fileName } `
} ) ;
logger . log ( ` [CapacitorPlatformService] Found backup file in subdirectory: ${ subFileName } in ${ path } / ${ fileName } ` ) ;
}
}
} catch ( subDirError ) {
const subDirErr = subDirError as Error ;
if ( subDirErr . message . includes ( "User denied storage permission" ) ) {
logger . warn ( ` [CapacitorPlatformService] Storage permission denied for subdirectory ${ path } / ${ fileName } , skipping ` ) ;
} else {
logger . warn ( ` [CapacitorPlatformService] Could not read subdirectory ${ path } / ${ fileName } : ` , subDirError ) ;
}
}
} else {
// Check if file matches backup criteria
const matchesBackupCriteria = name . includes ( 'timesafari' ) ||
name . includes ( 'backup' ) ||
name . includes ( 'contacts' ) ||
name . endsWith ( '.json' ) ;
if ( matchesBackupCriteria ) {
relevantFiles . push ( {
name : fileName ,
uri : ` file:// ${ file . uri || file } ` ,
size : typeof file === "string" ? undefined : file . size ,
path : path
} ) ;
} else {
logger . log ( ` [CapacitorPlatformService] Excluding non-backup file: ${ fileName } in ${ path } ` ) ;
}
}
}
// 2. Downloads/TimeSafari directory
try {
const downloadsResult = await Filesystem . readdir ( {
path : "Download/TimeSafari" ,
directory : Directory.ExternalStorage ,
} ) ;
if ( relevantFiles . length > 0 ) {
logger . log ( ` [CapacitorPlatformService] Found ${ relevantFiles . length } relevant files in ${ path } : ` , {
files : relevantFiles.map ( f = > f . name ) ,
timestamp : new Date ( ) . toISOString ( ) ,
const downloadFiles = downloadsResult . files
. filter ( ( file ) = > typeof file === "object" && file . type === "file" )
. map ( ( file ) = > {
const fileObj = file as any ;
return {
name : fileObj.name ,
uri : fileObj.uri ,
size : fileObj.size ,
path : "Download/TimeSafari" ,
} ;
} ) ;
allFiles . push ( . . . relevantFiles ) ;
}
} catch ( pathError ) {
const pathErr = pathError as Error ;
if ( pathErr . message . includes ( "User denied storage permission" ) ) {
logger . warn ( ` [CapacitorPlatformService] Storage permission denied for ${ path } , skipping ` ) ;
} else {
logger . warn ( ` [CapacitorPlatformService] Could not read ${ path } : ` , pathError ) ;
}
allFiles . push ( . . . downloadFiles ) ;
logger . log ( "[CapacitorPlatformService] Android Downloads/TimeSafari files found:" , {
fileCount : downloadFiles.length ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
} catch ( error ) {
const errorMessage = error instanceof Error ? error.message : String ( error ) ;
logger . warn ( "[CapacitorPlatformService] Could not read Downloads/TimeSafari:" , {
error : errorMessage ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
}
} else {
logger . log ( "[CapacitorPlatformService] Storage permissions not available, skipping external storage" , {
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
}
}
// Remove duplicates based on filename and path
const uniqueFiles = allFiles . filter ( ( file , index , self ) = >
index === self . findIndex ( f = > f . name === file . name && f . path === file . path )
) ;
// Always try app data directory as fallback
try {
const dataResult = await Filesystem . readdir ( {
path : "." ,
directory : Directory.Data ,
} ) ;
const dataFiles = dataResult . files
. filter ( ( file ) = > typeof file === "object" && file . type === "file" )
. map ( ( file ) = > {
const fileObj = file as any ;
return {
name : fileObj.name ,
uri : fileObj.uri ,
size : fileObj.size ,
path : "Data" ,
} ;
} ) ;
allFiles . push ( . . . dataFiles ) ;
logger . log ( "[CapacitorPlatformService] App data directory files found:" , {
fileCount : dataFiles.length ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
} catch ( error ) {
const errorMessage = error instanceof Error ? error.message : String ( error ) ;
logger . warn ( "[CapacitorPlatformService] Could not read app data directory:" , {
error : errorMessage ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
}
logger . log ( "[CapacitorPlatformService] Enhanced file discovery results:" , {
total : uniqueFiles.length ,
files : uniqueFiles.map ( f = > ( { name : f.name , path : f.path } ) ) ,
totalFiles : allFiles.length ,
hasPermissions ,
platform : this.getCapabilities ( ) . isIOS ? "iOS" : "Android" ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
return uniqueFiles ;
return all Files;
} catch ( error ) {
logger . error ( "[CapacitorPlatformService] Enhanced file discovery failed:" , error ) ;
const errorMessage = error instanceof Error ? error.message : String ( error ) ;
logger . error ( "[CapacitorPlatformService] Error in enhanced file discovery:" , {
error : errorMessage ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
return [ ] ;
}
}
@ -2605,4 +2573,47 @@ export class CapacitorPlatformService implements PlatformService {
// Android 10 (API 29) and above have stricter storage restrictions
return androidVersion !== null && androidVersion >= 29 ;
}
/ * *
* Resets the permission state ( useful for testing or when permissions change )
* /
private resetPermissionState ( ) : void {
this . permissionGranted = false ;
this . permissionChecked = false ;
this . permissionRequestLock = null ;
logger . log ( "[CapacitorPlatformService] Permission state reset" , {
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
}
/ * *
* Checks if storage permissions are available without requesting them
* @returns Promise resolving to true if permissions are available
* /
private async hasStoragePermissions ( ) : Promise < boolean > {
if ( this . permissionChecked ) {
return this . permissionGranted ;
}
// If not checked yet, check without requesting
try {
await Filesystem . readdir ( {
path : "." ,
directory : Directory.ExternalStorage ,
} ) ;
this . permissionGranted = true ;
this . permissionChecked = true ;
return true ;
} catch ( error ) {
const errorMessage = error instanceof Error ? error.message : String ( error ) ;
if ( errorMessage . includes ( "user denied permission" ) ||
errorMessage . includes ( "permission request" ) ) {
this . permissionGranted = false ;
this . permissionChecked = true ;
return false ;
}
// For other errors, assume we don't have permissions
return false ;
}
}
}