a080975

Use service instead of asynctask to download dictionaries in the background

app/src/main/java/eu/lepiller/nani/DictionaryActivity.java

6767
        });
6868
    }
6969
70+
    @Override
71+
    protected void onResume() {
72+
        super.onResume();
73+
        updateDataset();
74+
    }
75+
7076
    private class DownloadTask extends AsyncTask<String, Integer, Integer> {
7177
        private SwipeRefreshLayout refresher;
7278

114120
        protected void onPostExecute(Integer i) {
115121
            super.onPostExecute(i);
116122
            refresher.setRefreshing(false);
117-
            DictionaryFactory.updatePackageList();
118-
            dictionaries.clear();
119-
            dictionaries.addAll(DictionaryFactory.getDictionnaries(getApplicationContext()));
120-
            adapter.notifyDataSetChanged();
123+
            updateDataset();
121124
            if(i == null)
122125
                return;
123126
            Snackbar.make(findViewById(R.id.dictionary_view), getString(i),

125128
        }
126129
    }
127130
131+
    private void updateDataset() {
132+
        DictionaryFactory.updatePackageList();
133+
        dictionaries.clear();
134+
        dictionaries.addAll(DictionaryFactory.getDictionnaries(getApplicationContext()));
135+
        adapter.notifyDataSetChanged();
136+
    }
137+
128138
    @Override
129139
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
130140
        super.onActivityResult(requestCode, resultCode, data);

app/src/main/java/eu/lepiller/nani/DictionaryDownloadActivity.java

11
package eu.lepiller.nani;
22
3-
import android.app.NotificationManager;
3+
import android.content.Intent;
44
import android.graphics.drawable.Drawable;
5-
import android.os.AsyncTask;
65
import android.os.Build;
76
8-
import androidx.core.app.NotificationCompat;
97
import androidx.appcompat.app.AppCompatActivity;
8+
import androidx.lifecycle.LiveData;
9+
import androidx.lifecycle.Observer;
10+
1011
import android.os.Bundle;
1112
import android.util.Log;
12-
import android.util.Pair;
1313
import android.view.View;
1414
import android.widget.ImageView;
1515
import android.widget.LinearLayout;
1616
import android.widget.ProgressBar;
1717
import android.widget.TextView;
1818
19-
import com.google.android.material.snackbar.Snackbar;
20-
21-
import java.io.File;
22-
import java.io.FileOutputStream;
23-
import java.io.IOException;
24-
import java.io.InputStream;
25-
import java.io.OutputStream;
26-
import java.net.HttpURLConnection;
27-
import java.net.URL;
28-
import java.util.List;
29-
import java.util.Map;
30-
3119
import eu.lepiller.nani.dictionary.Dictionary;
3220
import eu.lepiller.nani.dictionary.DictionaryFactory;
3321
3422
public class DictionaryDownloadActivity extends AppCompatActivity {
35-
    final static String TAG = "DWN";
36-
    final static int notificationID = 1111;
3723
    final static String EXTRA_DICTIONARY = "eu.lepiller.nani.extra.DICTIONARY";
24+
    private final static String TAG = "DictionaryDownload";
3825
3926
    Dictionary d;
40-
    DownloadTask currentDownloadTask = null;
41-
    ImageView download_button = null;
4227
28+
    ImageView download_button = null;
4329
    ProgressBar download_bar;
4430
45-
    NotificationManager manager;
46-
    NotificationCompat.Builder builder;
31+
    Observer<DictionaryDownloadService.DownloadData> observer;
4732
4833
    View.OnClickListener download_click_listener = new View.OnClickListener() {
4934
        @Override
5035
        public void onClick(View v) {
51-
            if(currentDownloadTask != null)
52-
                return;
53-
54-
            currentDownloadTask = new DownloadTask();
55-
            builder.setProgress(0,0,true);
56-
            builder.setOnlyAlertOnce(true);
57-
            manager.notify(notificationID, builder.build());
58-
            currentDownloadTask.execute(d);
59-
            setIcon(download_button, R.drawable.ic_pause);
60-
            download_button.setContentDescription(getString(R.string.alt_text_pause));
61-
62-
            download_button.setOnClickListener(pause_click_listener);
36+
            Intent serviceIntent = new Intent(DictionaryDownloadActivity.this, DictionaryDownloadService.class);
37+
            serviceIntent.setAction(DictionaryDownloadService.DOWNLOAD_ACTION);
38+
            serviceIntent.putExtra(EXTRA_DICTIONARY, d.getName());
39+
            startService(serviceIntent);
6340
        }
6441
    };
6542
6643
    View.OnClickListener pause_click_listener = new View.OnClickListener() {
6744
        @Override
6845
        public void onClick(View v) {
69-
            currentDownloadTask.cancel(true);
70-
            setIcon(download_button, R.drawable.ic_nani_download);
71-
            download_button.setContentDescription(getString(R.string.alt_text_download));
72-
73-
            download_button.setOnClickListener(download_click_listener);
74-
            currentDownloadTask = null;
46+
            Intent serviceIntent = new Intent(DictionaryDownloadActivity.this, DictionaryDownloadService.class);
47+
            serviceIntent.setAction(DictionaryDownloadService.PAUSE_ACTION);
48+
            serviceIntent.putExtra(EXTRA_DICTIONARY, d.getName());
49+
            startService(serviceIntent);
7550
        }
7651
    };
7752

8661
            name = extras.getString(EXTRA_DICTIONARY);
8762
8863
        d = DictionaryFactory.getByName(this, name);
89-
90-
        manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
91-
        builder =  new NotificationCompat.Builder(DictionaryDownloadActivity.this, "dico_dll")
92-
                .setSmallIcon(R.drawable.ic_launcher_foreground)
93-
                .setContentTitle(d.getName())
94-
                .setContentText(getString(R.string.downloading));
64+
        download_bar = findViewById(R.id.download_progress);
65+
        download_button = findViewById(R.id.download_button);
9566
9667
        setResult(DictionaryActivity.DICO_REQUEST);
97-
        updateLayout(d);
68+
69+
        updateLayout(false);
70+
    }
71+
72+
    @Override
73+
    protected void onResume() {
74+
        super.onResume();
75+
        LiveData<DictionaryDownloadService.DownloadData> data = DictionaryDownloadService.getData();
76+
        final String dictionaryName = d.getName();
77+
78+
        if (dictionaryName == null)
79+
            return;
80+
81+
        observer = new Observer<DictionaryDownloadService.DownloadData>() {
82+
            @Override
83+
            public void onChanged(DictionaryDownloadService.DownloadData downloadData) {
84+
                boolean pending = false;
85+
                for(String n: downloadData.downloading) {
86+
                    if(n.equals(dictionaryName)) {
87+
                        pending = true;
88+
                        break;
89+
                    }
90+
                }
91+
                Log.d(TAG, "onChanged: " + downloadData.currentName);
92+
93+
                if(dictionaryName.equals(downloadData.currentName)) {
94+
                    download_bar.setMax(100);
95+
                    if(downloadData.currentProgress >= 0) {
96+
                        download_bar.setIndeterminate(false);
97+
                        download_bar.setProgress(downloadData.currentProgress);
98+
                    } else {
99+
                        download_bar.setIndeterminate(true);
100+
                    }
101+
                    updateLayout(true);
102+
                } else if(pending) {
103+
                    download_bar.setIndeterminate(true);
104+
                    updateLayout(true);
105+
                } else {
106+
                    if(d.isDownloaded()) {
107+
                        download_bar.setProgress(100);
108+
                    } else {
109+
                        download_bar.setProgress(d.getSize()*100 / d.getExpectedFileSize());
110+
                    }
111+
                    updateLayout(false);
112+
                }
113+
            }
114+
        };
115+
        data.observe(this, observer);
116+
    }
117+
118+
    @Override
119+
    protected void onPause() {
120+
        LiveData<DictionaryDownloadService.DownloadData> data = DictionaryDownloadService.getData();
121+
        data.removeObserver(observer);
122+
        super.onPause();
98123
    }
99124
100-
    private void updateLayout(final Dictionary d) {
125+
    private void updateLayout(final boolean isDownloading) {
126+
        Log.d(TAG, "updateLayout: " + isDownloading);
101127
        TextView name_view = findViewById(R.id.name_view);
102128
        name_view.setText(d.getName());
103129

106132
        TextView full_description_view = findViewById(R.id.extended_info_view);
107133
        full_description_view.setText(d.getFullDescription());
108134
109-
        download_bar = findViewById(R.id.download_progress);
110-
        if(d.isDownloaded()) {
111-
            download_bar.setProgress(100);
112-
        } else {
113-
            download_bar.setProgress(d.getSize()*100 / d.getExpectedFileSize());
114-
        }
115-
116135
        ImageView icon_view = findViewById(R.id.icon_view);
117136
        Drawable icon = d.getDrawable(getApplicationContext());
118137
        if (icon != null) {
119138
            icon_view.setImageDrawable(icon);
120139
        }
121140
122-
        int drawableResId = d.isDownloaded() ? R.drawable.ic_nani_refresh : R.drawable.ic_nani_download;
123-
        download_button = findViewById(R.id.download_button);
124-
        if(d.isDownloaded())
125-
            download_button.setContentDescription(getString(R.string.alt_text_refresh));
126-
        else
127-
            download_button.setContentDescription(getString(R.string.alt_text_download));
141+
        int drawableResId;
142+
        if(isDownloading) {
143+
            drawableResId = R.drawable.ic_pause;
144+
            download_button.setContentDescription(getString(R.string.alt_text_pause));
145+
            download_button.setOnClickListener(pause_click_listener);
146+
        } else {
147+
            if(d.isDownloaded()) {
148+
                drawableResId = R.drawable.ic_nani_refresh;
149+
                download_button.setContentDescription(getString(R.string.alt_text_refresh));
150+
            } else {
151+
                drawableResId = R.drawable.ic_nani_download;
152+
                download_button.setContentDescription(getString(R.string.alt_text_download));
153+
            }
154+
            download_button.setOnClickListener(download_click_listener);
155+
        }
156+
128157
        setIcon(download_button, drawableResId);
129158
        download_button.setEnabled(true);
130159

133162
        setIcon(trash_button, drawableResId);
134163
135164
        LinearLayout remove_layout = findViewById(R.id.remove_layout);
136-
        remove_layout.setVisibility(d.getSize() > 0? View.VISIBLE: View.INVISIBLE);
165+
        remove_layout.setVisibility(d.getSize() > 0 && !isDownloading? View.VISIBLE: View.INVISIBLE);
137166
138167
        TextView size_view = findViewById(R.id.size_view);
139168
        int size = d.getSize();

144173
        else
145174
            size_view.setText(String.format(getResources().getString(R.string.dictionary_size_mb), size/1000000));
146175
147-
        download_button.setOnClickListener(download_click_listener);
148-
149176
        trash_button.setOnClickListener(new View.OnClickListener() {
150177
            @Override
151178
            public void onClick(View v) {
152179
                d.remove();
153-
                updateLayout(d);
180+
                updateLayout(isDownloading);
154181
            }
155182
        });
156183
    }

164191
        }
165192
        download_button.setImageDrawable(drawable);
166193
    }
167-
168-
    private void notifyProgress(int progress, int max) {
169-
        builder.setProgress(max, progress, false);
170-
        manager.notify(notificationID, builder.build());
171-
    }
172-
173-
    private void removeProgress() {
174-
        manager.cancel(notificationID);
175-
    }
176-
177-
    private class DownloadTask extends AsyncTask<Dictionary, Integer, String> {
178-
        private Dictionary d;
179-
        private int lastNotifiedProgress = -1;
180-
181-
        @Override
182-
        protected String doInBackground(Dictionary... sDicos) {
183-
            InputStream input = null;
184-
            OutputStream output = null;
185-
            HttpURLConnection connection = null;
186-
            d = sDicos[0];
187-
188-
            Log.d(TAG, "Start downloading");
189-
190-
            for (Map.Entry<String, Pair<File, File>> e : d.getDownloads().entrySet()) {
191-
                try {
192-
                    String uri = e.getKey();
193-
                    Log.d(TAG, "URL: " + uri);
194-
195-
                    File temporaryFile = e.getValue().first;
196-
                    File cacheFile = e.getValue().second;
197-
198-
                    // Download a .sha256 file if it exists
199-
                    URL url = new URL(uri + ".sha256");
200-
                    byte[] data = new byte[4096];
201-
                    int count;
202-
                    File file;
203-
204-
                    connection = (HttpURLConnection) url.openConnection();
205-
                    connection.connect();
206-
                    if(connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
207-
                        input = connection.getInputStream();
208-
                        file = new File(cacheFile + ".sha256");
209-
                        if(file.getParentFile() == null || (!file.getParentFile().exists() && !file.getParentFile().mkdirs()))
210-
                            Log.w(TAG, "could not create parent of " + file);
211-
                        File old_file = new File(file + ".old");
212-
                        if(old_file.exists()) {
213-
                            if(!old_file.delete())
214-
                                Log.w(TAG, "could not delete file " + old_file);
215-
                        }
216-
                        if(file.exists()) {
217-
                            if(!file.renameTo(old_file))
218-
                                Log.w(TAG, "could not rename "+ file + " to " + old_file);
219-
                        }
220-
                        output = new FileOutputStream(file);
221-
                        while((count = input.read(data)) != -1) {
222-
                            if (isCancelled()) {
223-
                                input.close();
224-
                                return null;
225-
                            }
226-
                            output.write(data, 0, count);
227-
                        }
228-
229-
                        if(old_file.exists()) {
230-
                            // Check that we continue to download the same file
231-
                            String old_hash = Dictionary.readSha256FromFile(old_file);
232-
                            String new_hash = Dictionary.readSha256FromFile(file);
233-
234-
                            if(old_hash.compareTo(new_hash) != 0) {
235-
                                // Remove file that is now different
236-
                                d.remove();
237-
                            }
238-
239-
                            if(!old_file.delete())
240-
                                Log.w(TAG, "could not delete file " + old_file);
241-
                        }
242-
                    }
243-
244-
                    // .sha256 file now downloaded
245-
                    url = new URL(uri);
246-
                    file = temporaryFile;
247-
                    if(file.getParentFile() == null || (!file.getParentFile().exists() && !file.getParentFile().mkdirs()))
248-
                        Log.w(TAG, "could not create parent of " + file);
249-
                    long expectedLength = -1;
250-
                    boolean acceptRanges = false;
251-
                    if(file.exists()) {
252-
                        // if file exists, it is not fully downloaded, so we check whether we can
253-
                        // do a partial download
254-
                        connection = (HttpURLConnection) url.openConnection();
255-
                        connection.setRequestMethod("HEAD");
256-
                        connection.connect();
257-
                        if(connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
258-
                            expectedLength = connection.getContentLength();
259-
                            acceptRanges = connection.getHeaderFields().containsKey("accept-ranges");
260-
                            if (acceptRanges) {
261-
                                List<String> headers = connection.getHeaderFields().get("accept-ranges");
262-
                                if(headers != null) {
263-
                                    for (String h : headers) {
264-
                                        if (h.toLowerCase().compareTo("none") == 0)
265-
                                            acceptRanges = false;
266-
                                    }
267-
                                }
268-
                            }
269-
                            Log.d(TAG, "Can do range? " + acceptRanges);
270-
                        }
271-
                    }
272-
273-
                    long total = 0;
274-
                    connection = (HttpURLConnection) url.openConnection();
275-
                    if(expectedLength > 0 && acceptRanges && file.length() < expectedLength) {
276-
                        connection.addRequestProperty("Range", "bytes=" + file.length() + "-" + (expectedLength-1));
277-
                        total = file.length();
278-
                        output = new FileOutputStream(file, true);
279-
                    } else {
280-
                        output = new FileOutputStream(file);
281-
                    }
282-
                    connection.connect();
283-
284-
                    // expect HTTP 200 OK, so we don't mistakenly save error report
285-
                    // instead of the file
286-
                    if (connection.getResponseCode() != HttpURLConnection.HTTP_OK &&
287-
                            connection.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) {
288-
                        Log.e(TAG, "Server returned HTTP " + connection.getResponseCode()
289-
                                + " " + connection.getResponseMessage());
290-
                        return "Server returned HTTP " + connection.getResponseCode()
291-
                                + " " + connection.getResponseMessage();
292-
                    }
293-
294-
                    // this will be useful to display download percentage
295-
                    // might be -1: server did not report the length
296-
                    int fileLength = connection.getContentLength();
297-
298-
                    // download the file
299-
                    input = connection.getInputStream();
300-
301-
                    while ((count = input.read(data)) != -1) {
302-
                        // allow canceling with back button
303-
                        if (isCancelled()) {
304-
                            input.close();
305-
                            return null;
306-
                        }
307-
                        total += count;
308-
                        output.write(data, 0, count);
309-
                        // publishing the progress....
310-
                        if (fileLength > 0) {// only if total length is known
311-
                            float progress = (int) (total * 100 / fileLength);
312-
                            if(lastNotifiedProgress < progress - 2) {
313-
                                lastNotifiedProgress = (int)progress;
314-
                                publishProgress((int) progress);
315-
                                output.flush();
316-
                            }
317-
                        }
318-
                    }
319-
                } catch (Exception ex) {
320-
                    Log.e(TAG, ex.toString());
321-
                    return ex.toString();
322-
                } finally {
323-
                    try {
324-
                        if (output != null)
325-
                            output.close();
326-
                        if (input != null)
327-
                            input.close();
328-
                    } catch (IOException ignored) {
329-
                    }
330-
331-
                    if (connection != null)
332-
                        connection.disconnect();
333-
                }
334-
            }
335-
            return null;
336-
        }
337-
338-
        @Override
339-
        protected void onPreExecute() {
340-
            super.onPreExecute();
341-
            download_bar.setIndeterminate(true);
342-
        }
343-
344-
        @Override
345-
        protected void onProgressUpdate(Integer... progress) {
346-
            super.onProgressUpdate(progress);
347-
            // if we get here, length is known, now set indeterminate to false
348-
            download_bar.setIndeterminate(false);
349-
            download_bar.setMax(100);
350-
            download_bar.setProgress(progress[0]);
351-
            notifyProgress(progress[0], 100);
352-
        }
353-
354-
355-
        @Override
356-
        protected void onPostExecute(String result) {
357-
            download_bar.setProgress(100);
358-
            removeProgress();
359-
            currentDownloadTask = null;
360-
            d.switchToCacheFile();
361-
            if(!d.isDownloaded() && d.getExpectedFileSize() <= d.getSize()) {
362-
                Snackbar.make(findViewById(R.id.name_view), getString(R.string.error_dico_checksum_fail),
363-
                        Snackbar.LENGTH_LONG).show();
364-
                d.remove();
365-
            }
366-
            updateLayout(d);
367-
        }
368-
    }
369194
}