Android xamarin forms: Edit private files - android

I am developing an Android app with Xamarin forms where I have a series of files with different extensions that have been downloaded and stored in my applications private local storage area.
I then have the requirement that users should be able to open these files in the App of their choosing, make any changes and then Save/overwrite that original file. I find however that in quite a lot of Apps, the files open Read-only and in others, the behaviour is inconsistant e.g. in Xodo, PDF files can be edited and saved no problem whilst jpg files always open readonly.
I therefore wonder, is the code correct and if there is any other way to achieve this to maximise the number of Apps able to open files in write mode?
At the point of download, it is not a problem to have the files stored in a different location however this location is stored in a Sqlite database so that the file can be re-uploaded (with changes) at a later date.
I have added the main parts of the code as below:
public async Task OpenFileForEditFromPathAsync(string filePath)
{
Java.IO.File file = new Java.IO.File(filePath);
var exists = file.Exists();
Android.Net.Uri path = Android.Net.Uri.FromFile(file);
string extension = MimeTypeMap.GetFileExtensionFromUrl(Android.Net.Uri.FromFile(file).ToString());
string mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
Intent intent = new Intent(Intent.ActionEdit);
intent.AddFlags(ActivityFlags.ClearWhenTaskReset);
intent.AddFlags(ActivityFlags.GrantReadUriPermission);
intent.AddFlags(ActivityFlags.GrantWriteUriPermission);
intent.AddFlags(ActivityFlags.GrantPersistableUriPermission);
var uri = FileProvider.GetUriForFile(Android.App.Application.Context, Android.App.Application.Context.PackageName + ".fileprovider", file);
if (uri != null)
{
intent.SetData(uri);
}
this.StartActivity(Intent.CreateChooser(intent, "Choose App"));
}
public static class ObjectExtensions
{
public static void StartActivity(this object o, Intent intent)
{
var context = o as Context;
if (context != null)
context.StartActivity(intent);
else
{
intent.SetFlags(ActivityFlags.NewTask);
Application.Context.StartActivity(intent);
}
}
}
with the manifest settings as:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="27" />
<application android:label="ExampleApp.Android">
<provider android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="#xml/file_paths"></meta-data>
</provider>
</application>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest>

Related

Opening pdf file Error: This file could not be accessed Check the location or the network and try again

Hi inside of the adapter I am displaying a button to view PDf .When I am try to click on that button it is giving me this error.below One is complete what i have tried in my application.
Can any one help me resolve this issue.
This file could not be accessed Check the location or the network and try again.//error
holder.pdf.setOnClickListener(new View.OnClickListener() {
#Override public void onClick(View v) {
File file = new File(Environment.getExternalStorageDirectory(),
opportunity.getPdf_link());
// Uri path = Uri.fromFile(file);
Uri pdf = FileProvider.getUriForFile(mContext, mContext.getPackageName() + ".provider", file);
Intent pdfOpenintent = new Intent(Intent.ACTION_VIEW);
pdfOpenintent.setDataAndType(pdf, "application/pdf");
pdfOpenintent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
pdfOpenintent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
try {
mContext.startActivity(pdfOpenintent);
} catch (ActivityNotFoundException e) {
// handle no application here....
}
}
});
Manifest:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/provider_paths"/>
</provider>
Remove:
pdfOpenintent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
Or, if you are really sure that you need that flag, switch to addFlags() rather than setFlags(). As it stands, setFlags() is replacing the flag that you added in the previous line... and that flag (FLAG_GRANT_READ_URI_PERMISSION) is much more important.

Open a audio file...("audio/wav") using intent [duplicate]

This is a part of my manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.asd"
android:versionCode="118"
android:versionName="118" >
<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="19" />
<application
android:name="com.example.asd.AsdApplication"
android:allowBackup="true"
android:allowTaskReparenting="true"
android:theme="#style/AsdTheme" >
...
<provider
android:name="com.example.asd.database.hq.ContentProviderDB"
android:authorities="ourContentProviderAuthorities" >
</provider>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.asd.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/filepaths" />
</provider>
...
</application>
</manifest>
This is the filepaths file in raw/xml/filepaths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="media"/>
</paths>
I download a video from internet and save it to internal storage this way:
public static boolean saveInputStreamToInternalStorageFile(Context context, String filename, byte[] dataToWrite, Context ctx) {
FileOutputStream fos;
try {
fos = new FileOutputStream(context.getFilesDir() + File.separator + filename);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(dataToWrite);
oos.close();
return true;
} catch (FileNotFoundException e) {
e.printStackTrace();
return false;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
I try to use it like so:
private void playVideoFromDeviceWithWorkaround(String fileName) {
File newFile = new File(getFilesDir(), fileName);
Uri contentUri = FileProvider.getUriForFile(getApplicationContext(), "com.example.asd", newFile);
try {
vvVideoFullscreen.setVideoURI(contentUri);
showMediaControls = true;
playVideo();
} catch (Exception e) {
playVideoFromNetwork();
}
}
At this line:
Uri contentUri = FileProvider.getUriForFile(getApplicationContext(), "com.example.asd", newFile);
I get the following error:
java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.res.XmlResourceParser android.content.pm.ProviderInfo.loadXmlMetaData(android.content.pm.PackageManager, java.lang.String)' on a null object reference
at android.support.v4.content.FileProvider.parsePathStrategy(FileProvider.java:560)
at android.support.v4.content.FileProvider.getPathStrategy(FileProvider.java:534)
at android.support.v4.content.FileProvider.getUriForFile(FileProvider.java:376)
The problem was that in Manifest I had this line:
android:authorities="com.example.asd.fileprovider"
and when calling getUriForFile I was passing:
Uri contentUri = FileProvider.getUriForFile(getApplicationContext(), "com.example.asd", newFile);
So changed from "com.example.asd" to "com.example.asd.fileprovider" and it worked
You can do this without hardcoding the package name with an additional benefit of being able to run multiple variants on the same device (think release and debug with applicationIdSuffix, see these issues):
Based on FileProvider.java:560
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
final XmlResourceParser in = info.loadXmlMetaData( //560
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
you were using the wrong authority and it didn't find the ContentProvider (info == null).
Change your manifest to (${applicationId} will be replaced by Manifest Merger)
android:authorities="${applicationId}.share"
and
Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".share", result);
The .share suffix is optional, in case you have a real ContentProvider which is better to have the package name as the authority.
In my case, I got the error because the
BuildConfig.APPLICATION_ID
was being imported from
import android.support.v4.BuildConfig;
So the string it returned was "android.support.v4" instead of my project package name. Check out the import file is from your import project.Buildconfig and not another. Example:
import com.example.yourProjectName.BuildConfig;
Finally, in <provider> tag in Manifest I have android:authorities="${applicationId}" to always get my project package name as the authority
<manifest>
..
..
<application>
..
..
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/ruta_fileprovider" />
</provider>
</application>
</manifest>
First, be sure that you provider android:authorities does not conflict with your other providers. Besides that you may choose any name for the last part of its name: "provider", "fileprovider" etc., but app crashes when there are more than one android:authorities listed, while documentation states that it allows multiple values listed.
file:// scheme is now not allowed to be attached with Intent on targetSdkVersion >= 24 (Android N 7.0), only content:// is always passed for all devices (Android 5, 6 and 7). But we encountered that Xiaomi breaks this Google convention and sends file://, hence data.getData().getAuthority() gives empty string.
final String uriScheme = currentUri.getScheme();
if ("content".equals(uriScheme)) {
// getting full file path (works with some providers, i.e. Gallery)
path = FileUtils.getPath(getContext(), currentUri);
if (path != null) {
currentFile = new File(path);
}
} else if ("file".equals(uriScheme)) {
// in a rare case we received file:// in currentUri, we need to:
// 1. create new File variable from currentUri that looks like "file:///storage/emulated/0/download/50044382b.jpg"
// 2. generate a proper content:// Uri for it
currentFile = new File(currentUri.getPath());
String authority = data.getData().getAuthority();
if (authority != null && !authority.isEmpty()) {
currentUri = FileProvider.getUriForFile(getActivity(), authority, currentFile);
}
} else {
// throw exception
}
Also, the bug when FileProvider.getUriForFile() resulted in crash java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/0/Android/data/com.example/files/attachments/image.jpg was fixed in Android Support Library v24.2.0. The problem was that FileProvider.java did not see external-path folders.
If you're building your AUTHORITY at runtime using BuildConfig make sure you use the full class name including your package name.
Bad:
final String AUTHORITY = BuildConfig.APPLICATION_ID + ".provider";
Good:
final String AUTHORITY = com.mycompany.myapp.BuildConfig.APPLICATION_ID + ".provider";
Following worked for me.
mUri = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID + ".provider",
fileObject);
Here is what i did to fix the issue. I gave fully qualified name in android:name. It works in android 6,7,8
<provider android:authorities="${applicationId}.opener.provider"
android:exported="false" android:grantUriPermissions="true"
android:name="io.github.pwlin.cordova.plugins.fileopener2.FileProvider">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="#xml/provider_paths" />
</provider>
You should try it:
Context context = PostAdapter.this.activity;
StringBuilder stringBuilder2 = new StringBuilder();
stringBuilder2.append(PostAdapter.this.activity.getPackageName());
stringBuilder2.append(".provider");
Uri uri;
uri = FileProvider.getUriForFile(context,stringBuilder2.toString(), newFile);
This is what you need to do:
Uri fileURI = FileProvider.getUriForFile(getActivity(), getActivity().getPackageName() + ".fileprovider", file);

How to send(share) csv file?

How to send(share) *csv file?
I have android app, wich create a csv file. And i want to share it: send to email, open at excel and so on.
I read tutorials:set up file sharing and sharing a file, but can not understand: is it for my option or not?
I declare a manifest:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.nam1.name2.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/filepaths" />
</provider>
But what i should do next?
Can you tell me some simple code example (maybe git-repo) how to share file to another apps?
Thank you!
EDIT: I write some new code to share files (look at FileProvider example)
public class ExportActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_export);
Intent intentShareFile = new Intent(Intent.ACTION_SEND);
intentShareFile.setAction(Intent.ACTION_SEND);
File imagePath = new File(getApplicationContext().getFilesDir(), "statistics");
File newFile = new File(imagePath, "stat.csv");
CSVHelper csvHelper = new CSVHelper(SharedData.AllPlayersColl);
try {
String path=imagePath.getAbsolutePath();
csvHelper.SaveToCSV(path);
} catch (IOException e) {
e.printStackTrace();
}
Context context=getApplicationContext();
Uri contentUri = getUriForFile(context, "com.name1.name2.fileprovider", newFile);
List<ResolveInfo> resInfoList = getApplicationContext().getPackageManager()
.queryIntentActivities(intentShareFile, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
getApplicationContext()
.grantUriPermission(packageName, contentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
intentShareFile.setData(contentUri);
intentShareFile.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
setResult(100, intentShareFile);
startActivityForResult(intentShareFile,100);
}
}
Where CSVHelper is:
fun SaveToCSV(fileName: String?) {
if (fileName == null)
throw NullPointerException("fileName is null!")
val writer = CSVWriter(FileWriter(fileName), '\t')
// make csv file
writer.writeNext(firstLine);
}
And filepaths is:
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<paths>
<files-path name="stat" path="statistics/" />
</paths>
But, i get a error:
Caused by: android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.SEND dat=content://com.name1.name2.fileprovider/stat/stat.csv flg=0x2 }
I want, that when i start an Intent a list of variants shows (at android device) : email_app,excel_app and so on. So, i want to send noname intent.
FileProvider is a helper class designed to share files as content:// URIs instead of file:// URIs which have been deprecated for some time and forbidden since API level 24.
You have declared the content provider, you need now to configure it, i.e. choose what file it can serve. This is done in the xml/filepath file that you have declared in the meta-data. You will then be able to generate shareable content URIs using FileProvider.getUriForFile().
There is a complete wrap up
Unfortunately not all application understand content:// URIs. If you need to share a file with a (very old or badly written) application which only handle file:// URIs you may still use them if you set the target level of your application to 23 or lower.

FileProvider crash - npe attempting to invoke XmlResourceParser on a null String

This is a part of my manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.asd"
android:versionCode="118"
android:versionName="118" >
<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="19" />
<application
android:name="com.example.asd.AsdApplication"
android:allowBackup="true"
android:allowTaskReparenting="true"
android:theme="#style/AsdTheme" >
...
<provider
android:name="com.example.asd.database.hq.ContentProviderDB"
android:authorities="ourContentProviderAuthorities" >
</provider>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.asd.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/filepaths" />
</provider>
...
</application>
</manifest>
This is the filepaths file in raw/xml/filepaths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="media"/>
</paths>
I download a video from internet and save it to internal storage this way:
public static boolean saveInputStreamToInternalStorageFile(Context context, String filename, byte[] dataToWrite, Context ctx) {
FileOutputStream fos;
try {
fos = new FileOutputStream(context.getFilesDir() + File.separator + filename);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(dataToWrite);
oos.close();
return true;
} catch (FileNotFoundException e) {
e.printStackTrace();
return false;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
I try to use it like so:
private void playVideoFromDeviceWithWorkaround(String fileName) {
File newFile = new File(getFilesDir(), fileName);
Uri contentUri = FileProvider.getUriForFile(getApplicationContext(), "com.example.asd", newFile);
try {
vvVideoFullscreen.setVideoURI(contentUri);
showMediaControls = true;
playVideo();
} catch (Exception e) {
playVideoFromNetwork();
}
}
At this line:
Uri contentUri = FileProvider.getUriForFile(getApplicationContext(), "com.example.asd", newFile);
I get the following error:
java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.res.XmlResourceParser android.content.pm.ProviderInfo.loadXmlMetaData(android.content.pm.PackageManager, java.lang.String)' on a null object reference
at android.support.v4.content.FileProvider.parsePathStrategy(FileProvider.java:560)
at android.support.v4.content.FileProvider.getPathStrategy(FileProvider.java:534)
at android.support.v4.content.FileProvider.getUriForFile(FileProvider.java:376)
The problem was that in Manifest I had this line:
android:authorities="com.example.asd.fileprovider"
and when calling getUriForFile I was passing:
Uri contentUri = FileProvider.getUriForFile(getApplicationContext(), "com.example.asd", newFile);
So changed from "com.example.asd" to "com.example.asd.fileprovider" and it worked
You can do this without hardcoding the package name with an additional benefit of being able to run multiple variants on the same device (think release and debug with applicationIdSuffix, see these issues):
Based on FileProvider.java:560
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
final XmlResourceParser in = info.loadXmlMetaData( //560
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
you were using the wrong authority and it didn't find the ContentProvider (info == null).
Change your manifest to (${applicationId} will be replaced by Manifest Merger)
android:authorities="${applicationId}.share"
and
Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".share", result);
The .share suffix is optional, in case you have a real ContentProvider which is better to have the package name as the authority.
In my case, I got the error because the
BuildConfig.APPLICATION_ID
was being imported from
import android.support.v4.BuildConfig;
So the string it returned was "android.support.v4" instead of my project package name. Check out the import file is from your import project.Buildconfig and not another. Example:
import com.example.yourProjectName.BuildConfig;
Finally, in <provider> tag in Manifest I have android:authorities="${applicationId}" to always get my project package name as the authority
<manifest>
..
..
<application>
..
..
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/ruta_fileprovider" />
</provider>
</application>
</manifest>
First, be sure that you provider android:authorities does not conflict with your other providers. Besides that you may choose any name for the last part of its name: "provider", "fileprovider" etc., but app crashes when there are more than one android:authorities listed, while documentation states that it allows multiple values listed.
file:// scheme is now not allowed to be attached with Intent on targetSdkVersion >= 24 (Android N 7.0), only content:// is always passed for all devices (Android 5, 6 and 7). But we encountered that Xiaomi breaks this Google convention and sends file://, hence data.getData().getAuthority() gives empty string.
final String uriScheme = currentUri.getScheme();
if ("content".equals(uriScheme)) {
// getting full file path (works with some providers, i.e. Gallery)
path = FileUtils.getPath(getContext(), currentUri);
if (path != null) {
currentFile = new File(path);
}
} else if ("file".equals(uriScheme)) {
// in a rare case we received file:// in currentUri, we need to:
// 1. create new File variable from currentUri that looks like "file:///storage/emulated/0/download/50044382b.jpg"
// 2. generate a proper content:// Uri for it
currentFile = new File(currentUri.getPath());
String authority = data.getData().getAuthority();
if (authority != null && !authority.isEmpty()) {
currentUri = FileProvider.getUriForFile(getActivity(), authority, currentFile);
}
} else {
// throw exception
}
Also, the bug when FileProvider.getUriForFile() resulted in crash java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/0/Android/data/com.example/files/attachments/image.jpg was fixed in Android Support Library v24.2.0. The problem was that FileProvider.java did not see external-path folders.
If you're building your AUTHORITY at runtime using BuildConfig make sure you use the full class name including your package name.
Bad:
final String AUTHORITY = BuildConfig.APPLICATION_ID + ".provider";
Good:
final String AUTHORITY = com.mycompany.myapp.BuildConfig.APPLICATION_ID + ".provider";
Following worked for me.
mUri = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID + ".provider",
fileObject);
Here is what i did to fix the issue. I gave fully qualified name in android:name. It works in android 6,7,8
<provider android:authorities="${applicationId}.opener.provider"
android:exported="false" android:grantUriPermissions="true"
android:name="io.github.pwlin.cordova.plugins.fileopener2.FileProvider">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="#xml/provider_paths" />
</provider>
You should try it:
Context context = PostAdapter.this.activity;
StringBuilder stringBuilder2 = new StringBuilder();
stringBuilder2.append(PostAdapter.this.activity.getPackageName());
stringBuilder2.append(".provider");
Uri uri;
uri = FileProvider.getUriForFile(context,stringBuilder2.toString(), newFile);
This is what you need to do:
Uri fileURI = FileProvider.getUriForFile(getActivity(), getActivity().getPackageName() + ".fileprovider", file);

Create and Share a File from Internal Storage

My goal is to create a XML file on internal storage and then send it through the share Intent.
I'm able to create a XML file using this code
FileOutputStream outputStream = context.openFileOutput(fileName, Context.MODE_WORLD_READABLE);
PrintStream printStream = new PrintStream(outputStream);
String xml = this.writeXml(); // get XML here
printStream.println(xml);
printStream.close();
I'm stuck trying to retrieve a Uri to the output file in order to share it. I first tried to access the file by converting the file to a Uri
File outFile = context.getFileStreamPath(fileName);
return Uri.fromFile(outFile);
This returns file:///data/data/com.my.package/files/myfile.xml but I cannot appear to attach this to an email, upload, etc.
If I manually check the file length, it's proper and shows there is a reasonable file size.
Next I created a content provider and tried to reference the file and it isn't a valid handle to the file. The ContentProvider doesn't ever seem to be called a any point.
Uri uri = Uri.parse("content://" + CachedFileProvider.AUTHORITY + "/" + fileName);
return uri;
This returns content://com.my.package.provider/myfile.xml but I check the file and it's zero length.
How do I access files properly? Do I need to create the file with the content provider? If so, how?
Update
Here is the code I'm using to share. If I select Gmail, it does show as an attachment but when I send it gives an error Couldn't show attachment and the email that arrives has no attachment.
public void onClick(View view) {
Log.d(TAG, "onClick " + view.getId());
switch (view.getId()) {
case R.id.share_cancel:
setResult(RESULT_CANCELED, getIntent());
finish();
break;
case R.id.share_share:
MyXml xml = new MyXml();
Uri uri;
try {
uri = xml.writeXmlToFile(getApplicationContext(), "myfile.xml");
//uri is "file:///data/data/com.my.package/files/myfile.xml"
Log.d(TAG, "Share URI: " + uri.toString() + " path: " + uri.getPath());
File f = new File(uri.getPath());
Log.d(TAG, "File length: " + f.length());
// shows a valid file size
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
shareIntent.setType("text/plain");
startActivity(Intent.createChooser(shareIntent, "Share"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
break;
}
}
I noticed that there is an Exception thrown here from inside createChooser(...), but I can't figure out why it's thrown.
E/ActivityThread(572): Activity
com.android.internal.app.ChooserActivity has leaked IntentReceiver
com.android.internal.app.ResolverActivity$1#4148d658 that was
originally registered here. Are you missing a call to
unregisterReceiver()?
I've researched this error and can't find anything obvious. Both of these links suggest that I need to unregister a receiver.
ChooserActivity has leaked IntentReceiver
Why does Intent.createChooser() need a BroadcastReceiver and how to implement?
I have a receiver setup, but it's for an AlarmManager that is set elsewhere and doesn't require the app to register / unregister.
Code for openFile(...)
In case it's needed, here is the content provider I've created.
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
String fileLocation = getContext().getCacheDir() + "/" + uri.getLastPathSegment();
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(new File(fileLocation), ParcelFileDescriptor.MODE_READ_ONLY);
return pfd;
}
It is possible to expose a file stored in your apps private directory via a ContentProvider. Here is some example code I made showing how to create a content provider that can do this.
Manifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.providertest"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="11" android:targetSdkVersion="15" />
<application android:label="#string/app_name"
android:icon="#drawable/ic_launcher"
android:theme="#style/AppTheme">
<activity
android:name=".MainActivity"
android:label="#string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="MyProvider"
android:authorities="com.example.prov"
android:exported="true"
/>
</application>
</manifest>
In your ContentProvider override openFile to return the ParcelFileDescriptor
#Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
File cacheDir = getContext().getCacheDir();
File privateFile = new File(cacheDir, "file.xml");
return ParcelFileDescriptor.open(privateFile, ParcelFileDescriptor.MODE_READ_ONLY);
}
Make sure you have copied your xml file to the cache directory
private void copyFileToInternal() {
try {
InputStream is = getAssets().open("file.xml");
File cacheDir = getCacheDir();
File outFile = new File(cacheDir, "file.xml");
OutputStream os = new FileOutputStream(outFile.getAbsolutePath());
byte[] buff = new byte[1024];
int len;
while ((len = is.read(buff)) > 0) {
os.write(buff, 0, len);
}
os.flush();
os.close();
is.close();
} catch (IOException e) {
e.printStackTrace(); // TODO: should close streams properly here
}
}
Now any other apps should be able to get an InputStream for your private file by using the content uri (content://com.example.prov/myfile.xml)
For a simple test, call the content provider from a seperate app similar to the following
private class MyTask extends AsyncTask<String, Integer, String> {
#Override
protected String doInBackground(String... params) {
Uri uri = Uri.parse("content://com.example.prov/myfile.xml");
InputStream is = null;
StringBuilder result = new StringBuilder();
try {
is = getApplicationContext().getContentResolver().openInputStream(uri);
BufferedReader r = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = r.readLine()) != null) {
result.append(line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try { if (is != null) is.close(); } catch (IOException e) { }
}
return result.toString();
}
#Override
protected void onPostExecute(String result) {
Toast.makeText(CallerActivity.this, result, Toast.LENGTH_LONG).show();
super.onPostExecute(result);
}
}
So Rob's answer is correct I assume but I did it a bit differently. As far as I understand, with the setting in in provider:
android:exported="true"
you are giving public access to all your files?! Anyway, a way to give only access to some files is to define file path permissions in the following way:
<provider
android:authorities="com.your.app.package"
android:name="android.support.v4.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/file_paths" />
</provider>
and then in your XML directory you define file_paths.xml file as follows:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path path="/" name="allfiles" />
<files-path path="tmp/" name="tmp" />
</paths>
now, the "allfiles" gives the same kind of public permission I guess as the option android:exported="true" but you don't really want that I guess so to define a subdirectory is the next line. Then all you have to do is store the files you want to share, there in that dir.
Next what you have to do is, as also Rob says, obtain a URI for this file. This is how I did it:
Uri contentUri = FileProvider.getUriForFile(context, "com.your.app.package", sharedFile);
Then, when I have this URI, I had to attach to it permissions for other app to use it. I was using or sending this file URI to camera app. Anyway this is the way how I got the other app package info and granted permissions to the URI:
PackageManager packageManager = getPackageManager();
List<ResolveInfo> list = packageManager.queryIntentActivities(cameraIntent, PackageManager.MATCH_DEFAULT_ONLY);
if (list.size() < 1) {
return;
}
String packageName = list.get(0).activityInfo.packageName;
grantUriPermission(packageName, sharedFileUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
ClipData clipData = ClipData.newRawUri("CAMFILE", sharedFileUri);
cameraIntent.setClipData(clipData);
cameraIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
cameraIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(cameraIntent, GET_FROM_CAMERA);
I left the code for camera as I did not want to take some other example I did not work on. But this way you see that you can attach permissions to URI itself.
The camera's thing is that I can set it via ClipData and then additionally set permissions. I guess in your case you only need FLAG_GRANT_READ_URI_PERMISSION as you are attaching a file to an email.
Here is the link to help on FileProvider as I based all of my post on the info I found there. Had some trouble finding a package info for camera app though.
Hope it helps.
None of the above answers helped. My problem was the point of passing intent extras but I'll walk you through all the steps to share a file.
Step 1: Create a Content Provider
This will make the file accessible to whichever app you want to share with.
Paste the following in the Manifest.xml file inside the <application></applicatio> tags
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="{YOUR_PACKAGE_NAME}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/provider_paths" />
</provider>
Step 2: Define paths accessible by the content provider
Do this by creating a file called provider_paths.xml (or a name of your choice) under res/xml. Put the following code in the file
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external"
path="." />
<external-files-path
name="external_files"
path="." />
<cache-path
name="cache"
path="." />
<external-cache-path
name="external_cache"
path="." />
<files-path
name="files"
path="." />
</paths>
Step 3: Create the Intent to share the file
Intent intentShareFile = new Intent(Intent.ACTION_SEND);
Uri uri = FileProvider.getUriForFile(getApplicationContext(), getPackageName() + ".fileprovider", fileToShare);
intentShareFile.setDataAndType(uri, URLConnection.guessContentTypeFromName(fileToShare.getName()));
//Allow sharing apps to read the file Uri
intentShareFile.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//Pass the file Uri instead of the path
intentShareFile.putExtra(Intent.EXTRA_STREAM,
uri);
startActivity(Intent.createChooser(intentShareFile, "Share File"));
If you need to permission other apps to see your app's private files (for Share, or otherwise) you might be able to save some time and just use v4 compat library's FileProvider
This is what i'm using:
I combined some answers and used the current AndroidX Doku:
Sharing files Android Development
Basic Process: You change the manifest to make it possible for other apps to access your local files. the filepath's that are allowed to be accessed from outside are found in the res/xml/filepaths.xml. When sharing you create an intent to share and set a Flag that temporarily allowed the other app to access your local files. Android documentation claims this is the secure way to share files
Step1: Add FileProvider to Manifest
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.YOUR.APP.PACKAGE.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/filepaths" />
</provider>
Step2: Add filepaths.xml to res/xml (if XML folder does not exists just create it yourself)
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path path="share/" name="share" />
</paths>
Step3: Use a function like this to start a file share. this function moves the file to the predefined share folder and creates a Url to it. the ShareDir is the File pointing to the files/share/ directory. the copy_File function copies the given file to the share directory in order to be accessible from the outside.
The function also makes it possible to Send the File as email with given header and body. if not needed just set it to empty strings
public void ShareFiles(Activity activity, List<File> files, String header, String body) {
ArrayList<Uri> uriList = new ArrayList<>();
if(files != null) {
for (File f : files) {
if (f.exists()) {
File file_in_share = copy_File(f, ShareDir);
if(file_in_share == null)
continue;
// Use the FileProvider to get a content URI
try {
Uri fileUri = FileProvider.getUriForFile(
activity,
"com.YOUR.APP.PACKAGE.fileprovider",
file_in_share);
uriList.add(fileUri);
} catch (IllegalArgumentException e) {
Log.e("File Selector",
"The selected file can't be shared: " + f.toString());
}
}
}
}
if(uriList.size() == 0)
{
Log.w("StorageModule", "ShareFiles: no files to share");
return;
}
Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setType("text/html");
intent.putExtra(Intent.EXTRA_SUBJECT, header);
intent.putExtra(Intent.EXTRA_TEXT, body);
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList);
activity.startActivity(Intent.createChooser(intent, "Share Files"));
}

Categories

Resources