Android Share Intents - Some files won't share - java

I am having some issues with Android share intents. I am able to succesfully share image files (jpg, png, gifs), but when I try to share any other files (doc, docx, xlsx, ppt), I get errors from the apps saying that there were errors opening the files, but when I try to open them from the file manager, they work fine.
var uri = Android.Net.Uri.Parse(System.IO.Path.Combine(Environment.GetExternalStoragePublicDirectory(Environment.DirectoryDownloads).AbsolutePath, fileName));
string auth = "xamarintestapp.xamarintestapp.fileprovider";
string mimeType = Android.Webkit.MimeTypeMap.Singleton.GetMimeTypeFromExtension(Android.Webkit.MimeTypeMap.GetFileExtensionFromUrl(fileName.ToLower()));
if (mimeType == null)
mimeType = "*/*";
var file = new Java.IO.File(System.IO.Path.Combine(Environment.GetExternalStoragePublicDirectory(Environment.DirectoryDownloads).AbsolutePath, fileName));
Android.Net.Uri intentUri = null;
Intent intent = new Intent(Intent.ActionView);
intent.SetDataAndType(uri, mimeType);
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
Forms.Context.StartActivity(Intent.CreateChooser(intent, "Choose an App"));
I have tried checking the MIME type, and they seem to be correct (application/vnd.openxmlformats-officedocument.wordprocessingml.document for doc and docx files). Any help would be greatly appreciated.

You need to use a file scheme-based based uri instead of just passing a filesystem-based path.
Note: The Downloads directory is a publicly accessible file location on Android so no granting of rights, nor content provider, is needed, but if these files, doc|x or not, are coming from within your app's sandbox, then you would need to implement a content provider and share a content://-based provider uri to Word, Excel and other apps...
Example:
var fileName = "demo.docx";
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(fileName)) ?? "*/*";
var downloads = Environment.GetExternalStoragePublicDirectory(Environment.DirectoryDownloads);
using (var intent = new Intent(Intent.ActionView))
using (var uri = new Uri.Builder()
.Scheme("file")
.Authority("localhost")
.AppendEncodedPath(downloads.CanonicalPath)
.AppendEncodedPath(fileName)
.Build())
{
intent.SetDataAndType(uri, mimeType);
StartActivity(Intent.CreateChooser(intent, "Choose an App"));
}
Update:
....exposed beyond app through ClipData.Item.getUri()
Compiling against, say Android P/API-28, and using a minSDKVersion but no targetSDKVersion in the manifest (Xamarin calls it "automatic") and this code will work (I have Android P apps using the latest APIs running using the above code but they do not "target" a specific API level at runtime.)
But you are targeting a specific API >= Nougat thus you will have to implement a file provider to "share" even public files and thus provide content://-based uris to the app you are sharing to.

Related

How to convert file path to treeUri

In my application i have a regular Settings, where user can choose a video directory on his android device, and videos found in that directory will play at some point in the application. Now, when user himself is doing that via Preferences, i am opening ACTION_OPEN_DOCUMENT_TREE, and Uri returned is treeUri, that i am later using in my Video Activity with DocumentsContract.buildChildDocumentsUriUsingTree. This part is completely fine, it's working and all is OK.
However, since this application is going to be used in enterprises, i have a request, that IT department can submit an external xml file that i am completely controlling in application external directory, with all options that you as a user can set in the application itself, and if that file exists, those options need to be implemented in SharedPreferences. I got all settings to be implemented from that external xml file, apart that video directory. I was thinking that user can give regular path in the xml, like <setting key="video_folder">/sdcard/Movies</setting>, and then i parse that in the application, get treeUri from path, and save it in SharedPreferences for later use. Any ideas if this is doable, or this should have some other approach?
I've tried this method below:
public static Uri[] getSafUris (Context context, File file) {
Uri[] uri = new Uri[2];
String scheme = "content";
String authority = "com.android.externalstorage.documents";
// Separate each element of the File path
// File format: "/storage/XXXX-XXXX/sub-folder1/sub-folder2..../filename"
// (XXXX-XXXX is external removable number
String[] ele = file.getPath().split(File.separator);
// ele[0] = not used (empty)
// ele[1] = not used (storage name)
// ele[2] = storage number
// ele[3 to (n-1)] = folders
// ele[n] = file name
// Construct folders strings using SAF format
StringBuilder folders = new StringBuilder();
if (ele.length > 4) {
folders.append(ele[3]);
for (int i = 4; i < ele.length - 1; ++i) folders.append("%2F").append(ele[i]);
}
String common = ele[2] + "%3A" + folders.toString();
// Construct TREE Uri
Uri.Builder builder = new Uri.Builder();
builder.scheme(scheme);
builder.authority(authority);
builder.encodedPath("/tree/" + common);
uri[0] = builder.build();
// Construct DOCUMENT Uri
builder = new Uri.Builder();
builder.scheme(scheme);
builder.authority(authority);
if (ele.length > 4) common = common + "%2F";
builder.encodedPath("/document/" + common + file.getName());
uri[1] = builder.build();
return uri;
}
but that gives me:
Permission Denial: reading com.android.externalstorage.ExternalStorageProvider uri content://com.android.externalstorage.documents/tree/Movies%3A/document/Movies%3A/children from pid=28318, uid=10157 requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs
I had to completely rearrange logic for playing videos, to read them from regular file:// uri's, in order to bypass SAF. In short, my question was, if you don't obtain tree URI from ACTION_OPEN_DOCUMENT, ACTION_OPEN_DOCUMENT_TREE, and similar API's, is there any way possible, to obtain a permission for such tree URI. As far as i could tell, that is not possible (not even with any kind of runtime permission presented to the user). If anyone else believes that there is such a possibility, please, let me know.

Android open an external directory using an intent

There is not a straightforward or a clear way to make an implicit intent to open a folder/directory in Android.
Specifically here I want to open getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).
I tried these ones but they will just open a FileManager app, not the directory I want:
val directory = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
val uri = Uri.parse(directory.path)
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.setDataAndType(uri, "*/*")
startActivity(Intent.createChooser(openIntent, "Open Folder"))
Another example:
val directory = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
val uri = Uri.parse(directory.path)
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, "resource/folder")
startActivity(Intent.createChooser(openIntent, "Open Folder"))
Opening 'THE' Downloads folder
If you want to open the downloads folder, you need to use DownloadManager.ACTION_VIEW_DOWNLOADS, like this:
Intent downloadIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(downloadIntent);
No need to use mime type resource/folder as Android doesn't have an official folder mime type, so you could produce some errors in some devices. Your device seems not to support that mime type. You need to use the code above, as it just passes to the intent the official folder you want to go to.
Edit: For other custom directories, I don't think that you can just pass a path to the intent like the one. I don't think that there is a reliable way to open a folder in Android.
Using FileProvider(Test)
Edit: Try instead of just parsing the Uri, if you are using FileProvider, which you should, use getUriForFile(). Like this:
val dir = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
val intent = new Intent(Intent.ACTION_VIEW)
val mydir = getUriForFile(context, "paste_your_authority", dir)
intent.setDataAndType(mydir, "resource/folder")
startActivity(intent);
or instead of using resource/folder, use:
DocumentsContract.Document.MIME_TYPE_DIR
Moral of the story:
There is no standard way of opening files. Every device is different and the code above is not guaranteed to work in every single device.

Filter by mimetype or extention in native Android file picker

In Android, one can use the ACTION_OPEN_DOCUMENT Intent to open the native file picker and select for example an .mp4 file. This is achived by setting the mime type to video/mp4 using the following code:
public static void pickFile(Context mContext, int REQUEST_CODE) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("video/mp4");
((Activity) mContext).startActivityForResult(intent, REQUEST_CODE);
}
In my project, I want to pick a custom extension file whose mime type is not known in android's MimeTypeMap like for example .qgs or .dcm files.
To solve this I see two possibilities that we, so far, failed to implement:
filter by extension in the ACTION_OPEN_DOCUMENT Intent
register a new mime type to android so that it can be used with the ACTION_OPEN_DOCUMENT Intent
is either of those options doable and how?
or are there other approaches I missed without coding my own file picker?
You can try creating a custom file picker and pass the mount storage points and recursively iterate on all the file types:
public void scanFiles(File file) {
File[] fileArray = file.listFiles();
for (File f : fileArray){
if (f.isDirectory())
scanFiles(f);
if (f.isFile() && (f.getpath().endswith("..qgs") || //any extensions)) {
//Add to your list
}
}
}
And call scan files for all the mount points in android using a process builder and mount command and you can add checks for all the mount points you wish to support.
one can use the ACTION_OPEN_DOCUMENT Intent to open the native file picker
That is not a "file picker". There is no requirement that every DocumentsProvider on the device be serving documents that happen to be files.
I want to pick a custom extension file whose mime type is not known in android's MimeTypeMap like for example .qgs or .dcm files
There is no requirement that every DocumentsProvider on the device be using documents that have filenames for their files. The names that you see are "display names", and while for some providers they will be filenames, for other providers they can be anything that the provider wants.
is either of those options doable
No.
or are there other approaches I missed without coding my own file picker?
If you only want files, there are plenty of existing file picker implementations available in libraries. ACTION_OPEN_DOCUMENT is there for when you want to work with all possible document sources (removable storage, cloud providers, etc.), not just files.

ACTION_GET_CONTENT gives wrong path

I'm using ACTION_GET_CONTENT so that the user can select text files that the rest of my code can read and deal with.
Intent intent = new Intent();
intent.setAction(Intent.ACTION_GET_CONTENT);
intent.setType("text/*");
startActivityForResult(Intent.createChooser(intent, "select data"), SELECT_DATA);
Above is my code so that the user can browse which works fine.
Uri DataUri = data.getData();
File FileUri = new File(DataUri.getPath());
If I convert DataUri or FileUri to a string after using getPath or getAbsolutePath, I get a completely wrong path.
The path should be /storage/emulated/0/Documents/myFile but it gives me /document/primary:Documents/myFile. I have no idea what this "primary:Documents" thing is.
The data from the intent itself already has the wrong path, any suggestions?
I'm using ACTION_GET_CONTENT so that the user can select text files
No, you are using ACTION_GET_CONTENT so that the user can select some content that has a MIME type of text/*. Where that content comes from is largely up to the user. It may or may not be a file on the filesystem, and if it is, is may or may not be a file that you could access yourself directly (e.g., internal storage of the Dropbox app, files on removable storage on Android 4.4+ devices).
The path should be /storage/emulated/0/Documents/myFile
No, it should not.
but it gives me /document/primary:Documents/myFile
What you get will vary by whatever activity handles the ACTION_GET_CONTENT request. What that activity is will depend on what the user has installed that supports ACTION_GET_CONTENT and what the user chooses out of that list of installed stuff.
Usually, you will get a content Uri (i.e., getScheme() on Uri will return content), as the Uri will point to data being served by a ContentProvider.
The data from the intent itself already has the wrong path
It has the correct path. It is simply not a path on the filesystem.
any suggestions?
Use a ContentResolver and openInputStream() to get an InputStream for the content represented by the content Uri.

Android list applications that can view an unknown file

Hi I'm working on an app where I need to list all types of applications able to open a file. If the file were an image I would do something like this normally to show all applications capable of viewing the image.
Intent intent = new Intent();
intent.setAction(android.content.Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file), "image/*");
context.startActivity(intent);
However say I have an unknown file how could I list all applications able to view a file so the user can select the appropriate application to open the file in my application.
I also need the titles listed in an arraylist if possible so I can list them in a listview.
Thank you for any help with getting a list of applications capable of viewing a file
===================================
Edit
Alright well something easier how can i get the applications from the above intent into an arraylist i could just do image/* audio/* etc and add them all to a list and then list them in a listview and that would solve my problem
As far as i know there is no API in android that links files to a program like in windows. Your best choose I'm afraid is to build a database of known file types and program/android app linked to them.
Okay well i figured my problem out what i was looking for was
Intent audioIntent = new Intent(android.content.Intent.ACTION_VIEW);
audioIntent.setDataAndType(Uri.fromFile(Opener.file), "audio/*");
List<ResolveInfo> audio = packageManager.queryIntentActivities(audioIntent, 0);
for (ResolveInfo info : audio){
String label = info.loadLabel(packageManager).toString();
Drawable icon = info.loadIcon(packageManager);
String packageName = info.activityInfo.packageName;
String name = info.activityInfo.name;
iconlabel.add(a.new HolderObject(label, icon, audioIntent, "audio/*", packageName, name));
}
But the main thing in the code above is the queryIntentActivities method that was what solved my issue allowing me to add those apps to a list
This may help you to list the application that are capable of opening a particular file.
// extension : The file extension (.pdf, .docx)
String extension = filePath.substring(filePath.lastIndexOf(".") + 1);
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase());
if (null == mimeType) {
// Need to set the mimetype for files whose mimetype is not understood by android MimeTypeMap.
mimeType = "";
}
File file = new File(filePath);
Uri data = Uri.fromFile(file);
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(data, mimeType);
context.startActivity(Intent.createChooser(intent, "Complete action using"));

Categories