1

I have looked at many, many SO posts to find an answer, but no luck...

How do I send a file from public storage as an e-mail attachment?

Most posts are asking about internal storage files. So for that, they use FileProvider (or something like that). In the docs for FileProvider, I read:

FileProvider ... facilitates secure sharing of files associated with an app...

But the file I want to send is not associated with my app. It is a .csv file in public storage in the Documents folder. So it seems really bizarre for my app to set up a FileProvider with access permissions for a public file. Even more strange to use my package name as the "authority".

I tried the following code, which I put together based on answers in 1 or 2 other posts:

File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
File f = new File(path, _sFileName);
_sFileDir = f.getCanonicalPath();

and in another method I have:

Intent i = new Intent(android.content.Intent.ACTION_SEND);
String Email[] = { "my.name@gmail.com" };
i.putExtra(android.content.Intent.EXTRA_EMAIL, Email);
i.putExtra(android.content.Intent.EXTRA_SUBJECT, "My Report");
i.setType("plain/text");
String sURI = "file:/" + _sFileDir;
i.putExtra(Intent.EXTRA_STREAM, Uri.parse(sURI));
startActivity(i);

This causes the app to crash with the message: "MyApp has stopped"

When I changed "file:/" to "content:/" the app no longer crashed, and the email was sent, but no file was attached.

My AndroidManifest.xml contains the following permissions:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

Here is my LogCat from the time when I use "content:/":

07-28 17:19:22.528 27809-27809/? I/art: Late-enabling -Xcheck:jni
07-28 17:19:22.569 27809-27816/? E/art: Failed sending reply to debugger: Broken pipe
07-28 17:19:22.569 27809-27816/? I/art: Debugger is no longer active
07-28 17:19:22.569 27809-27816/? I/art: Starting a blocking GC Instrumentation
07-28 17:19:22.582 27809-27809/? W/ActivityThread: Application com.myorg.myapp is waiting for the debugger on port 8100...
07-28 17:19:22.583 27809-27809/? I/System.out: Sending WAIT chunk
07-28 17:19:24.636 27809-27816/com.myorg.myapp I/art: Debugger is active
07-28 17:19:24.785 27809-27809/com.myorg.myapp I/System.out: Debugger has connected
07-28 17:19:24.785 27809-27809/com.myorg.myapp I/System.out: waiting for debugger to settle...
07-28 17:19:24.985 27809-27809/com.myorg.myapp I/System.out: waiting for debugger to settle...
07-28 17:19:25.186 27809-27809/com.myorg.myapp I/System.out: waiting for debugger to settle...
07-28 17:19:25.386 27809-27809/com.myorg.myapp I/System.out: waiting for debugger to settle...
07-28 17:19:25.588 27809-27809/com.myorg.myapp I/System.out: waiting for debugger to settle...
07-28 17:19:25.788 27809-27809/com.myorg.myapp I/System.out: waiting for debugger to settle...
07-28 17:19:25.989 27809-27809/com.myorg.myapp I/System.out: waiting for debugger to settle...
07-28 17:19:26.189 27809-27809/com.myorg.myapp I/System.out: debugger has settled (1454)
07-28 17:19:26.408 27809-27809/com.myorg.myapp W/System: ClassLoader referenced unknown path: /data/app/com.myorg.myapp-1/lib/arm
07-28 17:19:26.444 27809-27809/com.myorg.myapp I/InstantRun: starting instant run server: is main process
07-28 17:19:26.508 27809-27816/com.myorg.myapp I/art: Starting a blocking GC Instrumentation
07-28 17:19:26.801 27809-27809/com.myorg.myapp W/art: Before Android 4.1, method android.graphics.PorterDuffColorFilter android.support.graphics.drawable.VectorDrawableCompat.updateTintFilter(android.graphics.PorterDuffColorFilter, android.content.res.ColorStateList, android.graphics.PorterDuff$Mode) would have incorrectly overridden the package-private method in android.graphics.drawable.Drawable
07-28 17:19:28.471 27809-27814/com.myorg.myapp I/art: Do partial code cache collection, code=14KB, data=23KB
07-28 17:19:28.471 27809-27814/com.myorg.myapp I/art: After code cache collection, code=14KB, data=23KB
07-28 17:19:28.471 27809-27814/com.myorg.myapp I/art: Increasing code cache capacity to 128KB
07-28 17:19:28.475 27809-27814/com.myorg.myapp I/art: Do partial code cache collection, code=14KB, data=42KB
07-28 17:19:28.476 27809-27814/com.myorg.myapp I/art: After code cache collection, code=14KB, data=42KB
07-28 17:19:28.476 27809-27814/com.myorg.myapp I/art: Increasing code cache capacity to 256KB
07-28 17:19:29.036 27809-27814/com.myorg.myapp I/art: Do full code cache collection, code=119KB, data=108KB
07-28 17:19:29.037 27809-27814/com.myorg.myapp I/art: After code cache collection, code=113KB, data=86KB
07-28 17:19:29.216 27809-27895/com.myorg.myapp I/Adreno: QUALCOMM build                   : 7d18700, I8ee426a9a2
                                                                       Build Date                       : 10/07/16
                                                                       OpenGL ES Shader Compiler Version: XE031.09.00.03
                                                                       Local Branch                     : mybranch22308589
                                                                       Remote Branch                    : quic/LA.BR.1.3.6_rb1.6
                                                                       Remote Branch                    : NONE
                                                                       Reconstruct Branch               : NOTHING
07-28 17:19:29.229 27809-27895/com.myorg.myapp I/OpenGLRenderer: Initialized EGL, version 1.4
07-28 17:19:29.229 27809-27895/com.myorg.myapp D/OpenGLRenderer: Swap behavior 1
07-28 17:19:29.388 27809-27814/com.myorg.myapp I/art: Do partial code cache collection, code=125KB, data=105KB
07-28 17:19:29.388 27809-27814/com.myorg.myapp I/art: After code cache collection, code=125KB, data=105KB
07-28 17:19:29.388 27809-27814/com.myorg.myapp I/art: Increasing code cache capacity to 512KB
07-28 17:19:29.691 27809-27809/com.myorg.myapp W/art: Before Android 4.1, method int android.support.v7.widget.ListViewCompat.lookForSelectablePosition(int, boolean) would have incorrectly overridden the package-private method in android.widget.ListView
07-28 17:19:29.701 27809-27809/com.myorg.myapp I/Choreographer: Skipped 97 frames!  The application may be doing too much work on its main thread.
07-28 17:19:34.479 27809-27809/com.myorg.myapp I/Choreographer: Skipped 124 frames!  The application may be doing too much work on its main thread.
07-28 17:19:37.417 27809-27809/com.myorg.myapp W/IInputConnectionWrapper: reportFullscreenMode on inexistent InputConnection
07-28 17:19:37.417 27809-27809/com.myorg.myapp W/IInputConnectionWrapper: finishComposingText on inactive InputConnection
07-28 17:19:41.841 27809-27814/com.myorg.myapp I/art: Do full code cache collection, code=252KB, data=246KB
07-28 17:19:41.844 27809-27814/com.myorg.myapp I/art: After code cache collection, code=248KB, data=216KB
07-28 17:19:45.257 27809-27809/com.myorg.myapp I/Choreographer: Skipped 204 frames!  The application may be doing too much work on its main thread.
07-28 17:19:49.125 27809-27809/com.myorg.myapp W/IInputConnectionWrapper: reportFullscreenMode on inexistent InputConnection
07-28 17:19:49.126 27809-27809/com.myorg.myapp W/IInputConnectionWrapper: finishComposingText on inactive InputConnection
07-28 17:19:57.498 27809-27814/com.myorg.myapp I/art: Do partial code cache collection, code=250KB, data=227KB
07-28 17:19:57.499 27809-27814/com.myorg.myapp I/art: After code cache collection, code=250KB, data=227KB
07-28 17:19:57.499 27809-27814/com.myorg.myapp I/art: Increasing code cache capacity to 1024KB
07-28 17:20:12.117 27809-27809/com.myorg.myapp W/IInputConnectionWrapper: reportFullscreenMode on inexistent InputConnection
07-28 17:20:12.117 27809-27809/com.myorg.myapp W/IInputConnectionWrapper: finishComposingText on inactive InputConnection

Here's another LogCat when "file:/" is used & the app crashes:

07-28 17:43:57.190 30651-30651/com.myorg.myapp D/AndroidRuntime: Shutting down VM
07-28 17:43:57.232 30651-30651/com.myorg.myapp E/AndroidRuntime: FATAL EXCEPTION: main
 Process: com.myorg.myapp, PID: 30651
 java.lang.IllegalStateException: Could not execute method for android:onClick
    at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:293)
    at android.view.View.performClick(View.java:5612)
    at android.view.View$PerformClick.run(View.java:22285)
    at android.os.Handler.handleCallback(Handler.java:751)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:154)
    at android.app.ActivityThread.main(ActivityThread.java:6123)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757)
  Caused by: java.lang.reflect.InvocationTargetException
    at java.lang.reflect.Method.invoke(Native Method)
    at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288)
    at android.view.View.performClick(View.java:5612) 
    at android.view.View$PerformClick.run(View.java:22285) 
    at android.os.Handler.handleCallback(Handler.java:751) 
    at android.os.Handler.dispatchMessage(Handler.java:95) 
    at android.os.Looper.loop(Looper.java:154) 
    at android.app.ActivityThread.main(ActivityThread.java:6123) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757) 
  Caused by: android.os.FileUriExposedException: file://storage/emulated/0/Documents/Tasks.csv exposed beyond app through ClipData.Item.getUri()
    at android.os.StrictMode.onFileUriExposed(StrictMode.java:1813)
    at android.net.Uri.checkFileUriExposed(Uri.java:2360)
    at android.content.ClipData.prepareToLeaveProcess(ClipData.java:832)
    at android.content.Intent.prepareToLeaveProcess(Intent.java:8957)
    at android.content.Intent.prepareToLeaveProcess(Intent.java:8942)
    at android.app.Instrumentation.execStartActivity(Instrumentation.java:1583)
    at android.app.Activity.startActivityForResult(Activity.java:4228)
    at android.support.v4.app.BaseFragmentActivityJB.startActivityForResult(BaseFragmentActivityJB.java:50)
    at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:79)
    at android.app.Activity.startActivityForResult(Activity.java:4187)
    at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:859)
    at android.app.Activity.startActivity(Activity.java:4515)
    at android.app.Activity.startActivity(Activity.java:4483)
    at com.myorg.myapp.MainActivity.onClickSend(ProgSummary.java:209)
    at java.lang.reflect.Method.invoke(Native Method) 
    at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288) 
    at android.view.View.performClick(View.java:5612) 
    at android.view.View$PerformClick.run(View.java:22285) 
    at android.os.Handler.handleCallback(Handler.java:751) 
    at android.os.Handler.dispatchMessage(Handler.java:95) 
    at android.os.Looper.loop(Looper.java:154) 
    at android.app.ActivityThread.main(ActivityThread.java:6123) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757)

What is the right way to do this?

New Information:

When I used "content:/", this time I saw a message (Toast?) flash briefly on the Gmail app, saying "Can't attach empty file". But it's not empty!!!

More New Information:

I tried using FileProvider to do this. Here's the code...

AndroidManifest.xml:

<provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.myorg.myapp.fileprovider"
        android:grantUriPermissions="true"
        android:exported="false">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/filepaths" />
    </provider>

filepaths.xml:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path name="publicdocuments/" path="documents/" />
</paths>

MainActivity.java:

public void onClickSend(View v) {
    Intent i = new Intent(android.content.Intent.ACTION_SEND);
    String Email[] = { "my.address@gmail.com" };
    i.putExtra(android.content.Intent.EXTRA_EMAIL, Email);
    i.putExtra(android.content.Intent.EXTRA_SUBJECT, "Report");
    i.setType("text/plain");
    File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
    File f = new File(path, "file.csv");
    Uri sUri = FileProvider.getUriForFile(MainActivity.this, "com.myorg.myapp.fileprovider", f);
    i.setData(sUri);
    i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

    startActivity(i);
}

The problem now is that it doesn't work - my app crashes again on the call to FileProvider.getUriForFile().

Here's the new LogCat:

  --------- beginning of crash
07-29 13:45:27.116 10397-10397/com.myorg.myapp E/AndroidRuntime: FATAL EXCEPTION: main
  Process: com.myorg.myapp, PID: 10397
  java.lang.IllegalStateException: Could not execute method for android:onClick
      at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:293)
      at android.view.View.performClick(View.java:5612)
      at android.view.View$PerformClick.run(View.java:22285)
      at android.os.Handler.handleCallback(Handler.java:751)
      at android.os.Handler.dispatchMessage(Handler.java:95)
      at android.os.Looper.loop(Looper.java:154)
      at android.app.ActivityThread.main(ActivityThread.java:6123)
      at java.lang.reflect.Method.invoke(Native Method)
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867)
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757)
   Caused by: java.lang.reflect.InvocationTargetException
      at java.lang.reflect.Method.invoke(Native Method)
      at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288)
      at android.view.View.performClick(View.java:5612) 
      at android.view.View$PerformClick.run(View.java:22285) 
      at android.os.Handler.handleCallback(Handler.java:751) 
      at android.os.Handler.dispatchMessage(Handler.java:95) 
      at android.os.Looper.loop(Looper.java:154) 
      at android.app.ActivityThread.main(ActivityThread.java:6123) 
      at java.lang.reflect.Method.invoke(Native Method) 
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867) 
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757) 
   Caused by: java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/0/Documents/Tasks.csv
      at android.support.v4.content.FileProvider$SimplePathStrategy.getUriForFile(FileProvider.java:711)
      at android.support.v4.content.FileProvider.getUriForFile(FileProvider.java:400)
      at com.myorg.myapp.MainActivity.onClickSend(MainActivity.java:214)
      at java.lang.reflect.Method.invoke(Native Method) 
      at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288) 
      at android.view.View.performClick(View.java:5612) 
      at android.view.View$PerformClick.run(View.java:22285) 
      at android.os.Handler.handleCallback(Handler.java:751) 
      at android.os.Handler.dispatchMessage(Handler.java:95) 
      at android.os.Looper.loop(Looper.java:154) 
      at android.app.ActivityThread.main(ActivityThread.java:6123) 
      at java.lang.reflect.Method.invoke(Native Method) 
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867) 
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757)

As you can see, there is an InvocationTargetException, but I don't know why...

Additional Information: Replacing MainActivity.this with v.getContext() in FileProvider.getUriForFile() gives the same result.

Todd Hoatson
  • 123
  • 2
  • 18
  • Take a look at the LogCat Stacktrace – creativecreatorormaybenot Jul 28 '17 at 21:13
  • I think you forgot an uses permission for reading external storage.. and check if you have permission for storage enabled in application settings.. – mmmatey Jul 28 '17 at 21:23
  • Thanks, @mmmatey, I have updated my code (above) to include the permissions. – Todd Hoatson Jul 28 '17 at 22:08
  • Ok, so far so good, can you paste also log cat here.. Or try to make file from your current sURI, and check on file if actually exists? "new File(sUri).exists" .. if returns false, file is not here, or path is incorrect or permissions are not allowed application to use, operate with this file.. – mmmatey Jul 28 '17 at 22:14
  • And also try to replace, getCanonicalPath(), using only getPath().. – mmmatey Jul 28 '17 at 22:17
  • Don't see anything helpful in the LogCat, @ creativecreatorormaybenot... Do you want me to post it for you? – Todd Hoatson Jul 28 '17 at 22:22
  • _... try to make file from your current sURI, and check on file if actually exists? "new File(sUri).exists" .. if returns false, file is not here, or path is incorrect or permissions are not allowed application to use, operate with this file_ After the first code snippet (above), my app opens the file (successfully) and reads all of its data. So no problem for my app to access the file, @mmmatey. – Todd Hoatson Jul 28 '17 at 22:31

1 Answers1

0

So it seems really bizarre for my app to set up a FileProvider with access permissions for a public file

The app that you are trying to send it through may not have external storage access. Hence, FileProvider is still the recommended pattern.

This causes the app to crash with the message: "MyApp has stopped"

Use LogCat to examine the Java stack trace associated with the crash in whatever app "MyApp" is.

Also, please note that plain/text is not a valid MIME type. Presumably you mean text/plain.

And, given a File, use Uri.fromFile() to generate a Uri for it. Bear in mind that your app may crash on Android 7.0+ for using a file scheme, though, which is another reason for using FileProvider.

What is the right way to do this?

Use FileProvider.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • _The app that you are trying to send it through may not have external storage access._ I don't believe Gmail should have that problem... – Todd Hoatson Jul 29 '17 at 00:38
  • _Also, please note that plain/text is not a valid MIME type. Presumably you mean text/plain._ Hmmm... odd - I got that from an example in another post. Anyway, I've changed it, as you suggest. – Todd Hoatson Jul 29 '17 at 00:40
  • _Use FileProvider._ Actually, I started doing that before I posted this question, but I stopped and backed out my changes because I couldn't see my way forward. So, in my manifest file I should have: `android:authorities="com.myorg.myapp.fileprovider"` or what? Again, should my app have authority over a file created by Excel on someone's laptop...? – Todd Hoatson Jul 29 '17 at 00:42
  • @ToddHoatson: "Authority" is an IETF standard term. Don't read too much into it. – CommonsWare Jul 29 '17 at 00:48
  • The Android Developers docs for `FileProvider` states: "The element must contain one or more of the following child elements: " I assume that I need to use external-path... right? – Todd Hoatson Jul 29 '17 at 00:49
  • The docs state that name refers to "A URI path segment. To enforce security, this value hides the name of the subdirectory you're sharing." and it shows the following example: `` Then it states this will result in the following URI: `content://com.mydomain.fileprovider/my_images/default_image.jpg`. Sorry, but I fail to see how replacing images with my_images is enforcing security. And all of this is complete overkill for a file in public storage... – Todd Hoatson Jul 29 '17 at 00:57
  • @ToddHoatson: "I assume that I need to use external-path... right?" -- correct. – CommonsWare Jul 29 '17 at 10:30