Import files into app?

Is it possible to import files into an Ionic app? For example, there are some iOS apps which will show up under ‘Open with…’ when a particular file type is selected and recognised. Apple show how to make your app appear in this list for specific file types, however, how can you then get your app to perform a function with that file?

A real world example: I would like to be able to import GPX files, read them as text, and then parse and manipulate them. The parsing is trivial, but opening the GPX file within Ionic is not. On Android, I assume you can just point to the file on the device using the built-in file explorer. How would you get similar functionality on iOS?

Thanks in advance.

Why doesn’t the standard Javascript fileReader.readAsText() work for you? Maybe I don’t understand iOS well enough, but I don’t yet see why GPX files are a special issue.

You can open files with ionic-native File component. I believe, but am not sure, that you can enumerate files and folders with this component as well.

@BradBurns @AaronSterling you’re both correct, the file can easily be read as text using the file plugin.
The problem i face is, for example, if a user wants to import the file into the app, one would have to direct the app to wherever the file is. On iOS this is normally done (i say ‘normally’ meaning other apps that use GPX files do this…) by opening the GPX file directly from an email attachment (since the iOS filesystem is locked down per app). I’m not sure how to achieve this using Ionic. I understand adding the file type to the Info.plist will make the Ionic app show on the ‘Open with…’ list on the device, however i am not sure how to then perform an action with that file/how the file is passed into the app.

On android, i imagine there is a way to locate the file using the built in file browser (from within the Ionic app), then feeding the path into the read-as-text/parse function, however this isn’t possible on iOS as far as I am aware.

Edit: i should mention, when i say importing the file, i mean from a location that the app doesn’t belong to. E.g. downloads folder on android (not sure of an iOS equivalent??)

I’ll be honest, I’m with you - I really don’t know if this is possible since, as you mention, the file system on iOS is highly locked down from cross-app file handling, etc. Sorry I can’t be of more help. :grimacing:

No problem! I know it’s possible with native apps, so I’ll have a play around in xcode and see if i can figure anything out! Seems it might take a bit of native/hybrid crossover.

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.