Создать и поделиться файлом из внутреннего хранилища
Моя цель - создать файл XML на внутреннем хранилище и затем отправить его через общий ресурс.
Я могу создать файл XML, используя этот код
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();
Я застрял, пытаясь получить Uri в выходной файл, чтобы поделиться им. Сначала я попытался получить доступ к файлу, преобразовав файл в Uri
File outFile = context.getFileStreamPath(fileName);
return Uri.fromFile(outFile);
Это возвращает файл:///data/data/com.my.package/files/myfile.xml, но я не могу прикрепить его к электронному письму, загрузить и т. Д.
Если я вручную проверяю длину файла, это правильно и показывает, что есть разумный размер файла.
Затем я создал поставщика контента и попытался сослаться на файл, а он не является допустимым дескриптором файла. ContentProvider
кажется, никогда не называется какой-либо точки.
Uri uri = Uri.parse("content://" + CachedFileProvider.AUTHORITY + "/" + fileName);
return uri;
Это возвращает содержимое://com.my.package.provider/myfile.xml, но я проверяю файл и его нулевая длина.
Как правильно получить доступ к файлам? Нужно ли создавать файл с поставщиком контента? Если так, то как?
Обновить
Вот код, который я использую, чтобы поделиться. Если я выбираю Gmail, он отображается как вложение, но при отправке выдает ошибку. Невозможно отобразить вложение, а полученное письмо не имеет вложения.
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;
}
}
Я заметил, что есть Exception
брошенный сюда изнутри createChooser (...), но я не могу понять, почему он выброшен.
E / ActivityThread (572): активность com.android.internal.app.ChooserActivity просочилась в IntentReceiver com.android.internal.app.ResolverActivity$1@4148d658, который был первоначально зарегистрирован здесь. Вы пропускаете вызов unregisterReceiver()?
Я исследовал эту ошибку и не могу найти ничего очевидного. Обе эти ссылки предполагают, что мне нужно отменить регистрацию получателя.
- ChooserActivity утекла IntentReceiver
- Зачем Intent.createChooser() нужен BroadcastReceiver и как его реализовать?
У меня есть настройки приемника, но это для AlarmManager
это установлено в другом месте и не требует, чтобы приложение регистрировалось / отменяло регистрацию.
Код для openFile (...)
В случае необходимости, вот поставщик контента, который я создал.
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;
}
5 ответов
Можно открыть файл, хранящийся в личном каталоге ваших приложений, с помощью ContentProvider. Вот пример кода, который я сделал, показывающий, как создать контент-провайдера, который может это сделать.
манифест
<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>
В вашем ContentProvider переопределите openFile, чтобы вернуть 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);
}
Убедитесь, что вы скопировали свой XML-файл в каталог кэша.
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
}
}
Теперь любые другие приложения должны иметь возможность получить InputStream для вашего личного файла с помощью содержимого uri (content://com.example.prov/myfile.xml)
Для простого теста позвоните поставщику контента из отдельного приложения, подобного следующему
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);
}
}
Так что ответ Роба верен, но я сделал это немного по-другому. Насколько я понимаю, с настройкой в провайдере:
android:exported="true"
Вы предоставляете публичный доступ ко всем своим файлам?! В любом случае, способ предоставить доступ только к некоторым файлам состоит в том, чтобы определить разрешения пути к файлу следующим образом:
<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>
а затем в своем каталоге XML вы определяете файл file_paths.xml следующим образом:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path path="/" name="allfiles" />
<files-path path="tmp/" name="tmp" />
</paths>
теперь "allfiles" дает такое же общедоступное разрешение, как я полагаю, с опцией android:exported="true", но вы не очень хотите этого, так что определение подкаталога - следующая строка. Тогда все, что вам нужно сделать, это сохранить файлы, которыми вы хотите поделиться, в этом каталоге.
Следующее, что вам нужно сделать, это, как говорит Роб, получить URI для этого файла. Вот как я это сделал:
Uri contentUri = FileProvider.getUriForFile(context, "com.your.app.package", sharedFile);
Затем, когда у меня был этот URI, мне пришлось приложить к нему разрешения, чтобы другие приложения могли его использовать. Я использовал или отправлял этот файл URI в приложение камеры. В любом случае, вот как я получил информацию о другом пакете приложения и предоставил разрешения для 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);
Я оставил код для камеры, так как не хотел брать другой пример, над которым я не работал. Но, таким образом, вы видите, что можете прикрепить разрешения к самому URI.
Дело в том, что я могу установить его через ClipData, а затем дополнительно установить разрешения. Я предполагаю, что в вашем случае вам нужен только FLAG_GRANT_READ_URI_PERMISSION, так как вы прикрепляете файл к электронному письму.
Вот ссылка, чтобы помочь на FileProvider, так как я основывал все свои посты на информации, которую я там нашел. Однако возникли проблемы с поиском информации о пакете для приложения камеры.
Надеюсь, поможет.
Ни один из приведенных выше ответов не помог. Моя проблема заключалась в передаче дополнительных сведений о намерениях, но я проведу вас через все шаги, чтобы поделиться файлом.
Шаг 1. Создайте поставщика контента. Это сделает файл доступным для любого приложения, с которым вы хотите поделиться. Вставьте следующее в файл Manifest.xml внутри
<application></applicatio>
теги
<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>
Шаг 2: Определите пути, доступные поставщику контента. Для этого создайте файл с именем provider_paths.xml (или с именем по вашему выбору) в папке res/xml. Поместите следующий код в файл
<?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>
Шаг 3. Создайте намерение поделиться файлом
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"));
Если вам нужно разрешить другим приложениям просматривать личные файлы вашего приложения (для общего доступа или иным образом), вы можете сэкономить некоторое время и просто использовать FileProvider библиотеки v4 compat.
Это то, что я использую:
Я объединил некоторые ответы и использовал текущую AndroidX Doku:совместное использование файлов Android Development
Базовый процесс: вы изменяете манифест, чтобы другие приложения могли получить доступ к вашим локальным файлам. пути к файлам, к которым разрешен доступ извне, находятся в файле res/xml/filepaths.xml. При совместном использовании вы создаете намерение поделиться и устанавливаете флаг, который временно разрешает другому приложению доступ к вашим локальным файлам. Документация Android утверждает, что это безопасный способ обмена файлами
Шаг 1: добавьте FileProvider в манифест
<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>
Шаг 2: Добавьте filepaths.xml в res/xml (если папки XML не существует, просто создайте ее самостоятельно)
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path path="share/" name="share" />
</paths>
Шаг 3: Используйте подобную функцию, чтобы запустить общий файловый ресурс. эта функция перемещает файл в предопределенную общую папку и создает для него URL-адрес. ShareDir — это файл, указывающий на каталог files/share/. функция copy_File копирует данный файл в общий каталог, чтобы он был доступен извне. Эта функция также позволяет отправить файл по электронной почте с заданным заголовком и телом. если не нужно, просто установите пустые строки
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"));
}