Как улучшить поведение ConnectionRequest? - PullRequest
1 голос
/ 29 мая 2020

Я написал этот код:

public static Slider downloadStorageFile(String url, OnComplete<Integer> percentageCallback, OnComplete<String> filesavedCallback) {

        final String filename = getNewStorageFilename();

        ConnectionRequest cr = Rest.get(url).fetchAsBytes(response -> CN.callSerially(() -> filesavedCallback.completed(filename)));
        cr.setDestinationStorage(filename);

        Slider slider = new Slider();
        slider.addDataChangedListener((int type, int index) -> {
            CN.callSerially(() -> percentageCallback.completed(slider.getProgress()));
        });
        sliderBridgeMap.put(cr, slider);
        SliderBridge.bindProgress(cr, slider);

        return slider;
    }

В основном, как вы можете догадаться, он асинхронно загружает файл, предлагает два обратных вызова, которые будут запускаться на EDT, и немедленно возвращает слайдер, который отслеживает прогресс загрузки.

На Android загрузка продолжается и завершается, даже когда приложение находится в фоновом режиме. С другой стороны, на iOS приложение должно оставаться активным и постоянно находиться на переднем плане.

  1. Возможно ли завершить загрузку даже на iOS, когда приложение переходит в фоновый режим?

  2. В качестве альтернативы, могу ли я получить ConnectionRequest.retry(), , который автоматически вызывается моим обработчиком сетевых ошибок, когда приложение возвращается на передний план на iOS, чтобы перезапустить с того места, где была загружена загрузка, вместо того, чтобы перезапускать ее с нуля?

  3. Еще лучше, могу ли я получить обе точки 1 и 2?

Ответы [ 2 ]

1 голос
/ 01 июня 2020

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

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

Я разработал решение, которое имеет плюсы и минусы, и оно позволяет обойти проблему.

Плюсы: всегда позволяет вам завершить загрузку, даже если очень тяжелый (например, 100 МБ), даже если соединение нестабильно (сетевые ошибки) и даже если приложение временно работает в фоновом режиме

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

Предварительное условие 1: при обработке ошибок глобальной сети должен быть c .retry() автомат, как в этом коде : Различие guish между ошибками на стороне сервера и проблемами подключения

Предварительное условие 2: для реализации getFileSizeWithoutDownload(String url) и Wrapper см .: { ссылка }

Пояснение: код не требует пояснений. Обычно он загружает 512 Кбайт за раз, а затем объединяет их с выводом. Если происходит сетевая ошибка (и если на iOS приложение переходит в фоновый режим), все, что уже было загружено, не теряется (теряется максимум только последний фрагмент размером 512 Кбайт). По мере загрузки каждого фрагмента ConnectionRequest вызывает себя, изменяя заголовок для частичной загрузки. Обратный вызов filesavedCallback вызывается только после завершения всей загрузки.

Код:

    public static void downloadToStorage(String url, OnComplete<Integer> percentageCallback, OnComplete<String> filesavedCallback) throws IOException {

        final String output = getNewStorageFilename(); // get a new random available Storage file name
        final long fileSize = getFileSizeWithoutDownload(url); // total expected download size
        final int splittingSize = 512 * 1024; // 512 kbyte, size of each small download
        Wrapper<Integer> downloadedTotalBytes = new Wrapper<>(0);
        OutputStream out = Storage.getInstance().createOutputStream(output); // leave it open to append partial downloads
        Wrapper<Integer> completedPartialDownload = new Wrapper<>(0);

        ConnectionRequest cr = new GZConnectionRequest();
        cr.setUrl(url);
        cr.setPost(false);
        if (fileSize > splittingSize) {
            // Which byte should the download start from?
            cr.addRequestHeader("Range", "bytes=0-"  + splittingSize);
            cr.setDestinationStorage("split-" + output);
        } else {
            Util.cleanup(out);
            cr.setDestinationStorage(output);
        }
        cr.addResponseListener(a -> {
            CN.callSerially(() -> {
                try {
                    // We append the just saved partial download to the output, if it exists
                    if (Storage.getInstance().exists("split-" + output)) {
                        InputStream in = Storage.getInstance().createInputStream("split-" + output);
                        Util.copyNoClose(in, out, 8192);
                        Util.cleanup(in);
                        Storage.getInstance().deleteStorageFile("split-" + output);
                        completedPartialDownload.set(completedPartialDownload.get() + 1);
                    }
                    // Is the download finished?
                    if (fileSize <= 0 || completedPartialDownload.get() * splittingSize >= fileSize || downloadedTotalBytes.get() >= fileSize) {
                        // yes, download finished
                        Util.cleanup(out);
                        filesavedCallback.completed(output);
                    } else {
                        // no, it's not finished, we repeat the request after updating the "Range" header
                        cr.addRequestHeader("Range", "bytes=" + downloadedTotalBytes.get() + "-" + (downloadedTotalBytes.get() + splittingSize));
                        NetworkManager.getInstance().addToQueue(cr);
                    }
                } catch (IOException ex) {
                    Log.p("Error in appending splitted file to output file", Log.ERROR);
                    Log.e(ex);
                    Server.sendLogAsync();
                }
            });
        });
        NetworkManager.getInstance().addToQueue(cr);
        NetworkManager.getInstance().addProgressListener((NetworkEvent evt) -> {
            if (cr == evt.getConnectionRequest() && fileSize > 0) {
                downloadedTotalBytes.set(completedPartialDownload.get() * splittingSize + evt.getSentReceived());
                // the following casting to long is necessary when the file is bigger than 21MB, otherwise the result of the calculation is wrong
                percentageCallback.completed((int) ((long) downloadedTotalBytes.get() * 100 / fileSize));
            }
        });
    }

Я пробовал это решение в симуляторе на Android и iOS в различные сетевые условия, при загрузке 100 МБ и периодическом перемещении приложения в фоновом режиме (или разрешении go автоматически). Во всех случаях приложение успешно завершает загрузку. Однако различия между Android и iOS остаются, когда приложение работает в фоновом режиме.

Надеюсь, это полезно. Если кто-то хочет еще больше улучшить этот код, он может добавить еще один ответ :)

1 голос
/ 30 мая 2020

Это проблема c и Android, поскольку ОС может внезапно прервать вашу текущую загрузку, когда система ограничена. Например, в инструментах разработчика просто немедленно включите действия по уничтожению и увидите, что ваша загрузка d ie, как только вы сворачиваете приложение.

Большинство устройств этого не делают, но если устройство работает в режиме экономии заряда батареи, это может произойти.

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

См. JavaDo c для класса здесь: https://www.codenameone.com/javadoc/com/codename1/background/BackgroundFetch.html

И немного устаревшее сообщение в блоге на эту тему: https://www.codenameone.com/blog/background-fetch.html

...