3

I'm using this fileutils class when selecting a file from the device:

public class FileUtils {
private FileUtils() {
}

private static final String TAG = "FileUtils";
private static final boolean DEBUG = false;

private static boolean isExternalStorageDocument(Uri uri) {
    return "com.android.externalstorage.documents".equals(uri.getAuthority());
}

private static boolean isDownloadsDocument(Uri uri) {
    return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}

private static boolean isMediaDocument(Uri uri) {
    return "com.android.providers.media.documents".equals(uri.getAuthority());
}


private static boolean isGooglePhotosUri(Uri uri) {
    return "com.google.android.apps.photos.content".equals(uri.getAuthority());
}

private static String getDataColumn(Context context, Uri uri, String selection,
                                    String[] selectionArgs) {

    Cursor cursor = null;
    final String column = "_data";
    final String[] projection = {
            column
    };

    try {
        cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
                null);
        if (cursor != null && cursor.moveToFirst()) {
            if (DEBUG)
                DatabaseUtils.dumpCursor(cursor);

            final int column_index = cursor.getColumnIndexOrThrow(column);
            return cursor.getString(column_index);
        }
    } finally {
        if (cursor != null)
            cursor.close();
    }
    return null;
}

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static String getPath(final Context context, final Uri uri) {

    if (DEBUG)
        Log.d(TAG + " File -",
                "Authority: " + uri.getAuthority() +
                        ", Fragment: " + uri.getFragment() +
                        ", Port: " + uri.getPort() +
                        ", Query: " + uri.getQuery() +
                        ", Scheme: " + uri.getScheme() +
                        ", Host: " + uri.getHost() +
                        ", Segments: " + uri.getPathSegments().toString()
        );

    final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;

    // DocumentProvider
    if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {

        //// ExternalStorageProvider
        if (isExternalStorageDocument(uri)) {
            final String docId = DocumentsContract.getDocumentId(uri);
            final String[] split = docId.split(":");
            final String type = split[0];

            // This is for checking Main Memory
            if ("primary".equalsIgnoreCase(type)) {
                if (split.length > 1) {
                    return Environment.getExternalStorageDirectory() + "/" + split[1];
                } else {
                    return Environment.getExternalStorageDirectory() + "/";
                }
                // This is for checking SD Card
            } else {
                return "storage" + "/" + docId.replace(":", "/");
            }

        }
        // DownloadsProvider
        else if (isDownloadsDocument(uri)) {
            final String id = DocumentsContract.getDocumentId(uri);
            if (id.startsWith("raw:")) {
                String[] data = new String[2];
                data[0] = id.replaceFirst("raw:", "");
                data[1] = null;
                return data[0];
            }
            final Uri contentUri = ContentUris.withAppendedId(
                    Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

            return getDataColumn(context, contentUri, null, null);
        }
        // MediaProvider
        else if (isMediaDocument(uri)) {
            final String docId = DocumentsContract.getDocumentId(uri);
            final String[] split = docId.split(":");
            final String type = split[0];

            Uri contentUri = null;
            if ("image".equals(type)) {
                contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
            } else if ("video".equals(type)) {
                contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
            } else if ("audio".equals(type)) {
                contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
            }

            final String selection = "_id=?";
            final String[] selectionArgs = new String[]{
                    split[1]
            };

            return getDataColumn(context, contentUri, selection, selectionArgs);
        }
    }
    // MediaStore (and general)
    else if ("content".equalsIgnoreCase(uri.getScheme())) {

        // Return the remote address
        if (isGooglePhotosUri(uri))
            return uri.getLastPathSegment();

        return getDataColumn(context, uri, null, null);
    }
    // File
    else if ("file".equalsIgnoreCase(uri.getScheme())) {
        return uri.getPath();
    }

    return null;
}

I call this class in onActivityResult like this:

String sourcePath = FileUtils.getPath(this, data.getData());

For some reason I'm getting the following crash:

Caused by java.lang.IllegalArgumentException: Unknown URI: content://downloads/public_downloads/230
   at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:165)
   at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:135)
   at android.content.ContentProviderProxy.query(ContentProviderNative.java:418)
   at android.content.ContentResolver.query(ContentResolver.java:760)
   at android.content.ContentResolver.query(ContentResolver.java:710)
   at android.content.ContentResolver.query(ContentResolver.java:668)
   at com.HBiSoft.ProGolf.Utils.FileUtils.getDataColumn(FileUtils.java:50)
   at com.HBiSoft.ProGolf.Utils.FileUtils.getPath(FileUtils.java:116)
   at com.HBiSoft.ProGolf.MainActivity.onActivityResult(MainActivity.java:652)
   at android.app.Activity.dispatchActivityResult(Activity.java:7638)
   at android.app.ActivityThread.deliverResults(ActivityThread.java:4515)
   at android.app.ActivityThread.handleSendResult(ActivityThread.java:4563)
   at android.app.ActivityThread.-wrap21(Unknown Source)
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1779)
   at android.os.Handler.dispatchMessage(Handler.java:106)
   at android.os.Looper.loop(Looper.java:164)
   at android.app.ActivityThread.main(ActivityThread.java:7000)
   at java.lang.reflect.Method.invoke(Method.java)
   at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:441)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1408)

the crash only happens when selecting a file from the downloads folder.

Can someone please help me resolve this?

HB.
  • 4,116
  • 4
  • 29
  • 53
  • have you set up file provided? – karan Jan 19 '19 at 10:39
  • @KaranMer do you mean `FileProvider`? `FileProvider` is only necessary when sharing a file to another application, I'm not sharing the file, I'm selecting a file. – HB. Jan 19 '19 at 10:46
  • have you written your own file manager application? – karan Jan 19 '19 at 10:46
  • @KaranMer I'm not sure what you are asking, I select a file from my application and it crashes when the file is in the downloads directory. – HB. Jan 19 '19 at 10:49
  • on what android version you are testing? – karan Jan 19 '19 at 10:50
  • @KaranMer 8.1.0 Oreo – HB. Jan 19 '19 at 10:53
  • try testing on andorid 6 or lower, your code will work. you are picking file from another app that work as default filemanager app and not your own app, so to access file shared by that app you would need to create fileprovider, – karan Jan 19 '19 at 10:56
  • https://stackoverflow.com/questions/49221312/android-get-real-path-of-a-txt-file-selected-from-the-file-explorer https://stackoverflow.com/questions/48510584/onactivityresults-intent-getpath-doesnt-give-me-the-correct-filename https://stackoverflow.com/questions/35870825/getting-the-absolute-file-path-from-content-uri-for-searched-images -- in short, a `Uri` is not a file, and so stop treating it as one. – CommonsWare Jan 19 '19 at 13:00
  • @CommonsWare I managed to get it working, I uploaded a gist after doing some research on this topic. Please have a look here: https://gist.github.com/HBiSoft/15899990b8cd0723c3a894c1636550a8#file-fileutils-java-L90 – HB. Jan 20 '19 at 08:14
  • @CommonsWare By using the above it returns `storage/emulated/0/Download/filename.mp4` instead of `content://com.android.providers.downloads.documents/document/230` – HB. Jan 20 '19 at 08:30
  • There is no requirement for a `content` `Uri` to map to a file, and there are plenty of `Uri` values that will fail algorithms like you are attempting to implement. This includes not only things like `FileProvider` but manufacturer-implemented modifications to stock components like the downloads provider. All you are doing through this code is creating more compatibility problems. Use a `Uri` as a source of a stream, via `ContentResolver`, and nothing else. – CommonsWare Jan 20 '19 at 12:40
  • you found any solution? – Jaykishan Sewak Apr 19 '19 at 19:52
  • @Androidenthu. https://gist.github.com/HBiSoft/15899990b8cd0723c3a894c1636550a8 – HB. Apr 19 '19 at 19:54
  • Ok I am checking – Jaykishan Sewak Apr 19 '19 at 19:56
  • @HB It worked for me thanks , you made my day... – Jaykishan Sewak Apr 19 '19 at 20:19
  • @Androidenthu. Please read my answer below. – HB. Apr 20 '19 at 08:46
  • I had the same issue recently and it turns out avoiding getting file path, I found the path & name could be different for different Android version. Instead. using InputStream to get all file details such like name, size and content. Here's what I did: https://stackoverflow.com/questions/57576948/filechooser-selecting-a-word-file-downloaded-from-gmail-crashes-app/57629014#57629014. – Shawn Aug 23 '19 at 16:23

1 Answers1

3

I have created a gist that solves this issue, here is the link.


IMPORTANT!!

When you select a file from Google Drive or Dropbox then you will get the following error:

Caused by java.lang.IllegalArgumentException: column '_data' does not exist

To resolve this you actually have to make a temporary copy of the file you are trying select.

First you have to check if the content:// uri is from Google Drive, you can do this by doing the following:

public boolean isGoogleDrive(Uri uri) {
    return String.valueOf(uri).toLowerCase().contains("com.google.android.apps");
}

This will return true if it is a file from Google Drive. If it is, I will call a AsyncTask, as shown below:

//The data.getData() below refers to the uri you get in onActivityResult
if (isGoogleDrive(data.getData())) {
    DownloadAsyncTask asyntask = new DownloadAsyncTask(data.getData(), this);
    asyntask.execute();
    asyntask.callback = this;
}

In the AsyncTask we will use the Uri to open a InputStream and get the byte data back which we will use to make a copy of the file.

Here is the AsyncTask class (I've added comments to make it more understandable):

class DownloadAsyncTask extends AsyncTask<Uri, Void, String> {
    private Uri mUri;
    CallBackTask callback;
    Context mContext;
    private AlertDialog mdialog;

    DownloadAsyncTask(Uri uri, Context context) {
        this.mUri = uri;
        mContext = context;
    }

    // In the onPreExecute() I'm displaying a custom dialog, this is not necessary, but recommended for when the user selects a large file
    @Override
    protected void onPreExecute() {
        final AlertDialog.Builder mPro = new AlertDialog.Builder(new ContextThemeWrapper(mContext, R.style.myDialog));
        @SuppressLint("InflateParams") 
        //Get reference to dialog layout
        final View mPView = LayoutInflater.from(mContext).inflate(R.layout.dialog, null);
        //Get reference to dialog title
        final TextView title = mPView.findViewById(R.id.txtTitle);
        //Get reference to dialog description
        final TextView desc = mPView.findViewById(R.id.txtDesc);

        //Set title text
        title.setText("Please wait..");
        //Set description text
        desc.setText("Drive files needs to be imported, this might take some time depending on the file size.");

        mPro.setView(mPView);
        mdialog = mPro.create();
        mdialog.show();

    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override
    protected String doInBackground(Uri... params) {
        //This will be the file we will use (the one that will be copied)
        File file = null;
        try {
            //Create a temporary folder where the copy will be saved to
            File temp_folder = mContext.getExternalFilesDir("TempFolder");

            //Use ContentResolver to get the name of the original name
            //Create a cursor and pass the Uri to it
            Cursor cursor = mContext.getContentResolver().query(mUri, null, null, null, null);
            //Check that the cursor is not null
            assert cursor != null;
            int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
            cursor.moveToFirst();
            //Get the file name
            String filename = cursor.getString(nameIndex);
            //Close the cursor
            cursor.close();

            //open a InputStream by passing it the Uri
            //We have to do this in a try/catch
            InputStream is = null;
            try {
                is = mContext.getContentResolver().openInputStream(mUri);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }

            //We now have a folder and a file name, now we can create a file
            file = new File(temp_folder + "/" + filename);

            //We can now use a BufferedInputStream to pass the InputStream we opened above to it
            BufferedInputStream bis = new BufferedInputStream(is);
            //We will write the byte data to the FileOutputStream, but we first have to create it
            FileOutputStream fos = new FileOutputStream(file);

            byte data[] = new byte[1024];
            long total = 0;
            int count;
            //Below we will read all the byte data and write it to the FileOutputStream
            while ((count = bis.read(data)) != -1) {
                total += count;
                fos.write(data, 0, count);
            }
            //The FileOutputStream is done and the file is created and we can clean and close it
            fos.flush();
            fos.close();

        } catch (IOException e) {
            Log.e("IOException = ", String.valueOf(e));
        }

        //Finally we can pass the path of the file we have copied
        return file.getAbsolutePath();


    }

    protected void onPostExecute(String result) {
        //We are done and can cancel the dialog
        if (mdialog != null && mdialog.isShowing()) {
            mdialog.cancel();
        }
        //I'm using a callback to let my Activity know that the AsyncTask is done. I pass the path along.
        callback.getResultFromAsynTask(result);


    }
}

You will see that in the onPostExecute() above I have callback.getResultFromAsynTask(result);. As I mentioned in the comment, I'm using a callback method to let the Activity know that I'm done and I pass the path to the callback.

In you Activity you have to implement the callback, like this:

public class MainActivity extends AppCompatActivity implements CallBackTask {

and the CallBackTask will look like this:

interface CallBackTask {
    void getResultFromAsynTask(String result);
 }

now you have to implement it in your Activity to get the result in your Activity:

@Override
public void getResultFromAsynTask(String result) {
    // Do what you need with the result like starting your new Activity and passing the path
    final Intent intent = new Intent();
    intent.setClass(MainActivity.this, Player.class);
    intent.putExtra("path", result);
    startActivity(intent);
}

GREAT, you now have a Uri from a File://(instead of content://) that you have temporarily copied. But, don't forget to delete the file when you are done with it, otherwise your application size will keep growing bigger.

Below, I will delete the TempFolder we created earlier, I will be doing this in the Activities onBackPressed (this should be done in onDestroy() as well):

@Override
public void onBackPressed() {
    super.onBackPressed();
    File dir = getBaseContext().getExternalFilesDir("TempFolder");
    deleteRecursive(dir);
}

void deleteRecursive(File fileOrDirectory) {
    if (fileOrDirectory.isDirectory())
        for (File child : fileOrDirectory.listFiles())
            deleteRecursive(child);

    fileOrDirectory.delete();
}

This is a lot read BUT... By doing it this way, you will NEVER have issues with any content:// Uri's.

HB.
  • 4,116
  • 4
  • 29
  • 53