通过Seekable Pipe或Stream与另一个Android应用程序共享?

时间:2014-02-03 22:15:41

标签: android android-intent android-contentprovider

许多Intent行为(例如ACTION_VIEW)会Uri指向应执行操作的内容。如果内容由文件支持 - Uri是直接指向文件,还是指向提供文件的ContentProvidersee FileProvider) - 这通常有效。

在某些情况下,开发人员不希望将内容驻留在文件中以便与其他应用共享。一种常见的情况是加密:解密数据应驻留在RAM中,而不是磁盘上,以最大限度地降低某人获取该解密数据的风险。

我从RAM共享的经典解决方案是use ParcelFileDescriptor and createPipe()。但是,当响应ACTION_VIEW(或其他)的活动在该管道上获得InputStream时,与ContentProvider提供内容时获得的流相比,生成的流受到限制一份文件。例如,this sample app可以正常使用Adobe Reader并崩溃QuickOffice。

基于past related个问题,我的假设是createPipe()真正创建了一个管道,pipes are non-seekable。试图“倒带”或“快进”的客户因此会遇到问题。

我正在寻找一种可靠的解决方案,用于与第三方应用程序共享内存内容,以解决此限制。具体做法是:

  • 必须使用Uri语法,客户端应用程序(即ACTION_VIEW实施者)可能会尊重这种语法;涉及客户端应用程序不太可能识别的某些麻烦的解决方案(例如,通过额外的Intent传递某些内容)不符合条件

  • 要共享的数据无法作为共享的一部分写入文件(当然,客户端应用程序最终可能会将收到的字节保存到磁盘,但暂时忽略该风险)

  • 理想情况下,它不会让应用程序想要共享数据,从而导致ServerSocket或其他方面加剧安全风险

可能提出的建议包括:

  • 重新配置导致可搜索管道的createPipe()的某种方法

  • 使用基于套接字的FileDescriptor导致可搜索管道的一些方法

  • 某种RAM磁盘或其他类似于Android的其余部分的文件,但不是持久的

如果你愿意的话,一个关键的标准就是如果我能从RAM中获得QuickOffice可以阅读的PDF文件。

有什么建议吗?

谢谢!

4 个答案:

答案 0 :(得分:8)

您提出了非常困难的要求组合。

让我们看看你的解决方案的想法:

  

可能提出的建议包括:

     
      
  • 重新配置createPipe()导致可搜索管道的一些方法

  •   
  • 使用基于套接字的FileDescriptor导致可搜索管道的一些方法

  •   
  • 某种RAM磁盘或其他类似于Android的其余部分的文件,但不是持久的

  •   

第一个没有工作。这个问题是操作系统实现的管道原语从根本上说是不可寻找的。原因是支持需要操作系统缓冲整个管道内容的搜索内容" ......直到阅读结束。这是无法实现的......除非您对可以通过管道发送的数据量进行限制。

第二个也不会工作,原因几乎相同。操作系统级别的套接字不可搜索。

在一个层面上,最终的想法(一个RAM文件系统)可以工作,模拟Android OS支持这样的功能。 (毕竟,Ramfs文件是可以搜索的。)但是,文件流不是管道。特别是对于文件流和管道,文件结尾的行为是不同的。从读者的角度来看,文件流看起来像一个管道流,这将需要一些特殊的代码。 (问题类似于在日志文件上运行tail -f的问题...)


不幸的是,我不认为有任何其他方法来获取文件描述符,该文件描述符的行为类似于文件结尾的管道,并且也是可搜索的。 ...没有彻底修改操作系统。

如果您可以更改正在从流中读取的应用程序,则可以解决此问题。 fd需要通过QuickOffice读取和搜索这一事实(我假设您无法修改)这一事实排除了这一点。 (但是如果你可以改变应用程序,那么有办法让这项工作......)


顺便说一句,我认为您在Linux或Windows上遇到了这些要求的一些问题。它们不是特定于Java的。


<强>更新

对此有各种有趣的评论,我想在这里解决一些问题:

  1. OP已经解释了激发他的问题的用例。基本上,他想要一个数据通过&#34;频道&#34;在应用程序实际运行时,如果用户设备被盗(或被没收),应用程序之间不会容易受到攻击。

    这可以实现吗?

    • 理论上,没有。如果一个假设高度的技术复杂性(以及公众可能不知道的技术......)那么坏人&#34; &#34; channel&#34;可以进入操作系统并从共享内存中读取数据。保持活跃。

    • 我怀疑这种攻击(目前)在实践中是否可行。

    • 然而,即使我们假设&#34;频道&#34;没有写任何内容&#34; disc&#34; 仍然是内存中通道的痕迹:例如

      • 仍然挂载的RAMfs或仍处于活动状态的共享内存段,或

      • 以前的RAMfs /共享内存的残余。

      理论上,理论上可以检索这些数据,前提是“坏人”#34;不会关闭或重启设备。

  2. 有人建议ashmem可以在这种情况下使用:

    • 可以解决没有公共Java API的问题(例如,通过编写第三方API)

    • 真正的障碍是需要流API。根据&#34; ashmem&#34; docs,他们有一个类似文件的API。但我认为这只意味着它们符合&#34;文件描述符&#34;模型。这些FD可以从一个应用程序传递到另一个应用程序(通过fork / exec),并使用&#34; ioctl&#34;对他们进行操作。但没有迹象表明他们实施了#34;阅读&#34;和&#34;写&#34; ......更不用说&#34;寻找&#34;。

    • 现在,您可以在ashmem上实现读/写/可搜索流,在通道的两端使用本机和Java库。但是这两个应用程序都需要知道&#34;这个过程,可能是提供设置频道的命令行选项的水平。

    这些问题也适用于旧式shmem ...,但频道设置可能更难。

  3. 另一个可能的选择是使用RAM fs。

    • 这更容易实现。 RAMfs中的文件将表现为&#34; normal&#34;文件;当由应用程序打开时,您将获得可以读取,写入和搜索的文件描述符...取决于它是如何打开的。并且(我认为)您应该能够通过fork / exec为RAMfs文件传递可搜索的FD。

    • 问题是RAMfs需要安装&#34;由操作系统来使用它。安装时,另一个(特权)应用程序也可以打开和读取文件。当某些应用程序为RAMfs文件打开fds时,操作系统不会让你卸载RAMfs。

    • 有一种(假设的)方案可以部分缓解上述情况。

      1. 源应用程序创建并安装&#34;私有&#34; RAMFS。
      2. 源应用程序创建/打开文件以进行读/写,然后取消链接。
      3. 源应用程序使用打开的fd编写文件。
      4. 源应用程序分叉/执行接收器应用程序,传递fd。
      5. 接收器应用程序从(我认为)仍然可以搜索的fd中读取,根据需要进行搜索。
      6. 当源应用程序注意到(子)接收器应用程序进程已退出时,它会卸载并销毁RAMfs。
      7. 这不需要修改读取(接收器)应用程序。

        但是,第三个(特权)应用程序可能仍然可能进入RAMfs,在内存中找到未链接的文件,然后阅读它。

  4. 但是,重新审阅了上述所有内容后,最实用的解决方案仍然是修改读取(接收器)应用程序以将整个输入流读入byte[],然后在缓冲的数据上打开ByteArrayInputStream。核心应用程序可以随意查找和重置它。

答案 1 :(得分:4)

这不是您问题的一般解决方案,但在QuickOffice中打开PDF可以使用以下代码(基于您的示例)为我工作:

@Override
public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
    try {
        byte[] data = getData(uri);
        long size = data.length;
        ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
        new TransferThread(new ByteArrayInputStream(data), new AutoCloseOutputStream(pipe[1])).start();
        return new AssetFileDescriptor(pipe[0], 0, size);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
};

private byte[] getData(Uri uri) throws IOException {
    AssetManager assets = getContext().getResources().getAssets();
    InputStream is = assets.open(uri.getLastPathSegment());
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    copy(is, os);
    return os.toByteArray();
}

private void copy(InputStream in, OutputStream out) throws IOException {
    byte[] buf = new byte[1024];
    int len;
    while ((len = in.read(buf)) > 0) {
        out.write(buf, 0, len);
    }
    in.close();
    out.flush();
    out.close();
}

@Override
public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sort) {
    if (projection == null) {
        projection = new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
    }

    String[] cols = new String[projection.length];
    Object[] values = new Object[projection.length];
    int i = 0;
    for (String col : projection) {
        if (OpenableColumns.DISPLAY_NAME.equals(col)) {
            cols[i] = OpenableColumns.DISPLAY_NAME;
            values[i++] = url.getLastPathSegment();
        }
        else if (OpenableColumns.SIZE.equals(col)) {
            cols[i] = OpenableColumns.SIZE;
            values[i++] = AssetFileDescriptor.UNKNOWN_LENGTH;
        }
    }

    cols = copyOf(cols, i);
    values = copyOf(values, i);

    final MatrixCursor cursor = new MatrixCursor(cols, 1);
    cursor.addRow(values);
    return cursor;
}

private String[] copyOf(String[] original, int newLength) {
    final String[] result = new String[newLength];
    System.arraycopy(original, 0, result, 0, newLength);
    return result;
}

private Object[] copyOf(Object[] original, int newLength) {
    final Object[] result = new Object[newLength];
    System.arraycopy(original, 0, result, 0, newLength);
    return result;
}

答案 2 :(得分:2)

我相信您正在寻找API 26中添加的功能StorageManager.openProxyFileDescriptor。这将为您ParcelFileDescriptor的工作提供ContentProvider.openAssetFile。但是,您也可以获取其文件描述符并在文件I / O中使用它:new FileInputStream(fd.getFileDescriptor())

在函数描述中是:

当您要提供对磁盘上没有真实文件支持的大文件(例如文件或文件夹)的快速访问时,此功能很有用。 网络共享,云存储服务等。例如,您可以 回应ContentResolver#openFileDescriptor(android.net.Uri, 字符串)请求,方法是返回以此创建的ParcelFileDescriptor 方法,然后根据要求按需流式传输内容。另一个 一个有用的例子可能是您拥有一个加密文件 愿意按需解密,但要避免持久存在 明文版本。

它与ProxyFileDescriptorCallback一起使用,这是您提供I / O的功能,主要是从各种偏移量读取文件片段(或解密,从网络读取,生成等)。

正如我测试过的那样,它也非常适合通过content://方案进行视频播放,因为查找是高效的,没有像基于管道的方法那样按读取查找,但是Android确实会询问您的相关片段文件。

Android内部使用一些保险丝驱动程序在进程之间传输数据。

答案 3 :(得分:0)

我一直在试验@josias代码。我发现一些query(...)次调用带有_data的投影。包括该列的数据和设置实际长度意味着可以在更多应用程序中打开更多文件类型。即使不在传入投影中,也始终包括_data允许打开更多文件类型。

以下是我最终的结果:

private static final String[] PROJECTION = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE, "_data"};

@Override
public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sort) {

    byte[] data = getData(mSourcePath, url);

    final MatrixCursor cursor = new MatrixCursor(PROJECTION, 1);

    cursor.newRow()
        .add(url.getLastPathSegment())
        .add(data.length)
        .add(data);

    return cursor;
}