如何将大文件或多个文件发送到其他应用程序,并知道何时删除它们?

and*_*per 13 android file android-intent apk

背景

我有一个App-Manager应用程序,允许将APK文件发送到其他应用程序.

直到Android 4.4(包括),我必须为此任务执行的操作是将路径发送到原始APK文件(所有这些都在"/ data/app/..."下,即使没有root也可以访问).

这是发送文件的代码(此处提供的文档):

intent=new Intent(Intent.ACTION_SEND_MULTIPLE);
intent.setType("*/*");
final ArrayList<Uri> uris=new ArrayList<>();
for(...)
   uris.add(Uri.fromFile(new File(...)));
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM,uris);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_NO_HISTORY|Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET|Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
Run Code Online (Sandbox Code Playgroud)

问题

我做了什么工作,因为所有应用程序的APK文件都有一个唯一的名称(这是他们的包名称).

自从Lollipop(5.0)以来,所有应用程序的APK文件都被命名为"base.APK",这使得其他应用程序无法理解附加它们.

这意味着我有一些选项来发送APK文件.这就是我在想的:

  1. 将它们全部复制到一个文件夹,将它们全部重命名为唯一的名称,然后发送它们.

  2. 将它们全部压缩到一个文件然后发送它.压缩级别可能很小,因为APK文件已经被压缩了.

问题是我必须尽快发送文件,如果我真的必须拥有这些临时文件(除非有其他解决方案),也要尽快处理它们.

事实是,当第三方应用程序处理完临时文件时,我没有得到通知,我也认为选择多个文件需要花费一些时间来准备,无论我选择什么.

另一个问题是某些应用程序(如Gmail)实际上禁止发送APK文件.

这个问题

有没有我想到的解决方案的替代方案?有没有办法解决这个问题与我以前的所有优势(快速和没有垃圾文件留下)?

也许某种方式来监控文件?或创建一个流而不是一个真正的文件?

将临时文件放在缓存文件夹中会有什么帮助吗?

Ema*_*lin 7

为该Intent注册的任何应用程序都应该能够处理具有相同文件名但路径不同的文件.能够应对只有在接收活动正在运行时才能访问其他应用程序提供的文件的事实(请参阅运行4.2 在设备上访问Picasa图像时的安全例外使用Universal下载图像时的SecurityException Image-Downloader)接收应用程序需要将文件复制到他们可以永久访问的目录中.我的猜测是有些应用程序没有实现复制过程来处理相同的文件名(复制时文件路径可能对所有文件都相同).

我建议通过ContentProvider而不是直接从文件系统提供文件.这样,您就可以为要发送的每个文件创建唯一的文件名.

接收应用"应该"或多或少地接收文件:

ContentResolver contentResolver = context.getContentResolver();
Cursor cursor = contentResolver.query(uri, new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }, null, null, null);
// retrieve name and size columns from the cursor...

InputStream in = contentResolver.openInputStream(uri);
// copy file from the InputStream
Run Code Online (Sandbox Code Playgroud)

由于应用程序应该使用contentResolver.openInputStream()打开文件,因此ContentProvider应该/将工作,而不是仅仅在Intent中传递文件uri.当然可能有应用程序行为不端,需要对其进行彻底测试,但如果某些应用程序无法处理ContentProvider提供的文件,您可以添加两种不同的共享选项(一种是遗留选项,另一种是常规选项).

对于ContentProvider部分,有以下内容:https: //developer.android.com/reference/android/support/v4/content/FileProvider.html

不幸的是还有这个:

FileProvider只能为您事先指定的目录中的文件生成内容URI

如果您可以在构建应用程序时定义要共享文件的所有目录,则FileProvider将是您的最佳选择.我假设你的应用程序想要从任何目录共享文件,所以你需要自己的ContentProvider实现.

要解决的问题是:

  1. 如何在Uri中包含文件路径以便在稍后阶段(在ContentProvider中)提取相同的路径?
  2. 如何创建一个可以在ContentProvider中返回到接收应用程序的唯一文件名?对于ContentProvider的多次调用,此唯一文件名必须相同,这意味着无论何时调用ContentProvider,您都无法创建唯一ID,或者每次调用时都会获得不同的ID.

问题1

ContentProvider Uri由一个方案(内容://),一个权限和一个路径段组成,例如:

内容://lb.com.myapplication2.fileprovider/123/base.apk

第一个问题有很多解决方案.我建议base64编码文件路径并将其用作Uri中的最后一个段:

Uri uri = Uri.parse("content://lb.com.myapplication2.fileprovider/" + new String(Base64.encode(filename.getBytes(), Base64.DEFAULT));
Run Code Online (Sandbox Code Playgroud)

如果文件路径是例如:

/data/data/com.google.android.gm/base.apk

然后由此产生的Uri将是:

内容://lb.com.myapplication2.fileprovider/L2RhdGEvZGF0YS9jb20uZ29vZ2xlLmFuZHJvaWQuZ20vYmFzZS5hcGs=

要在ContentProvider中检索文件路径,只需执行以下操作:

String lastSegment = uri.getLastPathSegment();
String filePath = new String(Base64.decode(lastSegment, Base64.DEFAULT) );
Run Code Online (Sandbox Code Playgroud)

问题2

解决方案非常简单.我们在创建Intent时生成的Uri中包含唯一标识符.此标识符是Uri的一部分,可以由ContentProvider提取:

String encodedFileName = new String(Base64.encode(filename.getBytes(), Base64.DEFAULT));
String uniqueId = UUID.randomUUID().toString();
Uri uri = Uri.parse("content://lb.com.myapplication2.fileprovider/" + uniqueId + "/" + encodedFileName );
Run Code Online (Sandbox Code Playgroud)

如果文件路径是例如:

/data/data/com.google.android.gm/base.apk

然后由此产生的Uri将是:

内容://lb.com.myapplication2.fileprovider/d2788038-53da-4e84-b10a-8d4ef95e8f5f/L2RhdGEvZGF0YS9jb20uZ29vZ2xlLmFuZHJvaWQuZ20vYmFzZS5hcGs=

要在ContentProvider中检索唯一标识符,只需执行以下操作:

List<String> segments = uri.getPathSegments();
String uniqueId = segments.size() > 0 ? segments.get(0) : "";
Run Code Online (Sandbox Code Playgroud)

ContentProvider返回的唯一文件名将是原始文件名(base.apk)加上在基本文件名后插入的唯一标识符.例如base.apk成为基础<唯一ID> .apk.

虽然这可能听起来很抽象,但应该用完整的代码清楚:

意图

intent=new Intent(Intent.ACTION_SEND_MULTIPLE);
intent.setType("*/*");
final ArrayList<Uri> uris=new ArrayList<>();
for(...)
    String encodedFileName = new String(Base64.encode(filename.getBytes(), Base64.DEFAULT));
    String uniqueId = UUID.randomUUID().toString();
    Uri uri = Uri.parse("content://lb.com.myapplication2.fileprovider/" + uniqueId + "/" + encodedFileName );
    uris.add(uri);
}
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM,uris);
Run Code Online (Sandbox Code Playgroud)

内容提供商

public class FileProvider extends ContentProvider {

    private static final String[] DEFAULT_PROJECTION = new String[] {
        MediaColumns.DATA,
        MediaColumns.DISPLAY_NAME,
        MediaColumns.SIZE,
    };

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public String getType(Uri uri) {
        String fileName = getFileName(uri);
        if (fileName == null) return null;
        return MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileName);
    }

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        String fileName = getFileName(uri);
        if (fileName == null) return null;
        File file = new File(fileName);
        return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        String fileName = getFileName(uri);
        if (fileName == null) return null;

        String[] columnNames = (projection == null) ? DEFAULT_PROJECTION : projection;
        MatrixCursor ret = new MatrixCursor(columnNames);
        Object[] values = new Object[columnNames.length];
        for (int i = 0, count = columnNames.length; i < count; i++) {
            String column = columnNames[i];
            if (MediaColumns.DATA.equals(column)) {
                values[i] = uri.toString();
            }
            else if (MediaColumns.DISPLAY_NAME.equals(column)) {
                values[i] = getUniqueName(uri);
            }
            else if (MediaColumns.SIZE.equals(column)) {
                File file = new File(fileName);
                values[i] = file.length();
            }
        }
        ret.addRow(values);
        return ret;
    }

    private String getFileName(Uri uri) {
        String path = uri.getLastPathSegment();
        return path != null ? new String(Base64.decode(path, Base64.DEFAULT)) : null;
    }

    private String getUniqueName(Uri uri) {
        String path = getFileName(uri);
        List<String> segments = uri.getPathSegments();
        if (segments.size() > 0 && path != null) {
            String baseName = FilenameUtils.getBaseName(path);
            String extension = FilenameUtils.getExtension(path);
            String uniqueId = segments.get(0);
            return baseName + uniqueId + "." + extension;
        }

        return null;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;       // not supported
    }

    @Override
    public int delete(Uri uri, String arg1, String[] arg2) {
        return 0;       // not supported
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;    // not supported
    }

}
Run Code Online (Sandbox Code Playgroud)

注意:

你的清单必须像这样定义ContentProvider:

<provider
    android:name="lb.com.myapplication2.fileprovider.FileProvider"
    android:authorities="lb.com.myapplication2.fileprovider"
    android:exported="true"
    android:grantUriPermissions="true"
    android:multiprocess="true"/>
Run Code Online (Sandbox Code Playgroud)

没有android:grantUriPermissions ="true"和android:exported ="true"它将无法工作,因为其他应用程序无权访问ContentProvider(另请参阅http://developer.android.com/guide/topics /manifest/provider-element.html#exported).android:multiprocess ="true"另一方面是可选的,但应该使它更有效率.