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

标签 android file android-intent apk

背景

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

直到 Android 4.4(包括),我必须为这个任务做的就是发送原始 APK 文件的路径(所有文件都在“/data/app/...”下,即使没有 root 也可以访问)。

这是发送文件的代码(文档可用 here):

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);

问题

我所做的工作是因为所有应用程序的 APK 文件都有一个唯一的名称(这是它们的包名称)。

从 Lollipop (5.0) 开始,所有应用程序的 APK 文件都被简单地命名为“base.APK”,这使得其他应用程序无法理解附加它们。

这意味着我有一些选项可以发送 APK 文件。这就是我的想法:
  • 将它们全部复制到一个文件夹中,将它们全部重命名为唯一名称,然后发送它们。
  • 将它们全部压缩为一个文件,然后发送。压缩级别可能很小,因为 APK 文件无论如何都已经被压缩了。

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

    问题是,当第三方应用程序完成处理临时文件时,我不会收到通知,而且我还认为,无论我选择什么,选择多个文件都需要相当长的时间来准备。

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

    问题

    有没有我想到的解决方案的替代方案?有没有办法利用我以前拥有的所有优点(快速且不留下垃圾文件)来解决这个问题?

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

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

    最佳答案

    为该 Intent 注册的任何应用程序都应该能够处理具有相同文件名但路径不同的文件。为了能够处理只能在接收 Activity 运行时访问其他应用程序提供的文件的事实(请参阅 Security Exception when trying to access a Picasa image on device running 4.2SecurityException when downloading Images with the Universal-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
    

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

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

    不幸的是,还有这个:

    A FileProvider can only generate a content URI for files in directories that you specify beforehand



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

    要解决的问题是:
  • 您如何在 Uri 中包含文件路径,以便在稍后阶段(在 ContentProvider 中)提取完全相同的路径?
  • 如何创建一个唯一的文件名,您可以在 ContentProvider 中将其返回给接收应用程序?对于对 ContentProvider 的多次调用,此唯一文件名需要相同,这意味着您无法在调用 ContentProvider 时创建唯一 ID,否则每次调用都会得到不同的 ID。

  • 问题 1

    ContentProvider Uri 由方案(content://)、权限和路径段组成,例如:

    content://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));
    

    如果文件路径是例如:

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



    那么由此产生的 Uri 将是:

    content://lb.com.myapplication2.fileprovider/L2RhdGEvZGF0YS9jb20uZ29vZ2xlLmFuZHJvaWQuZ20vYmFzZS5hcGs=



    要检索 ContentProvider 中的文件路径,只需执行以下操作:
    String lastSegment = uri.getLastPathSegment();
    String filePath = new String(Base64.decode(lastSegment, Base64.DEFAULT) );
    

    问题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 );
    

    如果文件路径是例如:

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



    那么由此产生的 Uri 将是:

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



    要在 ContentProvider 中检索唯一标识符,只需执行以下操作:
    List<String> segments = uri.getPathSegments();
    String uniqueId = segments.size() > 0 ? segments.get(0) : "";
    

    ContentProvider 返回的唯一文件名将是原始文件名 (base.apk) 加上在基本文件名之后插入的唯一标识符。例如base.apk 变成 base.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);
    

    内容提供商
    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
        }
    
    }
    

    注意:
  • 我的示例代码使用 org.apache.commons 库进行文件名操作 (FilenameUtils.getXYZ)
  • 对文件路径使用 base64 编码是一种有效的方法,因为 base64 中使用的所有字符([a-zA-Z0-9_-=] 根据此 https://stackoverflow.com/a/6102233/534471 )在 Uri 路径(0-9, az, AZ , _-!.~'()*,;:$&+=/@ --> 见 https://developer.android.com/reference/java/net/URI.html )

  • 您的 list 必须像这样定义 ContentProvider:
    <provider
        android:name="lb.com.myapplication2.fileprovider.FileProvider"
        android:authorities="lb.com.myapplication2.fileprovider"
        android:exported="true"
        android:grantUriPermissions="true"
        android:multiprocess="true"/>
    

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

    关于android - 如何将一个大文件或多个文件发送到其他应用程序,并知道何时删除它们?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29680755/

    相关文章:

    java - 如何反序列化 Array google-gson 中的 Array

    android - 当前上下文中不存在名称 'AccountPicker' - Xamarin.Android

    java - 如何在调用 Intent 时保持 MainActivity 运行?

    android - 停止执行互联网 - 在应用程序子类中

    java - 导入 com.google.android.gms.* 时出错;

    java - Firebase .orderByChild - 尝试对数字进行排序,但它只对第一个数字进行排序

    file - 通过UDP发送时文件大小不同

    php - $_FILES ['' ] ['size' ] - PHP 是否在服务器端检查文件大小?

    file - 提高Powershell性能以生成随机文件

    android - Intent Extras 的通用对值类型