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

How to send a large file or multiple files to other apps, and know when to delete them?

背景

我有一个 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 文件。这就是我在想的:

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

  2. 将它们全部压缩到一个文件中,然后发送。压缩级别可以是最小的,因为 APK 文件已经被压缩了。

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

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

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

问题

除了我想到的解决方案,还有其他选择吗?有没有办法利用我以前的所有优势(快速且没有留下垃圾文件)来解决这个问题?

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

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

为该 Intent 注册的任何应用程序都应该能够处理具有相同文件名但不同路径的文件。为了能够应对其他应用提供的文件的访问只能在接收Activity为运行(参见Security Exception when trying to access a Picasa image on device running 4.2 or SecurityException when downloading Images with the Universal-Image-Downloader)时访问,接收应用需要将文件复制到他们可以永久访问的目录。我的猜测是一些应用程序没有实现复制过程来处理相同的文件名(复制时,所有文件的文件路径可能都是相同的)。

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

接收应用程序 "should" 接收文件大致如下:

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 should/will 工作而不是仅仅在 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 实现。

需要解决的问题有:

  1. 如何在 Uri 中包含文件路径以便在稍后阶段(在 ContentProvider 中)提取完全相同的路径?
  2. 如何在 ContentProvider 中为接收应用程序创建一个可以 return 的唯一文件名?对于多次调用 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 returns 的唯一文件名将是原始文件名 (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);

ContentProvider

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_-=] 根据此 ) are valid in an Uri path (0-9, a-z, A-Z, _-!.~'()*,;:$&+=/@ --> see https://developer.android.com/reference/java/net/URI.html

您的清单必须像这样定义 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" 是可选的,但应该使它更有效率。

这是使用 SymLinks 的有效解决方案。缺点:

  1. 从 API 14 开始工作,而不是在 API 10 开始工作,不确定两者之间的时间。
  2. 使用反射,因此将来可能无法在某些设备上使用。
  3. 必须在"getFilesDir"的路径中创建符号链接,因此您必须自己管理它们,并根据需要创建唯一的文件名。

示例共享当前应用的APK。

代码:

public class SymLinkActivity extends Activity{
  @Override
  protected void onCreate(Bundle savedInstanceState)
    {
    super.onCreate(savedInstanceState);
    setContentView(lb.com.myapplication2.R.layout.activity_main);
    final Intent intent=new Intent(Intent.ACTION_SEND_MULTIPLE);
    intent.setType(MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"));
    final String filePath;
    try
      {
      final android.content.pm.ApplicationInfo applicationInfo=getPackageManager().getApplicationInfo(getPackageName(),0);
      filePath=applicationInfo.sourceDir;
      }
    catch(NameNotFoundException e)
      {
      e.printStackTrace();
      finish();
      return;
      }
    final File file=new File(filePath);
    final String symcLinksFolderPath=getFilesDir().getAbsolutePath();
    findViewById(R.id.button).setOnClickListener(new android.view.View.OnClickListener(){
      @Override
      public void onClick(final android.view.View v)
        {
        final File symlink=new File(symcLinksFolderPath,"CustomizedNameOfApkFile-"+System.currentTimeMillis()+".apk");
        symlink.getParentFile().mkdirs();
        File[] oldSymLinks=new File(symcLinksFolderPath).listFiles();
        if(oldSymLinks!=null)
          {
          for(java.io.File child : oldSymLinks)
            if(child.getName().endsWith(".apk"))
              child.delete();
          }
        symlink.delete();
        // do some dirty reflection to create the symbolic link
        try
          {
          final Class<?> libcore=Class.forName("libcore.io.Libcore");
          final java.lang.reflect.Field fOs=libcore.getDeclaredField("os");
          fOs.setAccessible(true);
          final Object os=fOs.get(null);
          final java.lang.reflect.Method method=os.getClass().getMethod("symlink",String.class,String.class);
          method.invoke(os,file.getAbsolutePath(),symlink.getAbsolutePath());
          final ArrayList<Uri> uris=new ArrayList<>();
          uris.add(Uri.fromFile(symlink));
          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);
          startActivity(intent);
          android.widget.Toast.makeText(SymLinkActivity.this,"succeeded ?",android.widget.Toast.LENGTH_SHORT).show();
          }
        catch(Exception e)
          {
          android.widget.Toast.makeText(SymLinkActivity.this,"failed :(",android.widget.Toast.LENGTH_SHORT).show();
          e.printStackTrace();
          // TODO handle the exception
          }
        }
    });

    }
}

编辑:对于符号链接部分,对于 Android API 21 及以上,您可以使用它代替反射:

 Os.symlink(originalFilePath,symLinkFilePath);