Okay so I solved my problem, here’s the sol’n for those who might want this functionality.
Apologies for the length, wanted to make sure I had everything covered:
For Android:
I added the functionality by including a file manager plugin (and filepath resolver) - cordova-fileChooser and also cordova-filepath-resolver. Then at a button press, the file manager is launched - when the file is selected, the URI is sent to the function you want to execute ($cordovaFile.readAsText in my case):
$scope.importGpx = function(){
//fileChooser sends the content:///... uri to the success callback, fileSuccess
window.fileChooser.open(fileSuccess, fileFailure);
}
var fileSuccess = function(){
//'content:///...' uri converted into native uri
window.FilePath.resolveNativePath(uri, function(file){
//file is the native filepath of the selected file, not the content:///... filepath
.
do stuff with file...
.
e.g. $cordovaFile.readAsText(file)
}
}
For iOS:
Since iOS doesn’t have a file browser, I decided the best option was to add my app to the ‘Open In’ menu for a specific file type (*.gpx in my case, that can be opened from an email attachement or through the share menu in a cloud storage app). My goal was to parse these GPX files, so I wanted the app to be registered as a Viewer of GPX files, then copy the selected file into the directory where I was storing all my app files, then once moved to the application directory, I could parse them using cordova.
To do this, I first had to register my app as a viewer of the GPX filetype. To do this I had to add the following to my Info.plist:
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array>
<string>gpx_dox_hi</string>
</array>
<key>CFBundleTypeName</key>
<string>GPS Exchange Format (GPX)</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>com.topografix.gpx</string>
</array>
</dict>
</array>
This registeres the app as a Viewer of GPX files. I also had to register the app as an importer of GPX files by adding the following:
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.xml</string>
</array>
<key>UTTypeDescription</key>
<string>GPS Exchange Format (GPX)</string>
<key>UTTypeIdentifier</key>
<string>com.topographix.gpx</string>
<key>UTTypeReferenceURL</key>
<string>http://www.topografix.com/GPX/1/1</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>gpx</string>
<string>GPX</string>
</array>
<key>public.mime-type</key>
<string>application/gpx+xml</string>
</dict>
</dict>
</array>
The relevant UTImportedTypeDeclarations and CFBundleDocumentTypes can be found on forums for many common file formats that are not inlcuded on Apple’s predefined list of UTIs. You can also declare your own custom filetypes using this method.
The app will then appear in the ‘Open In’ menu. If the app is selected to open the file, the app will then be launched using different parameters. To handle this, find CDVAppDelegate.m
in CordovaLib public classes in Xcode and edit the - (BOOL)application:(UIApplication*)application openURL:(NSURL*)url sourceApplication:(NSString*)sourceApplication annotation:(id)annotation
method to include the following:
Replace:
if(!url){
return NO;
}
with:
if (!url) {
return NO;
} else {
//url is the URI of the file
//This reads the contents of the file to a string.
NSString *str = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:nil];
//Get /Library/NoCloud directory (where all my app data is stored)
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *libraryDirectory = [paths objectAtIndex:0];
NSString *appendage = @"/NoCloud";
NSString *nocloudDirectory = [libraryDirectory stringByAppendingString:appendage];
//Write to new file
NSError *error;
BOOL succeed = [str writeToFile:[nocloudDirectory stringByAppendingPathComponent:@"myNewFile.txt"]
atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (!succeed){
NSLog(@"File write error: %@", error);
}
}
And that’s it, using the above code, the text within the selected file will be copied into myNewFile.txt in the application directory. File extension checking on android can be done using Cordova.
Bonus - Similar functionality on Android
To get the same functionality on Android (open in app when selected in email client or file explorer, not launched from within the app), requires some adjustment of the Android Studio project. Firstly, you have to add an Intent Filter to the manifest that registers the app as a handler of the specific file type. I added a new Activity that would specifically handle the incoming files, such that a new activity has to be defined in the manifest: (note ‘FileHandlerActivity’, not ‘MainActivity’)
<activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale" android:label="@string/activity_name" android:launchMode="singleTop" android:name="FileHandlerActivity" android:screenOrientation="portrait" android:theme="@android:style/Theme.DeviceDefault.NoActionBar" android:windowSoftInputMode="adjustResize">
<intent-filter android:label="@string/launcher_name">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.gpx" />
<data android:host="*" />
</intent-filter>
</activity>
This registers the app to be on the ‘Open with’ menu when the file is selected from anywhere in the device. When the file is selected, the FileHandlerActivity will be launched. In here, create an onResume function (called whether app was dead or alive when file was tapped, as opposed to onCreate which is only called when app is dead)
In the onResume function, include the same functionality as the iOS version, then call the MainActivity to launch the app:
@Override
protected void onResume(){
super.onResume();
//Get file intent
intent = getIntent();
//intentConsumed (defined globally as 0) ensures importGpx is only called once, instead of every time the app resumes after a file has been selected
if(intentConsumed < 1) {
try {
importGpx(intent);
intentConsumed++;
} catch (IOException e) {
e.printStackTrace();
}
} else {
}
}
public void importGpx(Intent intent) throws IOException {
//action is the intent action defined in your Manifest.
String action = intent.getAction();
//Only act on the file if action is equal to the system defined Intent.ACTION_VIEW
if(action.compareTo(Intent.ACTION_VIEW) == 0){
//intent.getData() is the URI of the selected file
intentUri = intent.getData();
//Check permissions for API23+
int permissionCheck = ContextCompat.checkSelfPermission(this, "android.permission.WRITE_EXTERNAL_STORAGE");
//Permission check declined, permission popup
if(permissionCheck != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this,
new String[]{"android.permission.WRITE_EXTERNAL_STORAGE"},
0);
return;
} else {
//Permission check passed, do something with file URI
moveFile(intentUri);
//Start MainActivity to launch full app
Intent myIntent = new Intent(FileHandlerActivity.this, MainActivity.class);
FileHandlerActivity.this.startActivity(myIntent);
}
}
//Clear intent to make sure it's not called again on same file
intentUri = null;
intent.removeExtra("key");
//Kill FileHandlerActivity
finish();
return;
}
And that’s that. If anyone has problems with this, let me know and I’ll see if I can help.