Файл загрузки Firebase Storage в папку «Изображения» или «Фильм» в Android Q

Мне удалось загрузить файл из Firebase Storage в storage/emulated/0/Pictures, который является папкой по умолчанию для изображений, которые также используются в самых популярных приложениях, таких как Facebook или Instagram. Теперь, когда Android Q имеет множество поведенческих изменений при хранении и доступе к файлу, мое приложение больше не может загружать файл из корзины при работе в Android Q.

Это код, который записывает и загружает файл из корзины Firebase Storage в папки по умолчанию для массового хранилища, такие как «Изображения», «Фильмы», «Документы» и т. Д. Он работает на Android M, но на Q не будет работать.

    String state = Environment.getExternalStorageState();

    String type = "";

    if (downloadUri.contains("jpg") || downloadUri.contains("jpeg")
        || downloadUri.contains("png") || downloadUri.contains("webp")
        || downloadUri.contains("tiff") || downloadUri.contains("tif")) {
        type = ".jpg";
        folderName = "Images";
    }
    if (downloadUri.contains(".gif")){
        type = ".gif";
        folderName = "Images";
    }
    if (downloadUri.contains(".mp4") || downloadUri.contains(".avi")){
        type = ".mp4";
        folderName = "Videos";
    }


    //Create a path from root folder of primary storage
    File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + Environment.DIRECTORY_PICTURES + "/MY_APP_NAME");
    if (Environment.MEDIA_MOUNTED.equals(state)){
        try {
            if (dir.mkdirs())
                Log.d(TAG, "New folder is created.");
        }
        catch (Exception e) {
            e.printStackTrace();
            Crashlytics.logException(e);
        }
    }

    //Create a new file
    File filePath = new File(dir, UUID.randomUUID().toString() + type);

    //Creating a reference to the link
    StorageReference httpsReference = FirebaseStorage.getInstance().getReferenceFromUrl(download_link_of_file_from_Firebase_Storage_bucket);

    //Getting the file from the server
    httpsReference.getFile(filePath).addOnProgressListener(taskSnapshot ->                                 

showProgressNotification(taskSnapshot.getBytesTransferred(), taskSnapshot.getTotalByteCount(), requestCode)

);

При этом он загрузит файлы с сервера в хранилище вашего устройства по пути storage/emulated/0/Pictures/MY_APP_NAME, но с Android Q это больше не будет работать, так как многие API, такие как Environment.getExternalStorageDirectory(), устарели.

Использование android:requestLegacyExternalStorage=true - временное решение, но скоро перестанет работать на Android 11 и более поздних версиях.

Итак, мой вопрос: как я могу загружать файлы с помощью API хранилища Firebase в папке Picture или Movie по умолчанию, которая находится в корне, а не в Android/data/com.myapp.package/files.

Есть ли у MediaStore и ContentResolver решение для этого? Какие изменения мне нужно применить?


person Mihae Kheel    schedule 02.04.2020    source источник
comment
Вы можете скачать как Uri. После преобразования URI в растровое изображение. Затем сохраните файл в хранилище / emulated / 0 / Pictures / MY_APP_NAME с помощью MediaStore и ContentResolver.   -  person Kasım Özdemir    schedule 02.04.2020
comment
Или, если вы знаете URL-адрес файла, вы можете загрузить файл в виде растрового изображения с помощью Glide. Затем сохраните файл в хранилище / emulated / 0 / Pictures / MY_APP_NAME. Вы можете загрузить свой файл на внешнее хранилище, без проблем.   -  person Kasım Özdemir    schedule 02.04.2020
comment
Does MediaStore and ContentResolver has solution for this? . да. И код публиковался много раз за последние недели. Если бы вы только читали страницы с тегами `mediastore '.   -  person blackapps    schedule 02.04.2020
comment
вы хотите скачать его здесь storage / emulated / 0 / Pictures, не так ли?   -  person Kasım Özdemir    schedule 02.04.2020
comment
@ KasımÖzdemir да, но что, если это видео или PDF, а не просто изображение? Вы можете показать мне, как обращаться с возвращением Ури?   -  person Mihae Kheel    schedule 03.04.2020
comment
@blackapps Я уже потратил на поиски пару часов, но, похоже, ответа на такой вопрос пока нет. Как вы можете видеть здесь, приложению необходимо загрузить файл из хранилища Firebase, обычно storageReference.getFile (file_object_OR_a_uri) - самый простой способ, потому что он также предоставляет .addOnProgressListener (), который позволяет вам отслеживать загруженные байты и общее количество байтов, необходимых для скачать. Но этот подход больше не работает в Android Q. Использование другого API, такого как .getDownloadUrl () или .getBytes (file_size), не предоставит вам обратный вызов addOnProgressListener ().   -  person Mihae Kheel    schedule 03.04.2020
comment
Кажется, я не получаю правильный uri, необходимый для метода .getFile (), или что-то не так с выполнением. @blackapps см. firebase.google.com/docs/storage/android /   -  person Mihae Kheel    schedule 03.04.2020
comment
Используйте .getFile () для загрузки в getExternalFilesDir (). После загрузки используйте хранилище мультимедиа и преобразователь содержимого, чтобы скопировать файл в каталог изображений.   -  person blackapps    schedule 03.04.2020
comment
Довольно странно, что нет метода StorageReference, который делает это сразу. Это класс от Google. Или не?   -  person blackapps    schedule 03.04.2020
comment
@blackapps или лучше переместите это то, о чем я думаю, но безопасно ли это или потокобезопасно? Они всегда вносят радикальные изменения, даже не давая ответа на предстоящую проблему, подобную этой.   -  person Mihae Kheel    schedule 03.04.2020
comment
@blackapps StorageReference - это класс, включенный в Firebase Storage SDK Android com.google.firebase: firebase-storage: x.x.x   -  person Mihae Kheel    schedule 03.04.2020
comment
Непонятно, как бы вы сделали ход.   -  person blackapps    schedule 03.04.2020
comment
Вы можете протестировать его самостоятельно, поместив файл в буфер Firebase Storage. ›Получите его URL-адрес загрузки из самой консоли› а затем попробуйте загрузить его, как в приведенном выше коде, на Android ниже Q.   -  person Mihae Kheel    schedule 03.04.2020
comment
Вы говорите, что есть ссылка для скачивания? Затем вы можете использовать HttpUrlConnection для самостоятельной загрузки файлов непосредственно в изображения с помощью хранилища мультимедиа. И реализовать индикатор выполнения. Довольно стандартно все. Я не понимаю возни.   -  person blackapps    schedule 03.04.2020
comment
@blackapps Я постараюсь добавить больше в код, но если вы никогда не будете работать с Firebase, боюсь, вам будет непонятно.   -  person Mihae Kheel    schedule 03.04.2020
comment
Да, я не использую Firebase. Но если у вас есть URL-адрес для загрузки, загрузка и сохранение являются довольно стандартными.   -  person blackapps    schedule 03.04.2020
comment
@blackapps, поскольку то, что находится в коде, не нужно обрабатывать с помощью HttpUrlConnection, поскольку зависимость хранилища Firebase обрабатывает его уже с помощью StorageReference httpsReference = FirebaseStorage.getInstance (). getReferenceFromUrl (download_Url_of_file_from_Firebase_storage); затем просто вызовите httpsReference.getFile (fileObject) .addOnProgressListener (snapShot_where_you_can_listen_to_progress_and_get_bytes - ›{// Выполните вычисления с загруженными байтами и общим количеством байтов здесь});   -  person Mihae Kheel    schedule 03.04.2020
comment
@blackapps Я не думаю, что мы находимся на одной странице, поэтому я добавляю небольшой код и обновляю вопрос снова, но это все о том, как сохранить файл в папках по умолчанию, таких как Изображения, Документы, Фильмы, Музыка с Firebase Storage StorageReference, как в Android ниже Q   -  person Mihae Kheel    schedule 03.04.2020
comment
Я также предлагаю сначала попробовать поработать с Firebase Storage на Android, чтобы получить четкое представление о том, что происходит, если вышеуказанный вопрос вызывает путаницу.   -  person Mihae Kheel    schedule 03.04.2020
comment
Вам не нужно повторно указывать код для кода ниже Q. Я уже говорил вам, как это сделать для Q.   -  person blackapps    schedule 03.04.2020


Ответы (2)


Вот мое решение:

Скачать файл с помощью Glide

public void downloadFile(Context context, String url){
    String mimeType = getMimeType(url);
    Glide.with(context).asFile().load(url).listener(new RequestListener<File>() {
        @Override
        public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
            return false;
        }

        @Override
        public boolean onResourceReady(File resource, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
            saveFile(context,resource, mimeType);
            return false;
        }
    }).submit();
}

Получить файл mimeType

 public static String getMimeType(String url) {
    String mimeType = null;
    String extension = MimeTypeMap.getFileExtensionFromUrl(url);
    if (extension != null) {
        mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
    }
    return mimeType;
}

И сохраните файл на внешнем хранилище

public  Uri saveFile(Context context, File file, String mimeType) {
    String folderName = "Pictures";
    String extension = ".jpg";
    if(mimeType.endsWith("gif")){
        extension = ".gif";
    }else if(mimeType.startsWith("image/")){
        extension = ".jpg";
    }else if(mimeType.startsWith("video/")){
        extension = ".mp4";
        folderName = "Movies";
    }else if(mimeType.startsWith("audio/")){
        extension = ".mp3";
        folderName = "Music";
    }else if(mimeType.endsWith("pdf")){
        extension = ".pdf";
        folderName = "Documents";
    }
    if (android.os.Build.VERSION.SDK_INT >= 29) {
        ContentValues values = new ContentValues();
        values.put(MediaStore.Files.FileColumns.MIME_TYPE, mimeType);
        values.put(MediaStore.Files.FileColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
        values.put(MediaStore.Files.FileColumns.DATE_TAKEN, System.currentTimeMillis());
        values.put(MediaStore.Files.FileColumns.RELATIVE_PATH, folderName);
        values.put(MediaStore.Files.FileColumns.IS_PENDING, true);
        values.put(MediaStore.Files.FileColumns.DISPLAY_NAME, "file_" + System.currentTimeMillis() + extension);
        Uri uri = null;
        if(mimeType.startsWith("image/")){
            uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
        }else if(mimeType.startsWith("video/")){
            uri = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
        }else if(mimeType.startsWith("audio/")){
            uri = context.getContentResolver().insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
        }else if(mimeType.endsWith("pdf")){
            uri = context.getContentResolver().insert(MediaStore.Files.getContentUri("external"), values);
        }
        if (uri != null) {
            try {
                saveFileToStream(context.getContentResolver().openInputStream(Uri.fromFile(file)), context.getContentResolver().openOutputStream(uri));
                values.put(MediaStore.Video.Media.IS_PENDING, false);
                context.getContentResolver().update(uri, values, null, null);
                return uri;
            } catch (IOException e) {
                e.printStackTrace();
            }

            return null;
        }
    }
    return null;
}

сохранить файл в поток

private void saveFileToStream(InputStream input, OutputStream outputStream) throws IOException {
    try {
        try (OutputStream output = outputStream ){
            byte[] buffer = new byte[4 * 1024]; // or other buffer size
            int read;

            while ((read = input.read(buffer)) != -1) {
                output.write(buffer, 0, read);
            }
            output.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    } finally {
        input.close();
    }
}

Пробовал с эмулятором Android 29. Работает нормально.

Примечание. getExternalStorageDirectory () устарел на уровне API 29. Для повышения конфиденциальности пользователей прямой доступ к общим / внешним устройствам хранения не рекомендуется. Когда приложение нацелено на Build.VERSION_CODES.Q, путь, возвращаемый этим методом, больше не доступен приложениям напрямую. Приложения могут продолжать получать доступ к контенту, хранящемуся в общем / внешнем хранилище, путем перехода на альтернативные варианты, такие как Context # getExternalFilesDir (String), MediaStore или Intent # ACTION_OPEN_DOCUMENT.

person Kasım Özdemir    schedule 03.04.2020
comment
Спасибо за помощь, значит, я должен использовать Glide для загрузки файлов, заменяя использование Firebase Storage? Мое использование Firebase Storage здесь также имеет обратный вызов, поэтому я могу вычислить размер файла, который обновляется, и показать его прогресс через уведомление. - person Mihae Kheel; 03.04.2020
comment
Этот URL-адрес, который я установил для Glide, уже является URL-адресом хранилища Firebase. Я просто использовал Glide для загрузки файла. Вы можете скачать как хотите. Важный момент - saveFile func. - person Kasım Özdemir; 03.04.2020
comment
о, хорошо, хорошо, я пытаюсь реализовать это сейчас, исходя из моих требований, спасибо, отметим это как ответ, если он работает хорошо. - person Mihae Kheel; 03.04.2020
comment
Итак, я заменяю httpsReference.getFile () на httpsReference.getDownloadUrl (), как только я получу uri, я буду использовать Glide, чтобы загрузить его как обычный файл, верно? - person Mihae Kheel; 03.04.2020
comment
К сожалению, использование httpsReference.getDownloadUrl () означает, что я больше не могу использовать httpsReference.addOnProgressListener (), что означает, что я больше не могу отображать индикатор выполнения - person Mihae Kheel; 03.04.2020
comment
URL не нужен. Вы можете загрузить его как массив байтов, используя ссылку. Тогда вы можете использовать массив байтов вместо файла. - person Kasım Özdemir; 03.04.2020
comment
Но мне нужно следить за прогрессом загрузки с помощью addOnProgressListener (), кажется, только getFile () является методом, обеспечивающим этот обратный вызов - person Mihae Kheel; 03.04.2020
comment
Кроме того, Glide не является сетевой библиотекой с механизмом загрузки, что может вызвать огромные проблемы при загрузке тонны файла МБ. - person Mihae Kheel; 03.04.2020
comment
Я понял. Может быть, это работает на вас. - person Kasım Özdemir; 03.04.2020
comment
Большое спасибо, братан! У getByte () есть некоторые недостатки, но пока нет выбора, ваша идея и пример кода очень помогают. - person Mihae Kheel; 03.04.2020
comment
Нисколько. Если вы добавите прогресс в скольжение, он может быть таким, каким захотите. Я пробовал другие способы, но результатов не добился. - person Kasım Özdemir; 03.04.2020
comment
Да, но до тех пор, пока он загружает любой файл в папку по умолчанию, которую могут легко увидеть пользователи, я думаю, что это нормально, хотя я больше не могу отображать прогресс загрузки, как то, что может .getFile (файл) .addOnProgressListener (снимок - ›{}); я думаю, что это приемлемо на данный момент. Вы можете проверить мой ответ. - person Mihae Kheel; 03.04.2020

== НОВЫЙ ОТВЕТ ==

Если вы хотите отслеживать прогресс загрузки, вы можете использовать getStream() FirebaseStorage SDK следующим образом:

httpsReference.getStream((state, inputStream) -> {

                long totalBytes = state.getTotalByteCount();
                long bytesDownloaded = 0;

                byte[] buffer = new byte[1024];
                int size;

                while ((size = inputStream.read(buffer)) != -1) {
                    stream.write(buffer, 0, size);
                    bytesDownloaded += size;
                    showProgressNotification(bytesDownloaded, totalBytes, requestCode);
                }

                // Close the stream at the end of the Task
                inputStream.close();
                stream.flush();
                stream.close();

            }).addOnSuccessListener(taskSnapshot -> {
                showDownloadFinishedNotification(downloadedFileUri, downloadURL, true, requestCode);
                //Mark task as complete so the progress download notification whether success of fail will become removable
                taskCompleted();
                contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, false);
                resolver.update(uriResolve, contentValues, null, null);
            }).addOnFailureListener(e -> {
                Log.w(TAG, "download:FAILURE", e);

                try {
                    stream.flush();
                    stream.close();
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                    FirebaseCrashlytics.getInstance().recordException(ioException);
                }

                FirebaseCrashlytics.getInstance().recordException(e);

                //Send failure
                showDownloadFinishedNotification(null, downloadURL, false, requestCode);

                //Mark task as complete
                taskCompleted();
            });

== СТАРЫЙ ОТВЕТ ==

Наконец, спустя кучу часов мне удается это сделать, но в крайнем случае я использую .getBytes(maximum_file_size) вместо .getFile(file_object).

Большое спасибо @Kasim за то, что он предложил идею getBytes(maximum_file_size), а также пример кода, работающего с InputStream и OutputStream. Поиск по теме SO, связанной с вводом-выводом, также очень помогает здесь и здесь

Идея здесь в том, что .getByte(maximum_file_size) загрузит файл из корзины и вернет byte[] при обратном вызове addOnSuccessListener. Обратной стороной является то, что вы должны указать размер файла, который вы разрешили для загрузки, и вычисление прогресса загрузки не может быть выполнено AFAIK, если вы не поработаете с outputStream.write(0,0,0);. Я пытался записать его по частям, например здесь, но решение бросает ArrayIndexOutOfBoundsException, поскольку вы должны быть точными при работе с индексом в массиве.

Итак, вот код, который позволяет вам сохранять файл из хранилища Firebase в каталоги по умолчанию на вашем устройстве: storage/emulated/0/Pictures, storage/emulated/0/Movies, storage/emulated/0/Documents, you name it

//Member variable but depending on your scope
private ByteArrayInputStream inputStream;
private Uri downloadedFileUri;
private OutputStream stream;

//Creating a reference to the link
    StorageReference httpsReference = FirebaseStorage.getInstance().getReferenceFromUrl(downloadURL);

    Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
    String type = "";
    String mime = "";
    String folderName = "";

    if (downloadURL.contains("jpg") || downloadURL.contains("jpeg")
            || downloadURL.contains("png") || downloadURL.contains("webp")
            || downloadURL.contains("tiff") || downloadURL.contains("tif")) {
        type = ".jpg";
        mime = "image/*";
        contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        folderName = Environment.DIRECTORY_PICTURES;
    }
    if (downloadURL.contains(".gif")){
        type = ".gif";
        mime = "image/*";
        contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        folderName = Environment.DIRECTORY_PICTURES;
    }
    if (downloadURL.contains(".mp4") || downloadURL.contains(".avi")){
        type = ".mp4";
        mime = "video/*";
        contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
        folderName = Environment.DIRECTORY_MOVIES;
    }
    if (downloadURL.contains(".mp3")){
        type = ".mp3";
        mime = "audio/*";
        contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
        folderName = Environment.DIRECTORY_MUSIC;
    }

            final String relativeLocation = folderName + "/" + getString(R.string.app_name);

        final ContentValues contentValues = new ContentValues();
        contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, UUID.randomUUID().toString() + type);
        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mime); //Cannot be */*
        contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, relativeLocation);

        ContentResolver resolver = getContentResolver();
        Uri uriResolve = resolver.insert(contentUri, contentValues);

        try {

            if (uriResolve == null || uriResolve.getPath() == null) {
                throw new IOException("Failed to create new MediaStore record.");
            }

            stream = resolver.openOutputStream(uriResolve);

            //This is 1GB change this depending on you requirements
            httpsReference.getBytes(1024 * 1024 * 1024)
            .addOnSuccessListener(bytes -> {
                try {

                    int bytesRead;

                    inputStream = new ByteArrayInputStream(bytes);

                    while ((bytesRead = inputStream.read(bytes)) > 0) {
                        stream.write(bytes, 0, bytesRead);
                    }

                    inputStream.close();
                    stream.flush();
                    stream.close();
//FINISH

                } catch (IOException e) {
                    closeSession(resolver, uriResolve, e);
                    e.printStackTrace();
                    Crashlytics.logException(e);
                }
            });

        } catch (IOException e) {
            closeSession(resolver, uriResolve, e);
            e.printStackTrace();
            Crashlytics.logException(e);

        }
person Mihae Kheel    schedule 03.04.2020
comment
Причина, по которой мы не можем просто загрузить его через Glide, как то, что предлагает @Kasim или какой-либо менеджер загрузок, заключается в том, что у Firebase Storage есть набор правил, по которым вы можете редактировать его и ограничивать доступ в зависимости от ваших требований. Одно из моих основных правил заключается в том, что пользователь должен сначала пройти аутентификацию, прежде чем получить доступ для чтения и записи в нашу корзину. - person Mihae Kheel; 03.04.2020