Related
With the newer Android Q many things changed, especially with scoped storage and gradual deprecation of file:/// URIs. The problem is the lack of documentation on how to handle media files correctly on Android Q devices.
I have a media file (audio) management application and I could not find yet a reliable way to tell to the OS that I performed a change to a file so that it can update its MediaStore record.
Option #1: MediaScannerService
MediaScannerConnection.scanFile(context, new String[]{ filePath }, new String[]{"audio/*"}, new MediaScannerConnection.OnScanCompletedListener() {
#Override
public void onScanCompleted(String s, Uri uri) {
}
});
Works with file:// URIs from primary storage
Not works with file:// URIs from secondary storage (such as removable storage)
Not works with any content:// URI
Option #2: broadcast
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
Not working at all
Soon deprecated
Option #3: manual MediaStore insertion
AudioFileContentValues are some column values from MediaStore.Audio.AudioColumns.
Old method based on file:// URI:
Uri uri = MediaStore.Audio.Media.getContentUriForPath(file_path);
newUri = context.getContentResolver().insert(uri, AudioFileContentValues);
MediaStore.Audio.Media.getContentUriForPath is deprecated
Still not working
Newer method based on what I could put together from documentation:
Uri collection = MediaStore.Audio.Media.getContentUri(correctVolume);
newUri = context.getContentResolver().insert(collection, AudioFileContentValues);
Where correctVolume would be external from primary storage, while it would be something like 0000-0000 for secondary storage, depending on where the file is located.
Insertion returns a content URI such as content://media/external/audio/media/125 but then no record is persisted inside MediaStore for files located in primary storage
Insertion fails with no URI returned and no record in MediaStore
These are more or less all the methods available in previous Android versions but none of them now allow me to notify the system that I changed some audio file metadata and to get Android to update MediaStore records. Event though option #1 is partially working, this could never be a valuable solution because it's clearly not supporting content URIs.
Is there any reliable way to trigger media scan on Android Q, despite where the file is located? We shouldn't even care about file location, according to Google, since we will soon only use content URIs. MediaStore has always been a little frustrating in my opinion, but now the situation is pretty worse.
I'm also currently struggling with that.
I think what you want to do you cannot do any longer once you are on Android Q, because you are not allowed to access the Music directory on Q. You are only allowed to create and access files in directories you created. You did not create the music directory.
Now every change to the Media has to happen threw the MediaStore. So you insert your Music file beforehand and then get an outputstream from the MediaStore to write to it. All the changes on Q on Media should be done threw the MediaStore hence you informing the MediaStore of changes cannot even occur anymore, because you never directly access the File.
This has one giant caviat in that all the new things in MediaStore that make that possible do not exist in older versions of Android. So I do currently believe that you will need to implement everything twice, sadly. At least if you want to actively influences where your music is saved to that is.
Those two MediaStore columns are new in Q and do not exist before Q, witch you'll probably need to use in Q
MediaStore.Audio.Media.RELATIVE_PATH with that you can influence the path where it's saved. So I put "Music/MyAppName/MyLibraryName" there and that will end up saving "song.mp3" into "Music/MyAppName/MyLibraryName/song.mp3"
MediaStore.Audio.Media.IS_PENDING this you should be setting to 1 while the song is still being written and then afterwards you can update it to 0.
I've also now started to implement things twice with if checks for Android versions. It's annoying. I don't want to do it. But it seems like that's the only way.
I'm just gonna put a bit of code here on how I managed inserting music on Android.Q and below. It's not perfect. I have to specify the MIME type for Q, because flacs would now become .flac.mp3 somehow, because it does not quite seem to get that.
So, anyways this is a part that I have updated already to work with Q and before, it downloads a Music file from a music player on my NAS. The app is written in kotlin, not sure if that's a problem for you.
override fun execute(library : Library, remoteApi: RemoteApi, ctx: Context) : Boolean {
var success = false
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val values = ContentValues().apply {
put(MediaStore.Audio.Media.RELATIVE_PATH, library.rootFolderRelativePath)
put(MediaStore.Audio.Media.DISPLAY_NAME, remoteLibraryEntry.getFilename())
put(MediaStore.Audio.Media.IS_PENDING, 1)
}
val collection = MediaStore.Audio.Media
.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val uri = ctx.contentResolver.insert(collection, values)
ctx.contentResolver.openOutputStream(uri!!).use {
success = remoteApi.downloadMusic(remoteLibraryEntry, it!!)
}
if(success) {
values.clear()
val songId = JDrop.mediaHelper.getSongId(uri)
JDrop.db.music.insert(Music(mediaStoreId = songId, remoteId = remoteLibraryEntry.remoteId, libraryId = library.id))
values.put(MediaStore.Audio.Media.IS_PENDING, 0)
ctx.contentResolver.update(uri, values, null, null)
} else {
ctx.contentResolver.delete(uri, null, null)
}
} else {
val file = File("${library.rootFolderPublicDirectory}/${remoteLibraryEntry.getFilename()}")
if(file.exists()) file.delete()
success = remoteApi.downloadMusic(remoteLibraryEntry, file.outputStream())
if (success) {
MediaScannerConnection.scanFile(ctx, arrayOf(file.path), arrayOf("audio/*")) { _, uri ->
val songId = JDrop.mediaHelper.getSongId(uri)
JDrop.db.music.insert(Music(mediaStoreId = songId, remoteId = remoteLibraryEntry.remoteId, libraryId = library.id))
}
}
}
return success
}
And the MediaStoreHelper Method being this here
fun getSongId(uri : Uri) : Long {
val cursor = resolver.query(uri, arrayOf(Media._ID), null, null, null)
return if(cursor != null && cursor.moveToNext()) {
val idIndex = cursor.getColumnIndex(Media._ID)
val id = cursor.getLong(idIndex)
cursor.close()
id
} else {
cursor?.close()
-1
}
}
One thing when you do not specify the MIME type it seems to assume mp3 is the MIME type. So .flac files would get saved as name.flac.mp3, because it adds the mp3 file type if there is none and it thinks it's a mp3. It does not add another .mp3 for mp3 files. I don't currently have the MIME type anywhere... so I'm gonna go ahead and do this now, I guess.
There is also a helpful google IO talk about scoped/shared storage https://youtu.be/3EtBw5s9iRY
That probably won't answer all of your questions. It sure enough didn't for me. But it was a helpful start to have a rough idea what they even did change to begin with.
For deleting and updating files its kinda the same on Q if you call delete on a mediastore entry, the file will be deleted. Before, Q you have to manually delete the file also. But if you do that on Q your app will crash. So again you have to check wether or not youre on Q or an older version of android and take appropriate actions.
When the user clicks the "send file" button in google drive and selects my app. I want to get the filepath of that file and then allow the user to upload it to a different location.
I check these similar SO post for kitkat phones: Get real path from URI, Android KitKat new storage access framework
Android - Convert URI to file path on lollipop
However the solution to that no longer seems to work in Lollipop devices.
The problem seems to be that MediaStore.MediaColumns.DATA returns null when running a query on the ContentResolver.
https://code.google.com/p/android/issues/detail?id=63651
You should use ContentResolver.openFileDescriptor() instead of trying to get a raw filesystem path. The "_data" column is not part of the CATEGORY_OPENABLE contract, so Drive is not required to return it.
I've read this blog post by CommonsWare which suggest I "try using the Uri directly with ContentResolver" which I don't understand. How do I use the URI directly with ContentResolvers?
However, I'm still not clear on how best to approach these types of URIs.
The best solution i've been able to find is to call openFileDescriptor and then copy the filestream into a new file, then passing that new file path to my upload activity.
private static String getDriveFileAbsolutePath(Activity context, Uri uri) {
if (uri == null) return null;
ContentResolver resolver = context.getContentResolver();
FileInputStream input = null;
FileOutputStream output = null;
String outputFilePath = new File(context.getCacheDir(), fileName).getAbsolutePath();
try {
ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r");
FileDescriptor fd = pfd.getFileDescriptor();
input = new FileInputStream(fd);
output = new FileOutputStream(outputFilePath);
int read = 0;
byte[] bytes = new byte[4096];
while ((read = input.read(bytes)) != -1) {
output.write(bytes, 0, read);
}
return new File(outputFilePath).getAbsolutePath();
} catch (IOException ignored) {
// nothing we can do
} finally {
input.close();
output.close();
}
return "";
}
The only problem here is that I lose the filename of that file. This seems a bit over complicated just to get a filePath from drive. Is there a better way to do this?
thanks.
EDIT:
So i can use a normal query to get the filename. Then I can pass that into my getDriveAbsolutePath() method. Which will get me pretty close to what I want, the only problem now is that I'm missing file extensions. All searches I've done recommend using the filepath to get extensions, which I can't do with openFileDescriptor(). Any help?
String filename = "";
final String[] projection = {
MediaStore.MediaColumns.DISPLAY_NAME
};
ContentResolver cr = context.getApplicationContext().getContentResolver();
Cursor metaCursor = cr.query(uri, projection, null, null, null);
if (metaCursor != null) {
try {
if (metaCursor.moveToFirst()) {
filename = metaCursor.getString(0);
}
} finally {
metaCursor.close();
}
}
However, I'm not entirely convinced this is the "right" way to do this?
The only problem here is that I lose the filename of that file. This seems a bit over complicated just to get a filePath from drive. Is there a better way to do this?
You seem to miss an important point here. Files in Linux don't need to have a name. They may exist in memory (e.g. android.os.MemoryFile) or even reside in directory without having a name (such as files, created
with O_TMPFILE flag). What they do need to have is a file descriptor.
Short summary: file descriptors are better than simple files and should always be used instead, unless closing them after yourself is too much of burden. They can be employed for same things as File objects, and much more, if you can use JNI. They are made available by special ContentProvider and can be accessed via openFileDescriptor method of ContentResolver (which receives Uri, associated with target provider).
That said, just saying people, used to File objects, to replace them with descriptors sure sounds weird. Read an elaborate explanation below, if you feel like trying it out. If you don't, just skip to the bottom of the answer for "simple" solution.
EDIT: the answer below have been written before Lollipop became widespread. Nowadays there is a handy class for direct access to Linux system calls, that makes using JNI for working with file descriptors optional.
Quick briefing on descriptors
File descriptors come from Linux open system call and corresponding open() function in C library. You don't need to have access to file to operate on it's descriptor. Most access checks will simply be skipped, but some crucial information, such as access type (read/write/read-and-write etc.) is "hardcoded" into descriptor and can not be changed after it is created. File descriptors are represented by non-negative integer numbers, starting from 0. Those numbers are local to each process and don't have any persistent or system-wide meaning, they merely distinguish handles
to files from each other for given process (0, 1 and 2 traditionally reference stdin, stdout and stderr).
Each descriptor is represented by a reference to entry in descriptor table, stored in OS kernel. There are per-process and system-wide limits to number of entries in that table, so close your descriptors quickly, unless you want your attempts to open things and create new descriptors to suddenly fail.
Operating on descriptors
In Linux there are two kinds of C library functions and system calls: working with names (such as readdir(), stat(), chdir(), chown(), open(), link()) and operating on descriptors: getdents, fstat(), fchdir(), fchown(), fchownat(), openat(), linkat() etc. You can call these functions and system calls easily after a reading a couple of man pages and studying some dark JNI magic. That will raise quality of your software through the roof! (just in case: I am talking about reading and studying, not just blindly using JNI all the time).
In Java there is a class for working with descriptors: java.io.FileDescriptor. It can be used with FileXXXStream classes and thus indirectly with all framework IO classes, including memory-mapped and random access files, channels and channel locks. It is a tricky class. Because of requirement to be compatible with certain proprietary OS, this cross-platform class does not expose underlying integer number. It can not even be closed! Instead you are expected to close the corresponding IO classes, which (again, for compatibility reasons) share the same underlying descriptor with each other:
FileInputStream fileStream1 = new FileInputStream("notes.db");
FileInputStream fileStream2 = new FileInputStream(fileStream1.getFD());
WritableByteChannel aChannel = fileStream1.getChannel();
// pass fileStream1 and aChannel to some methods, written by clueless people
...
// surprise them (or get surprised by them)
fileStream2.close();
There are no supported ways to get integer value out of FileDescriptor, but you can (almost) safely assume, that on older OS versions there is a private integer descriptor field, which can be accessed via reflection.
Shooting yourself in the foot with descriptors
In Android framework there is a specialized class for working with Linux file descriptor: android.os.ParcelFileDescriptor. Unfortunately, it is almost as bad as FileDescriptor. Why? For two reasons:
1) It has a finalize() method. Read it's javadoc to learn, what this means for your performance. And you still has to close it, if you don't want to face sudden IO errors.
2) Because of being finalizable it will be automatically closed by virtual machine once the reference to a class instance goes out of scope. Here is why having finalize() on some framework classes, especially MemoryFile is a mistake on part of framework developers:
public FileOutputStream giveMeAStream() {
ParcelFileDescriptor fd = ParcelFileDescriptor.open("myfile", MODE_READ_ONLY);
return new FileInputStream(fd.getDescriptor());
}
...
FileInputStream aStream = giveMeAStream();
// enjoy having aStream suddenly closed during garbage collection
Fortunately, there is a remedy to such horrors: a magical dup system call:
public FileOutputStream giveMeAStream() {
ParcelFileDescriptor fd = ParcelFileDescriptor.open("myfile", MODE_READ_ONLY);
return new FileInputStream(fd.dup().getDescriptor());
}
...
FileInputStream aStream = giveMeAStream();
// you are perfectly safe now...
// Just kidding! Also close original ParcelFileDescriptor like this:
public FileOutputStream giveMeAStreamProperly() {
// Use try-with-resources block, because closing things in Java is hard.
// You can employ Retrolambda for backward compatibility,
// it can handle those too!
try (ParcelFileDescriptor fd = ParcelFileDescriptor.open("myfile", MODE_READ_ONLY)) {
return new FileInputStream(fd.dup().getDescriptor());
}
}
The dup syscall clones integer file descriptor, which makes corresponding FileDescriptor independent from original one. Note, that passing descriptors across processes does not require manual duplication: received
descriptors are independent from source process. Passing descriptor of MemoryFile (if you obtain it with reflection) does require the call to dup: having a shared memory region destroyed in originating
process will make it inaccessible to everyone. Furthermore, you have to either perform dup in native code or keep a reference to created ParcelFileDescriptor until a receiver is done with your MemoryFile.
Giving and receiving descriptors
There are two ways to give and receive file descriptors: by having a child process inherit creator's descriptors and via interprocess communication.
Letting children of process inherit files, pipes and sockets, open by creator, is a common practice in Linux, but requires forking in native code on Android – Runtime.exec() and ProcessBuilder close all extra
descriptors after creating a child process. Make sure to close unnecessary descriptors too, if you choose
to fork yourself.
The only IPC facilities, currently supporting passing file descriptors on Android are Binder and Linux domain sockets.
Binder allows you to give ParcelFileDescriptor to anything that accepts parcelable objects, including putting them in Bundles, returning from content providers and passing via AIDL calls to services.
Note, that most attempts to pass Bundles with descriptors outside of the process, including calling startActivityForResult will by denied by system, likely because timely closing those descriptors would have been too hard. Much better choices are creating a ContentProvider (which manages descriptor lifecycle for you, and publishes files via ContentResolver) or writing an AIDL interface and closing a descriptor right after it is transferred. Also note, that persisting ParcelFileDescriptor
anywhere does not make much sense: it would only work until process death and corresponding integer will most likely point to something else, once your process is recreated.
Domain sockets are low-level and a bit painful to use for descriptor transfer, especially compared to providers and AIDL. They are, however, a good (and the only documented) option for native processes. If you are
forced to open files and/or move data around with native binaries (which is usually the case for applications, using root privileges), consider not wasting your efforts and CPU resource on intricate communications with
those binaries, instead write an open helper. [shameless advert] By the way, you may use the one I wrote, instead of creating your own. [/shameless advert]
Answer to exact question
I hope, that this answer have given you a good idea, what's wrong with MediaStore.MediaColumns.DATA, and why creating this column is a misnomer on the part of Android development team.
That said, if you are still not convinced, want that filename at all costs, or simply failed to read the overwhelming wall of text above, here – have a ready-to-go JNI function; inspired by Getting Filename from file descriptor in C (EDIT: now has a pure-Java version):
// src/main/jni/fdutil.c
JNIEXPORT jstring Java_com_example_FdUtil_getFdPathInternal(JNIEnv *env, jint descriptor)
{
// The filesystem name may not fit in PATH_MAX, but all workarounds
// (as well as resulting strings) are prone to OutOfMemoryError.
// The proper solution would, probably, include writing a specialized
// CharSequence. Too much pain, too little gain.
char buf[PATH_MAX + 1] = { 0 };
char procFile[25];
sprintf(procFile, "/proc/self/fd/%d", descriptor);
if (readlink(procFile, buf, sizeof(buf)) == -1) {
// the descriptor is no more, became inaccessible etc.
jclass exClass = (*env) -> FindClass(env, "java/io/IOException");
(*env) -> ThrowNew(env, exClass, "readlink() failed");
return NULL;
}
if (buf[PATH_MAX] != 0) {
// the name is over PATH_MAX bytes long, the caller is at fault
// for dealing with such tricky descriptors
jclass exClass = (*env) -> FindClass(env, "java/io/IOException");
(*env) -> ThrowNew(env, exClass, "The path is too long");
return NULL;
}
if (buf[0] != '/') {
// the name is not in filesystem namespace, e.g. a socket,
// pipe or something like that
jclass exClass = (*env) -> FindClass(env, "java/io/IOException");
(*env) -> ThrowNew(env, exClass, "The descriptor does not belong to file with name");
return NULL;
}
// doing stat on file does not give any guarantees, that it
// will remain valid, and on Android it likely to be
// inaccessible to us anyway let's just hope
return (*env) -> NewStringUTF(env, buf);
}
And here is a class, that goes with it:
// com/example/FdUtil.java
public class FdUtil {
static {
System.loadLibrary(System.mapLibraryName("fdutil"));
}
public static String getFdPath(ParcelFileDescriptor fd) throws IOException {
int intFd = fd.getFd();
if (intFd <= 0)
throw new IOException("Invalid fd");
return getFdPathInternal(intFd);
}
private static native String getFdPathInternal(int fd) throws IOException;
}
Lots of Intent actions, like ACTION_VIEW, take a Uri pointing to the content the action should be performed upon. If the content is backed by a file -- whether the Uri points directly to the file, or to a ContentProvider serving the file (see FileProvider) -- this generally works.
There are scenarios in which developers do not want to have the content reside in a file for sharing with other apps. One common scenario is for encryption: the decrypted data should reside in RAM, not on disk, to minimize the risk of somebody getting at that decrypted data.
My classic solution to sharing from RAM is to use ParcelFileDescriptor and createPipe(). However, when the activity responding to ACTION_VIEW (or whatever) gets an InputStream on that pipe, the resulting stream is limited compared to the streams you get when the ContentProvider is serving up content from a file. For example, this sample app works fine with Adobe Reader and crashes QuickOffice.
Based on past related questions, my assumption is that createPipe() is truly creating a pipe, and that pipes are non-seekable. Clients that attempt to "rewind" or "fast forward" run into problems as a result.
I am seeking a reliable solution for sharing in-memory content with a third-party app that gets around this limitation. Specifically:
It has to use a Uri syntax that is likely to be honored by client apps (i.e., ACTION_VIEW implementers); solutions that involve something obtuse that client apps are unlikely to recognize (e.g., pass such-and-so via an Intent extra) do not qualify
The data to be shared cannot be written to a file as part of the sharing (of course, the client app could wind up saving the received bytes to disk, but let's ignore that risk for the moment)
Ideally it does not involve the app looking to share the data opening up a ServerSocket or otherwise exacerbating security risks
Possible suggested ideas include:
Some way to reconfigure createPipe() that results in a seekable pipe
Some way to use a socket-based FileDescriptor that results in a seekable pipe
Some kind of RAM disk or something else that feels like a file to the rest of Android but is not persistent
A key critierion, if you will, of a working solution is if I can get a PDF served from RAM that QuickOffice can read.
Any suggestions?
Thanks!
You've posed a really difficult combination of requirements.
Lets look at your ideas for solutions:
Possible suggested ideas include:
Some way to reconfigure createPipe() that results in a seekable pipe
Some way to use a socket-based FileDescriptor that results in a seekable pipe
Some kind of RAM disk or something else that feels like a file to the rest of Android but is not persistent
The first one won't work. This issue is that the pipe primitive implemented by the OS is fundamentally non-seekable. The reason is supporting seek that would require the OS to buffer the entire pipe "contents" ... until the reading end closes. That is unimplementable ... unless you place a limit on the amount of data that can be sent through the pipe.
The second one won't work either, for pretty much the same reason. OS-level sockets are not seekable.
At one level, the final idea (a RAM file system) works, modulo that such a capability is supported by the Android OS. (A Ramfs file is seekable, after all.) However, a file stream is not a pipe. In particular the behaviour with respect to the end-of-file is different for a file stream and a pipe. And getting a file stream to look like a pipe stream from the perspective of the reader would entail some special code on that side. (The problem is similar to the problem of running tail -f on a log file ...)
Unfortunately, I don't think there's any other way to get a file descriptor that behaves like a pipe with respect to end-of-file and is also seekable ... short of radically modifying the operating system.
If you could change the application that is reading from the stream, you could work around this. This is precluded by the fact that the fd needs to be read and seeked by QuickOffice which (I assume) you can't modify. (But if you could change the application, there are ways to make this work ...)
By the way, I think you'd have the some problems with these requirements on Linux or Windows. And they are not Java specific.
UPDATE
There have been various interesting comments on this, and I want to address some here:
The OP has explained the use-case that is motivating his question. Basically, he wants a scheme where the data passing through the "channel" between the applications is not going to be vulnerable in the event that the users device is stolen (or confiscated) while the applications are actually running.
Is that achievable?
In theory, no. If one postulates a high degree of technical sophistication (and techniques that the public may not know about ...) then the "bad guys" could break into the OS and read the data from shared memory while the "channel" remained active.
I doubt that such attacks are (currently) possible in practice.
However, even if we assume that the "channel" writes nothing to "disc" there could still be traces of the channel in memory: e.g.
a still mounted RAMfs or still active shared memory segments, or
remnants of previous RAMfs / shared memory.
In theory, this data could in theory be retrieved, provided that the "bad guy" doesn't turn of or reboot the device.
It has been suggested that ashmem could be used in this context:
The issue of there being no public Java APIs could be addressed (by writing 3rd-party APIs, for example)
The real stumbling block is the need for a stream API. According the "ashmem" docs, they have a file-like API. But I think that just means that they conform to the "file descriptor" model. These FDs can be passed from one application to another (across fork / exec), and you use "ioctl" to operate on them. But there is no indication that they implement "read" and "write" ... let alone "seek".
Now, you could probably implement a read/write/seekable stream on top of ashmem, using native and Java libraries on both ends of the channel. But both applications would need to be "aware" of this process, probably to the level of providing command line options to set up the channel.
These issues also apply to old-style shmem ... except that the channel setup is probably more difficult.
The other potential option is to use a RAM fs.
This is easier to implement. The files in the RAMfs will behave like "normal" files; when opened by an application you get a file descriptor that can be read, written and seeked ... depending on how it was opened. And (I think) you should be able to pass a seekable FD for a RAMfs file across a fork/exec.
The problem is that the RAMfs needs to be "mounted" by the operating system in order to use it. While it is mounted, another (privileged) application can also open and read files. And the OS won't let you unmount the RAMfs while some application has open fds for RAMfs files.
There is a (hypothetical) scheme that partly mitigates the above.
The source application creates and mounts a "private" RAMfs.
The source application creates/opens the file for read/write and then unlinks it.
The source application writes the file using the fd from the open.
The source application forks / execs the sink application, passing the fd.
The sink application reads from the (I think) still seekable fd, seeking as required.
When the source application notices that the (child) sink application process has exited, it unmounts and destroys the RAMfs.
This would not require modifying the reading (sink) application.
However, a third (privileged) application could still potentially get into the RAMfs, locate the unlinked file in memory, and read it.
However, having re-reviewed all of the above, the most practical solution is still to modify the reading (sink) application to read the entire input stream into a byte[], then open a ByteArrayInputStream on the buffered data. The core application can seek and reset it at will.
It's not a general solution to your problem, but opening a PDF in QuickOffice works for me with the following code (based on your sample):
#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;
}
I believe you're looking for StorageManager.openProxyFileDescriptor, function added in API 26. This will give you ParcelFileDescriptor, needed for your ContentProvider.openAssetFile to work. But you can also grab its file descriptor and use it in file I/O: new FileInputStream(fd.getFileDescriptor())
In function description is :
This can be useful when you want to provide quick access to a large file that isn't backed by a real file on disk, such as a file on a
network share, cloud storage service, etc. As an example, you could
respond to a ContentResolver#openFileDescriptor(android.net.Uri,
String) request by returning a ParcelFileDescriptor created with this
method, and then stream the content on-demand as requested. Another
useful example might be where you have an encrypted file that you're
willing to decrypt on-demand, but where you want to avoid persisting
the cleartext version.
It works with ProxyFileDescriptorCallback, which is your function to provide I/O, mainly read pieces of your file from various offsets (or decrypt it, read from network, generate, etc).
As I tested, it's well suited also for video playback over content:// scheme, because seeking is efficient, no seek-by-read as is the option for pipe-based approach, but Android really asks relevant fragments of your file.
Internally Android uses some fuse driver to transfer the data between processes.
I've been experimenting with #josias code. I found some of the query(...) calls came with a projection of _data. Including the data for that column and setting the actual length means more file types can be opened in more apps. Always including _data even when not in the passed in projection allows opening even more file types.
Here is what I ended up with:
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;
}
The final objective will be clear shortly.
I want to create a file object and instead of getting data from a real physical file I want to provide the buffer myself.
Then, I want to use this file, which does not really exist in the sdcard or anywhere outside my app, give it a name and send it by email as an attachment (using the EXTRA_STREAM).
I found the following bit of code, by Adriaan Koster (#adriaankoster), the post Write byte[] to File in Java
// convert byte[] to File
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
File fileFromBytes = (File) ois.readObject();
bis.close();
ois.close();
System.out.println(fileFromBytes);
I used it to create this function
private File fileFromBytes(byte[] buf) {
File f = null;
try {
ByteArrayInputStream bis = new ByteArrayInputStream(buf);
ObjectInputStream ois = new ObjectInputStream(bis);
f = (File) ois.readObject();
bis.close();
ois.close();
}
catch (Exception e) {}
return f;
}
and here is where I am stuck, because when I use it:
// When sent as body the mail is sent OK
// emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, dump());
// When I try to attach the mail is empty
emailIntent.putExtra(android.content.Intent.EXTRA_STREAM, fileFromBytes(dump().getBytes()));
I know from examples I've seen the second argument should be an URI, but: How do I create a virtual URI to fit my file?
EDIT:
The option to attach data directly from within the application is important to certain kind of applications. Namely, security & banking applications that do not want to move sensitive data around too much. Surely if the data does not reach the sdcard and goes directly to a mail attachment it is harder to sniff than within the application memory.
This is not my specific case, but I wanted to point out that this capability is important to have.
The first thing you'll want to do, I imagine, is create a ContentProvider. You can see an example implementation here
https://github.com/dskinner/AndroidWeb/blob/master/src/org/tsg/web/WebContentProvider.java
where in the above link's case, you would add this to your AndroidManifest.xml
<provider
android:name="org.tsg.web.WebContentProvider"
android:authorities="your.package.name" />
Now, you'll have a content uri available for use, content://your.package.name/.
The portion of the above ContentProvider your interested in, again I imagine, is the openFile method. When sharing data by intent across apps, certain things are expected. In your case, you're looking to share some byte data that's meant to be attached to the email.
So if you pass in a content uri to the email app such as content://your.package.name/foo with the appropriate intent flags, then openFile will get called on your ContentProvider. In this case, you can inspect the end of the uri segment to see foo was requested, and return appropriately.
The next issue you bring up is not having the file actually on disk. While I can't vouch for the method you used above (though it looks kosher), what you need to be returning is a ParcelFileDescriptor from your ContentProvider. If you look at the link I provided, you could possibly try to use that as a sample to get the file descriptor from your File object (my knowledge waivers here), but I imagine, the data simply wont be available at that point.
What you do bring up is security though. It's important to note that you can write data to disk privately so only the app has access to the data. I believe, but you might want to double check on this, if that data is private to the app, you can expose it via the ContentProvider and possibly lock down who and how the provider gets used, who can call it, etc. You may want to dig into android docs for that portion or look at some other SO questions.
Anyway, good luck.
Create the file in the application's cache directory. It will be created in the internal filesystem. Use 'getCacheDir()' API for getting the path to the cache dir. Write the data into this dir and then get the URI from the File object using ' Uri.fromFile (File file) '. When you are finished with the file, delete it.
Your application's cache is only available to your app, hence its safe to use for your purpose.
You can do some encryption if the data is too critical.
I think in order to do this, you are going to have to expose a ContentProvider, which will allow you handle a URI. The email application should then openInputStream on your URI, at which point you return an InputStream on your in-memory data.
I've not tried it, but in theory this should work.
i was busy with adding attachment to mail and i can send mail with attachment.
if you want to take a look: can not send mail with attachment in Android
I'm trying to expose a .png file located in my application's /data directory through a ContentProvider but instead of reaching the openFile method query is being called. Now I only ever have a single image which I need to expose for sharing to other applications, how can I setup my Intent to goto openFile instead of query?
Intent shareImageIntent = new Intent(Intent.ACTION_SEND);
shareImageIntent.setType("image/*");
shareImageIntent.putExtra(Intent.EXTRA_STREAM, imageUri);
startActivity(Intent.createChooser(shareImageIntent, "Share image"));
Where the Uri looks like
content://my.package.contentprovider/fileName
Or alternatively do I need to create a database for this and return a cursor?
UPDATE
So this appears to be working on everything except the SMS app (which is what I decided to test first) I would like to support sharing to it however.
Here's the relevant stack trace:
Caused by: java.lang.IllegalArgumentException: Query on
content://mypackage.myprovider/someImage.png returns null result. at
com.android.mms.ui.UriImage.initFromContentUri(UriImage.java:104) at
com.android.mms.ui.UriImage.(UriImage.java:63) at
com.android.mms.model.ImageModel.initModelFromUri(ImageModel.java:83)
at com.android.mms.model.ImageModel.(ImageModel.java:65) at
com.android.mms.data.WorkingMessage.changeMedia(WorkingMessage.java:481)
at
com.android.mms.data.WorkingMessage.setAttachment(WorkingMessage.java:375)
...
So the SMS app is performing a query instead of reading directly from openFile, which every other app on my phone seems to do (including other Google apps)
Does anyone know what I need to return here to fullfil the query appropriately? I'm going to go AOSP digging now.
After digging through the source code of the SMS (MMS really) app this is what I came up with.
Inside UriImage.initFromContentUri the application makes the query code and assumes there are 2 returned columns in the Cursor
} else {
filePath = c.getString(c.getColumnIndexOrThrow(Images.Media.DATA));
mContentType = c.getString(c.getColumnIndexOrThrow(Images.Media.MIME_TYPE));
}
So inorder for your ContentProvider to work with the MMS app, you need to return a Cursor in query that only has one row and the two columns (Images.Media.DATA & Images.Media.MIME_TYPE) with the appropriate data. The MMS will then make the call to openFile to actually retrieve the image.
An easier way to share a image resource is to save it to external storage (SD-card) and then do:
Uri imageUri = Uri.fromFile(pathToFile);
Update:
Try using
Uri imageUri = Uri.parse("android.resource://com.package.yourapp/" +imageResID);
Update2
Try saving file to Media Store and then sending it:
String url = Media.insertImage(context.getContentResolver(), imageFile.getAbsolutePath(), imageFile.getName(), imageFile.getName());
Uri imageUri = Uri.parse(url);
Final Update using ContentProvider and Cursor:
Your ContentProvider must implement query(..) method and it must return a Cursor. See the source code of UrlImage.initFromContentUri(..) (which is internally used by MMS app) to see how cursor is called. Take a look at the MatrixCursor if it fits the bill.
If your content provider is already working you can access to a ParcelFileDescriptor via the method openFileDescriptor in the content provider.
A quick, and dirty, example for this:
ParcelFileDescriptor descriptor = mContext.getContentResolver().openFileDescriptor(IMGURI, "r");
Bitmap bmp = BitmapFactory.decodeFileDescriptor(descriptor.getFileDescriptor());
Cheers!