1

I made custom Documents provider implementation where files are actually stored on a server.

I followed the documentation so the provider works most of the time, but I have issues with certain applications, namely:

1) Gmail attaching a file from my provider: Originally my public ParcelFileDescriptor openDocument was something like this:

ParcelFileDescriptor[] pipe=null;
pipe=ParcelFileDescriptor.createPipe();
OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]);
new TransferThread(file_id,out).start();
return pipe[0];

but this never worked with Gmail I got error broken pipe. It works if I first download file in the same method (blocking).

Original worked with most of the other applications

2) Blackberry Hub - I cannot get it working in anyway as it seems to be calling the provider methods on the main thread

I would give up but I see the dropbox provider worked with all the apps including BB hub.

Even worse :) it seems that dropbox can show own UI while downloading for example.

Any idea how this can be done?

Please note I checked the Documentation and several examples many times but still cannot get the provider to work with all the apps, I know that some of the apps may do wrong usage but dropbox proves it is possible to meet all of the apps

user1643723
  • 4,109
  • 1
  • 24
  • 48

1 Answers1

1

Your Gmail issue most likely comes from ContentProvider lifecycle.

ContentProvider is actually just IPC Binder, very similar to ones, commonly returned from Service#onBind. But instead of binding to your app, clients obtain it indirectly via ContentResolver each time they make a request. There is usually no explicit unbinding, Android system caches the provider for a short time after each IPC request completes.

Unfortunately, hidden nature of implicit ContentProvider binding means that there is no way to immediately release a provider. Worse yet, there is no way to handle errors in buggy providers — if your ContentProvider crashes before returning Cursor or ParcelFileDescriptor, the calling app will immediately go down with you! Google obviously knows about that, so they created another API for interacting with mis-trusted third-party ContentProviders — ContentProviderClient. Note, that the ContentProviderClient contains both means to handle remote exceptions and process death and a way to explicitly close ContentProvider.

Now imagine the hypothetical Gmail ContentProvider workflow:

ParcelFileDescriptor fd = null;

try (ContentProviderClient c = resolver.acquireUnstableContentProviderClient(...)) {
    fd = c.openFile(...)
} catch (Exception ohThoseBuggyProviders) {
    ...
}

// here ContentProvider is already closed

if (fd != null) {
    // use the received descriptor to create email attachment
    ...
}

But what if your ContentProvider wants to live a bit longer to read the rest of file from server in background thread? Well, the system will likely kill your process anyway, because it does not know, that you want that. Your process dies, Gmail gets the "broken pipe" error.

This is why you should not create new threads or use ContentProvider#openPipeHelper (why does that method even exist?), just do all your work in the calling thread.


The answer to second part of your question also lies in ContentProvider internals. When your provider is called from the main thread of calling app, your code is not executed on the main thread of your process — it is executed in the Binder thread pool as usual. But to make programmers' lives easier, Android takes several steps to make that less obvious:

  1. The priority of your thread is set to priority of thread, that makes the call (including raising to UI priority, if you get called from UI thread).
  2. Android passes current Strict Mode settings (the stuff that makes apps crash when they do network on main thread) from the calling app to your thread. After the call completes, all Strict Mode violations are collected and written to Parcel sent back to the calling app.

Even if ContentProvider ops are executed in binder pool, they behave almost as if there were no bounds between processes — including bad-bad-bad stuff happening when someone tries to download files from UI thread.

You should be able to get rid of that obnoxious "help" by using android.os.StrictMode, but that won't do, if the files in question are too large (ANR may still happen in the calling process). Instead of downloading files to pipe return from openDocument a ParcelFileDescriptor for socket.

Why does Dropbox not suffer from this issue? Because Dropbox Core is written in C++, and Android Strict Mode is currently Java-only construct, it does not hook into into the native code. If you write to disk or download from network in the main thread using C library calls, your app won't receive any repercussions (besides ANR, which is triggered on the UI thread independently from Strict Mode).

user1643723
  • 4,109
  • 1
  • 24
  • 48
  • `"But what if your ContentProvider wants to live a bit longer to read the rest of file from server in background thread? Well, the system will likely kill your process anyway, because it does not know, that you want that. Your process dies"` so are you saying that you cannot use a `Cursor` returned by a `CP#query` method for a longer time because the process that hosts that CP will be killed? if not (it will not be killed), what is a difference between returned `Cursor` and `PFD`? – pskink Nov 23 '16 at 10:44
  • @pskink For one, Cursor from ContentProvider, is [an AIDL interface by itself](https://github.com/android/platform_frameworks_base/blob/4b1a8f46d6ec55796bf77fd8921a5a242a219278/core/java/android/database/CursorWindow.aidl). ParcelFileDescriptor is [slightly simpler](https://github.com/android/platform_frameworks_base/blob/4b1a8f46d6ec55796bf77fd8921a5a242a219278/core/java/android/os/ParcelFileDescriptor.java), AFAIK it contains nothing except native descriptor (and, optionally, another descriptor for passing metadata around). – user1643723 Nov 23 '16 at 10:59
  • @pskink I am not saying, that this is the only possible explanation, just the simplest one. In absence of guarantees/documentation from Android framework developers I am going to assume the worst. – user1643723 Nov 23 '16 at 11:01
  • `PFD` is not just simple `int` `FD`, see `Parcel#writeFileDescriptor` and `Parcel#readFileDescriptor` those methods are native ones and among passing `int` `FD` they do much more like duplicating `FD` from one process to another and making sure that the source process is not terminated when connection is active – pskink Nov 23 '16 at 11:20
  • @pskink No,no,no. Parcel doesn't "duplicate `FD` between processes". Binder does. `dup` system call have no IPC semantics in Linux. It just assigns additional integer number to the in-kernel descriptor. The only special cheese, that seems to happen in `writeFileDescriptor`, is [setting 'cokie' to true](https://android.googlesource.com/platform/frameworks/native/+/jb-dev/libs/binder/Parcel.cpp), but that doesn't have any have any connection to lifecycle of processes ­— there is no special case for pipes. Show me fragment of Binder code, that treats pipe fds in special way, then I'd believe you. – user1643723 Nov 23 '16 at 12:04
  • @pskink The actual transfer of descriptor happens during [Binder#transact](https://developer.android.com/reference/android/os/Binder.html#transact%28int,%20android.os.Parcel,%20android.os.Parcel,%20int%29) in Binder kernel driver. You can write descriptors to Parcel without transferring them, in that case you will just duplicate a single descriptor within your process. – user1643723 Nov 23 '16 at 12:17
  • ok but `transact` takes two `Parcel`s: in and out, they are used for calls like `Parcel#writeFileDescriptor` so it is not `Binder` but `Parcel` where the things happen, `Binder` just `marshal`s / `unmarshal`s them – pskink Nov 23 '16 at 12:20
  • 1
    @pskink descriptors can't *just* travel between processes. They are stored in kernel descriptor table. The only way to pass a descriptor to someone is to edit that table. AFAIK, there are 3 ways to ask kernel for that: domain sockets (irrelevant here), `fork` (also irrelevant) and Binder driver. Can you show me place, where Parcel calls into `/dev/binder`? Unless you can, please admit, that the time and place where the transfer happens is after returning from [Binder#execTransact](https://github.com/android/platform_frameworks_base/blob/4b1a8f46d6ec557/core/java/android/os/Binder.java#L554). – user1643723 Nov 23 '16 at 12:31
  • sorry i have no time in digging over several 1000s lines of c code where it happens, maybe its `Binder`, it does not matter really, what matters is that `PFD` is not an ordinary `int` which is just copied like `Parcel#writeInt(int val)` but the system takes special care about its content, so your statements: `"AFAIK it contains nothing except native descriptor (and, optionally, another descriptor for passing metadata around)"` and `"Well, the system will likely kill your process anyway, because it does not know, that you want that"` are not right – pskink Nov 23 '16 at 14:27