Skip to content

Conversation

@npomfret
Copy link
Collaborator

No description provided.

@Elyx0
Copy link
Collaborator

Elyx0 commented Sep 2, 2016

If the way of returning data changed can you update the readme too ? Didn't have time to test yet

@vonovak
Copy link
Member

vonovak commented Sep 11, 2016

I implemented a similar module for android, came here to do a PR but I see there is one already.
Just wanted to say that this looks good, and @Elyx0 the interface for andorid and ios cannot be the same, so yes, the returned data will be different from ios.
@npomfret as of 0.33 there was a breaking change in onactivityresult, which is easy to fix. I'm just worried that the current implemetation will have permission issues if I try to e.g. download the selected file 'm somewhere else on android - i'm using acitivity.getContentResolver().takePersistableUriPermission. my 2 cents..

@npomfret
Copy link
Collaborator Author

Ok, I'll update it to work with rn 0.33 and update the docs. Maybe later today if I get time - thanks for checking it.

@npomfret
Copy link
Collaborator Author

@vonovak What did you mean:

I'm just worried that the current implemetation will have permission issues if I try to e.g. download the selected file 'm somewhere else on android

You you explain a bit more?

@Elyx0
Copy link
Collaborator

Elyx0 commented Sep 12, 2016

I added you as contributor as I don't have time at all to double check! Thanks again. I'll maybe have to update the npm still though, will check.

@npomfret
Copy link
Collaborator Author

Ok, thanks. I'll merge it tomorrow.

@vonovak
Copy link
Member

vonovak commented Sep 16, 2016

@npomfret so what I do is that I have a file selector same as the one you implemented and later I'm using the uri to download the file (if it is a remote file) busing context.getContentResolver().openInputStream(uri); and so I'm not sure if this is going to work with the current impl. I'll test it with your branch ad let you know.

Also, what would be nice is implement the android permissions in the js part of the module. There is a module in core for this as of 0.33. just do

import {PermissionsAndroid,} from 'react-native';

and
PermissionsAndroid.requestPermission(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
                title: 'rationale title',
                message: 'rationale message'
            })
                .then((response)=> {
                    if (response === true) {
                        showPicker()
                    }
                })

@npomfret npomfret closed this Sep 18, 2016
@npomfret
Copy link
Collaborator Author

npomfret commented Sep 18, 2016

I've merged to master - let incorporate your ideas before pushing a release. Did you mean something like this:

package com.reactnativedocumentpicker;

import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.OpenableColumns;
import android.util.Log;
import android.webkit.MimeTypeMap;

import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;

/**
 * @see <a href="https://developer.android.com/guide/topics/providers/document-provider.html">android documentation</a>
 */
public class DocumentPicker extends ReactContextBaseJavaModule implements ActivityEventListener {
    private static final String NAME = "RNDocumentPicker";
    private static final int READ_REQUEST_CODE = 41;

    private static class Fields {
        private static final String FILE_SIZE = "fileSize";
        private static final String FILE_NAME = "fileName";
        private static final String TYPE = "type";
    }

    private Callback callback;

    public DocumentPicker(ReactApplicationContext reactContext) {
        super(reactContext);
        reactContext.addActivityEventListener(this);
    }

    @Override
    public String getName() {
        return NAME;
    }

    @ReactMethod
    public void show(ReadableMap args, Callback callback) {
        Intent intent;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
            intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        } else {
            intent = new Intent(Intent.ACTION_PICK);
        }
        intent.addCategory(Intent.CATEGORY_OPENABLE);

        if (!args.isNull("filetype")) {
            ReadableArray filetypes = args.getArray("filetype");
            if (filetypes.size() > 0) {
                intent.setType(filetypes.getString(0));
            }
        }

        this.callback = callback;

        getReactApplicationContext().startActivityForResult(intent, READ_REQUEST_CODE, Bundle.EMPTY);
    }

    // removed @Override temporarily just to get it working on RN0.33 and RN0.32 - will remove
    public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
        onActivityResult(requestCode, resultCode, data);
    }

    // removed @Override temporarily just to get it working on RN0.33 and RN0.32
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode != READ_REQUEST_CODE)
            return;

        if (resultCode != Activity.RESULT_OK) {
            callback.invoke("Bad result code: " + resultCode, null);
            return;
        }

        if (data == null) {
            callback.invoke("No data", null);
            return;
        }

        try {
            Uri uri = data.getData();
            callback.invoke(null, toMapWithMetadata(uri));
        } catch (Exception e) {
            Log.e(NAME, "Failed to read", e);
            callback.invoke(e.getMessage(), null);
        }
    }

    private WritableMap toMapWithMetadata(Uri uri) {
        WritableMap map;
        if(uri.toString().startsWith("/")) {
            map = metaDataFromFile(new File(uri.toString()));
        } else if (uri.toString().startsWith("http")) {
            map = metaDataFromUri(uri);
        } else {
            map = metaDataFromContentResolver(uri);
        }

        map.putString("uri", uri.toString());

        return map;
    }

    private WritableMap metaDataFromUri(Uri uri) {
        WritableMap map = Arguments.createMap();

        File outputDir = getReactApplicationContext().getCacheDir();
        try {
            File downloaded = download(uri, outputDir);

            map.putInt(Fields.FILE_SIZE, (int) downloaded.length());
            map.putString(Fields.FILE_NAME, downloaded.getName());
            map.putString(Fields.TYPE, mimeTypeFromName(uri.toString()));
        } catch (IOException e) {
            Log.e("DocumentPicker", "Failed to download file", e);
        }

        return map;
    }

    private WritableMap metaDataFromFile(File file) {
        WritableMap map = Arguments.createMap();

        if(!file.exists())
            return map;

        map.putInt(Fields.FILE_SIZE, (int) file.length());
        map.putString(Fields.FILE_NAME, file.getName());
        map.putString(Fields.TYPE, mimeTypeFromName(file.getAbsolutePath()));

        return map;
    }

    private WritableMap metaDataFromContentResolver(Uri uri) {
        WritableMap map = Arguments.createMap();

        ContentResolver contentResolver = getReactApplicationContext().getContentResolver();

        map.putString(Fields.TYPE, contentResolver.getType(uri));

        Cursor cursor = contentResolver.query(uri, null, null, null, null, null);

        try {
            if (cursor != null && cursor.moveToFirst()) {

                map.putString(Fields.FILE_NAME, cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)));

                int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
                if (!cursor.isNull(sizeIndex)) {
                    String size = cursor.getString(sizeIndex);
                    if (size != null)
                        map.putInt(Fields.FILE_SIZE, Integer.valueOf(size));
                }
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        return map;
    }

    private static File download(Uri uri, File outputDir) throws IOException {
        File file = File.createTempFile("prefix", "extension", outputDir);

        URL url = new URL(uri.toString());

        ReadableByteChannel channel = Channels.newChannel(url.openStream());
        try{
            FileOutputStream stream = new FileOutputStream(file);

            try {
                stream.getChannel().transferFrom(channel, 0, Long.MAX_VALUE);
                return file;
            } finally {
                stream.close();
            }
        } finally {
            channel.close();
        }
    }

    private static String mimeTypeFromName(String absolutePath) {
        String extension = MimeTypeMap.getFileExtensionFromUrl(absolutePath);
        if (extension != null) {
            return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
        } else {
            return null;
        }
    }

    // Required for RN 0.30+ modules than implement ActivityEventListener
    public void onNewIntent(Intent intent) {
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants