Создание и совместное использование файла из внутренней памяти

Моя цель — создать XML-файл во внутреннем хранилище, а затем отправить его через общий ресурс Intent.

Я могу создать файл 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);

Это возвращает file:///data/data/com.my.package/files/myfile.xml, но я не могу прикрепить его к электронному письму, загрузить и т. д.

Если я вручную проверяю длину файла, это правильно и показывает, что размер файла разумный.

Затем я создал поставщика контента и попытался сослаться на файл, но это недопустимый дескриптор файла. Кажется, ContentProvider никогда не называлась любой точкой.

Uri uri = Uri.parse("content://" + CachedFileProvider.AUTHORITY + "/" + fileName);
return uri;

Это возвращает content://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;
    }
}

Я заметил, что внутри createChooser(...) здесь выбрасывается Exception, но я не могу понять, почему это выбрасывается.

E/ActivityThread(572): действие com.android.internal.app.ChooserActivity привело к утечке IntentReceiver com.android.internal.app.ResolverActivity$1@4148d658, который изначально был зарегистрирован здесь. Вы пропустили вызов unregisterReceiver()?

Я исследовал эту ошибку и не могу найти ничего очевидного. Обе эти ссылки предполагают, что мне нужно отменить регистрацию приемника.

У меня есть настройка приемника, но она предназначена для 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;
}

person Kirk    schedule 29.08.2012    source источник
comment
Я думаю, что первый метод должен работать, а не создавать contentProvider. ContentProvider используется для возврата данных, а не всего файла, он не будет соответствовать вашим потребностям. Я предполагаю, что нет приложений для обработки вашего xml. Можете ли вы опубликовать код, в котором создается ваше намерение поделиться   -  person nandeesh    schedule 29.08.2012
comment
Добавлено больше кода на случай, если это сделает его более очевидным, а также исключение, которое я не понимаю, связано оно или нет.   -  person Kirk    schedule 30.08.2012
comment
Вы создали ContentProvider? Если у вас есть, можете ли вы опубликовать свой код для него. Если вы этого не сделали, вам нужно создать ContentProvider и переопределить метод openFile. Этот метод будет вызываться gmail при попытке открыть файл, связанный с uri content://com.my.package.provider/myfile.xml.   -  person Rob    schedule 30.08.2012
comment
Я добавил метод openFile(...) созданного мной ContentProvider.   -  person Kirk    schedule 30.08.2012
comment
см. мой ответ здесь: stackoverflow.com/questions/15377373/   -  person dangalg    schedule 26.01.2015


Ответы (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);
    }
}
person Rob    schedule 30.08.2012
comment
Спасибо за ответ. Я надеюсь использовать ContentProvider, если это возможно, для доступа к личным данным. getExternalCacheDir() не поддерживается в API 7, на который мне нужно ориентироваться. - person Kirk; 30.08.2012
comment
В API 7 вы можете использовать getExternalStorageDirectory() для получения общедоступного каталога. Я не уверен, что можно предоставить публичный доступ к частной части файловой системы приложений, даже с помощью ContentProvider. - person Rob; 30.08.2012
comment
Спасибо за предложение. Я хочу писать во внутреннюю память, а не на SD-карту. Любые мысли о том, как это сделать? - person Kirk; 31.08.2012
comment
Извините, Кирк, я ошибся, можно получить доступ к личным файлам через ContentProvider. Ваша проблема беспокоила меня, поэтому я попробовал, ответ был обновлен, чтобы показать вам, как я создал ContentProvider, чтобы я мог открывать личные файлы. - person Rob; 31.08.2012
comment
Спасибо вам за вашу работу. Я не смогу попробовать это позже, но это то, что я собираюсь сделать. - person Kirk; 31.08.2012
comment
Я попытался реализовать это, и ошибка должна быть где-то еще, так как моя реализация уже практически такая же. К сожалению, я отказываюсь от этого сейчас, так как я потратил 4 дня на разные попытки просто поделиться файлом из внутренней памяти. ОЧЕНЬ расстроен. Я отмечу ваш ответ, так как вы проделали так много работы. - person Kirk; 01.09.2012
comment
Наиболее вероятная проблема, с которой вы столкнулись с вашим ContentProvider, - это URI, который вы передаете, совпадающий с URI в вашем манифесте, дважды проверьте их совместимость. Попробуйте изолировать отладку ContentProvider от части совместного использования вашего приложения, создайте небольшое тестовое приложение, которое взаимодействует с вашим основным поставщиком содержимого приложений, как только вы получите эту работу, начните пытаться поделиться URI с приложением электронной почты. Удачи =) - person Rob; 01.09.2012
comment
В этом подходе есть недостаток. На самом деле метод copyFileToInternal() создает дубликат файла в кэш-памяти для каждого файла во внутренней памяти. Итак, если у вас есть 100 МБ файлов во внутренней памяти, этот подход увеличит память телефона до 200 МБ за счет создания дубликатов файлов в кэш-памяти. Я думаю, это не надежное решение. - person Yasir Ali; 09.05.2014
comment
Спасибо. Это очень полезный и экономящий время ответ. - person mifthi; 05.08.2016
comment
Android на самом деле не разрешает android:exported=true в целях безопасности, поскольку все данные приложения не могут быть доступны для всех приложений открыто. Вам придется придерживаться android:exported=false - person Enock Lubowa; 16.07.2021

Итак, я предполагаю, что ответ Роба правильный, но я сделал это немного по-другому. Насколько я понимаю, с настройкой в ​​провайдере:

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, так как я основывал весь свой пост на информация, которую я нашел там. Однако возникли проблемы с поиском информации о пакете для приложения камеры.

Надеюсь, поможет.

person Dusko    schedule 06.11.2015

Если вам нужно разрешить другим приложениям просматривать личные файлы вашего приложения (для общего доступа или иным образом), вы можете сэкономить время и просто использовать библиотеку совместимости v4 FileProvider

person Splash    schedule 14.04.2015

Это то, что я использую:

Я объединил некоторые ответы и использовал текущий AndroidX Doku: Общий доступ к файлам Разработка Android

Базовый процесс: вы изменяете манифест, чтобы другие приложения могли получить доступ к вашим локальным файлам. пути к файлам, к которым разрешен доступ извне, находятся в файле 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"));
}
person save_jeff    schedule 09.12.2020

Ни один из приведенных выше ответов не помог. Моя проблема заключалась в передаче дополнительных сведений о намерениях, но я проведу вас через все шаги, чтобы поделиться файлом.

Шаг 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"));
person Enock Lubowa    schedule 15.07.2021