电子邮件来自内部存储

22

我在我的应用程序中按照android开发者指南将文件写入内部存储。然后稍后我想要将我写入内部存储的文件发送电子邮件。这是我的代码和我遇到的错误,请帮忙解决。

FileOutputStream fos = openFileOutput(xmlFilename, MODE_PRIVATE);
fos.write(xml.getBytes());
fos.close();
Intent intent = new Intent(android.content.Intent.ACTION_SEND);
intent.setType("text/plain");
...
Uri uri = Uri.fromFile(new File(xmlFilename));
intent.putExtra(android.content.Intent.EXTRA_STREAM, uri);
startActivity(Intent.createChooser(intent, "Send eMail.."));

而且出现了以下错误

文件附件路径必须指向 file://mnt/sdcard。忽略附件 file://...

7个回答

24

我认为你可能在安卓Gmail客户端中发现了一个bug(或者至少是不必要的限制)。我已经想出了解决办法,但它让我觉得太特定实现了,需要更多的工作才能移植:

首先,CommonsWare非常正确,需要将文件设置为全局可读:

fos = openFileOutput(xmlFilename, MODE_WORLD_READABLE);

接下来,我们需要解决 Gmail 坚持使用 /mnt/sdcard(或类似的实现)路径的问题:

Uri uri = Uri.fromFile(new File("/mnt/sdcard/../.."+getFilesDir()+"/"+xmlFilename));

至少在我修改过的Gingerbread设备上,这让我可以从私人存储中向自己发送Gmail附件,并在接收时使用预览按钮查看其内容。但我并不感到很“好”,因为不知道在另一个版本的Gmail或其他电子邮件客户端或将外部存储挂载在其他位置的手机上会发生什么。


请记住,这是一种直接克服特定目标程序作者奇怪期望的黑客方法。今天,应该考虑查看内容提供程序和其他答案中讨论的类似方法,它们在风格上更具“Android”特色,希望在效用上更加通用。 - Chris Stratton

13

最近我一直在苦恼这个问题,现在我想分享一下我找到的解决方案,使用支持库中的FileProvider。它是Content Provider的扩展,可以很好地解决这个问题,而且不需要太多额外的工作。

正如链接中所解释的那样,要激活Content Provider:在你的清单文件中写入:

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

元数据应该指示res/xml文件夹中的一个xml文件(我将其命名为file_paths.xml):

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path path="" name="document"/>
</paths>

当您使用内部文件夹时,路径为空,但是如果要使用更一般的位置(我们现在谈论的是内部存储路径),您应该使用其他路径。您编写的名称将用于内容提供程序提供给文件的URL。

现在,您可以通过简单地使用以下内容生成新的、全局可读的URL:

Uri contentUri = FileProvider.getUriForFile(context, "com.yourdomain.yourapp.fileprovider", file);

从res/xml/file_paths.xml元数据的路径中选择任意文件。

现在只需使用:

    Intent mailIntent = new Intent(Intent.ACTION_SEND);
    mailIntent.setType("message/rfc822");
    mailIntent.putExtra(Intent.EXTRA_EMAIL, recipients);

    mailIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
    mailIntent.putExtra(Intent.EXTRA_TEXT, body);
    mailIntent.putExtra(Intent.EXTRA_STREAM, contentUri);

    try {
        startActivity(Intent.createChooser(mailIntent, "Send email.."));
    } catch (android.content.ActivityNotFoundException ex) {
        Toast.makeText(this, R.string.Message_No_Email_Service, Toast.LENGTH_SHORT).show();
    }

当您将URL附加到文件时,无需授予权限,它会自动完成。

您不需要使文件MODE_WORLD_READABLE,此模式现已弃用,请使用MODE_PRIVATE。内容提供程序将为同一文件创建新的URL,其他应用程序可以访问该URL。

值得注意的是,我只在模拟器上测试过与Gmail的情况。


1
看起来运行得很好。这是我迄今为止遇到的(真的很烦人的)问题中最优雅的解决方案。 - Michael A.

7

Chris Stratton提出了很好的解决方法。然而,这种方法在很多设备上都无法成功。你不应该硬编码/mnt/sdcard路径。你最好计算它:

String sdCard = Environment.getExternalStorageDirectory().getAbsolutePath();
Uri uri = Uri.fromFile(new File(sdCard + 
          new String(new char[sdCard.replaceAll("[^/]", "").length()])
                    .replace("\0", "/..") + getFilesDir() + "/" + xmlFilename));

5
考虑到这里的建议:http://developer.android.com/reference/android/content/Context.html#MODE_WORLD_READABLE,自API 17以来,我们被鼓励使用ContentProviders等。 多亏了那个人及其帖子http://stephendnicholas.com/archives/974,我们有了解决方案:
public class CachedFileProvider extends ContentProvider {
public static final String AUTHORITY = "com.yourpackage.gmailattach.provider";
private UriMatcher uriMatcher;
@Override
public boolean onCreate() {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI(AUTHORITY, "*", 1);
    return true;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    switch (uriMatcher.match(uri)) {
        case 1:// If it returns 1 - then it matches the Uri defined in onCreate
            String fileLocation = AppCore.context().getCacheDir() + File.separator +     uri.getLastPathSegment();
            ParcelFileDescriptor pfd = ParcelFileDescriptor.open(new File(fileLocation),     ParcelFileDescriptor.MODE_READ_ONLY);
            return pfd;
        default:// Otherwise unrecognised Uri
            throw new FileNotFoundException("Unsupported uri: " + uri.toString());
    }
}
@Override public int update(Uri uri, ContentValues contentvalues, String s, String[] as) { return     0; }
@Override public int delete(Uri uri, String s, String[] as) { return 0; }
@Override public Uri insert(Uri uri, ContentValues contentvalues) { return null; }
@Override public String getType(Uri uri) { return null; }
@Override public Cursor query(Uri uri, String[] projection, String s, String[] as1, String s1) {     return null; }
}

在内部缓存中创建文件:
    File tempDir = getContext().getCacheDir();
    File tempFile = File.createTempFile("your_file", ".txt", tempDir);
    fout = new FileOutputStream(tempFile);
    fout.write(bytes);
    fout.close();

设置意图:

...
emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse("content://" + CachedFileProvider.AUTHORITY + "/" + tempFile.getName()));

在 AndroidManifest 文件中注册内容提供者:

<provider android:name="CachedFileProvider" android:authorities="com.yourpackage.gmailattach.provider"></provider>

1
不要忘记在提供程序中添加 android:grantUriPermissions="true" - Tim Malseed

4
File.setReadable(true, false);

对我很有帮助。


只要创建URI的文件对象被设置为可读,我就能够使用FileProvider通过Gmail从内部存储发送文件。 - joates
在 Android 上,我用 Gmail 可以工作,但是其他邮件应用程序(如 Mailbox)却不能。我认为 far.be 提供的内容提供者解决方案是最“正确”的方法。 - ToddH
内容提供者确实是Android架构中用于公开本来是私有文件的预期解决方案。 - Chris Stratton

1
错误相当具体:您应该使用外部存储中的文件才能进行附件。

3
MODE_PRIVATE更改为MODE_WORLD_READABLE。由于您没有编写电子邮件程序,因此它无法读取您的私人文件。 - CommonsWare
无法这样做,File() 构造函数不处理权限。而 Uri.fromFile() 不支持 FileReader()。 - Mr Jackson
停止从肚子里发射答案,而不是从头脑中思考。更改权限不会影响路径,请参考错误!我正在寻找一种将其引导到正确路径的方法。或者实现我所描绘的目标的另一种替代方式! - Mr Jackson
2
@Jackson先生:“手机内置32GB存储,因此没有外部存储”--您的设备有外部存储。 “外部存储”并不意味着“SD卡”,而是指“可以从主机PC访问”。 “File()构造函数不处理权限”--如果您遵循我之前评论中的第一句话的非常简单的说明(“或将MODE_PRIVATE更改为MODE_WORLD_READABLE”),它就不必这样做。 - CommonsWare
2
甚至尝试了get Uri uri = Uri.fromFile(FileStreamPath(xmlFilename)),但仍然不行。 - Mr Jackson
显示剩余2条评论

1
如果您要使用内部存储,请尝试使用确切的存储路径:
Uri uri = Uri.fromFile(new File(context.getFilesDir() + File.separator + xmlFilename));

或者在调试器中不断更改文件名,只需在每个文件上调用“new File(blah).exists()”即可快速查看文件的确切名称。
这也可能是特定于您的设备的实际设备实现问题。 您是否尝试使用其他设备/模拟器?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接