Use service instead of asynctask to download dictionaries in the background
app/src/main/java/eu/lepiller/nani/DictionaryActivity.java
67 | 67 | }); | |
68 | 68 | } | |
69 | 69 | ||
70 | + | @Override | |
71 | + | protected void onResume() { | |
72 | + | super.onResume(); | |
73 | + | updateDataset(); | |
74 | + | } | |
75 | + | ||
70 | 76 | private class DownloadTask extends AsyncTask<String, Integer, Integer> { | |
71 | 77 | private SwipeRefreshLayout refresher; | |
72 | 78 | ||
… | |||
114 | 120 | protected void onPostExecute(Integer i) { | |
115 | 121 | super.onPostExecute(i); | |
116 | 122 | refresher.setRefreshing(false); | |
117 | - | DictionaryFactory.updatePackageList(); | |
118 | - | dictionaries.clear(); | |
119 | - | dictionaries.addAll(DictionaryFactory.getDictionnaries(getApplicationContext())); | |
120 | - | adapter.notifyDataSetChanged(); | |
123 | + | updateDataset(); | |
121 | 124 | if(i == null) | |
122 | 125 | return; | |
123 | 126 | Snackbar.make(findViewById(R.id.dictionary_view), getString(i), | |
… | |||
125 | 128 | } | |
126 | 129 | } | |
127 | 130 | ||
131 | + | private void updateDataset() { | |
132 | + | DictionaryFactory.updatePackageList(); | |
133 | + | dictionaries.clear(); | |
134 | + | dictionaries.addAll(DictionaryFactory.getDictionnaries(getApplicationContext())); | |
135 | + | adapter.notifyDataSetChanged(); | |
136 | + | } | |
137 | + | ||
128 | 138 | @Override | |
129 | 139 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { | |
130 | 140 | super.onActivityResult(requestCode, resultCode, data); |
app/src/main/java/eu/lepiller/nani/DictionaryDownloadActivity.java
1 | 1 | package eu.lepiller.nani; | |
2 | 2 | ||
3 | - | import android.app.NotificationManager; | |
3 | + | import android.content.Intent; | |
4 | 4 | import android.graphics.drawable.Drawable; | |
5 | - | import android.os.AsyncTask; | |
6 | 5 | import android.os.Build; | |
7 | 6 | ||
8 | - | import androidx.core.app.NotificationCompat; | |
9 | 7 | import androidx.appcompat.app.AppCompatActivity; | |
8 | + | import androidx.lifecycle.LiveData; | |
9 | + | import androidx.lifecycle.Observer; | |
10 | + | ||
10 | 11 | import android.os.Bundle; | |
11 | 12 | import android.util.Log; | |
12 | - | import android.util.Pair; | |
13 | 13 | import android.view.View; | |
14 | 14 | import android.widget.ImageView; | |
15 | 15 | import android.widget.LinearLayout; | |
16 | 16 | import android.widget.ProgressBar; | |
17 | 17 | import android.widget.TextView; | |
18 | 18 | ||
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 | - | ||
31 | 19 | import eu.lepiller.nani.dictionary.Dictionary; | |
32 | 20 | import eu.lepiller.nani.dictionary.DictionaryFactory; | |
33 | 21 | ||
34 | 22 | public class DictionaryDownloadActivity extends AppCompatActivity { | |
35 | - | final static String TAG = "DWN"; | |
36 | - | final static int notificationID = 1111; | |
37 | 23 | final static String EXTRA_DICTIONARY = "eu.lepiller.nani.extra.DICTIONARY"; | |
24 | + | private final static String TAG = "DictionaryDownload"; | |
38 | 25 | ||
39 | 26 | Dictionary d; | |
40 | - | DownloadTask currentDownloadTask = null; | |
41 | - | ImageView download_button = null; | |
42 | 27 | ||
28 | + | ImageView download_button = null; | |
43 | 29 | ProgressBar download_bar; | |
44 | 30 | ||
45 | - | NotificationManager manager; | |
46 | - | NotificationCompat.Builder builder; | |
31 | + | Observer<DictionaryDownloadService.DownloadData> observer; | |
47 | 32 | ||
48 | 33 | View.OnClickListener download_click_listener = new View.OnClickListener() { | |
49 | 34 | @Override | |
50 | 35 | 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); | |
63 | 40 | } | |
64 | 41 | }; | |
65 | 42 | ||
66 | 43 | View.OnClickListener pause_click_listener = new View.OnClickListener() { | |
67 | 44 | @Override | |
68 | 45 | 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); | |
75 | 50 | } | |
76 | 51 | }; | |
77 | 52 | ||
… | |||
86 | 61 | name = extras.getString(EXTRA_DICTIONARY); | |
87 | 62 | ||
88 | 63 | 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); | |
95 | 66 | ||
96 | 67 | 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(); | |
98 | 123 | } | |
99 | 124 | ||
100 | - | private void updateLayout(final Dictionary d) { | |
125 | + | private void updateLayout(final boolean isDownloading) { | |
126 | + | Log.d(TAG, "updateLayout: " + isDownloading); | |
101 | 127 | TextView name_view = findViewById(R.id.name_view); | |
102 | 128 | name_view.setText(d.getName()); | |
103 | 129 | ||
… | |||
106 | 132 | TextView full_description_view = findViewById(R.id.extended_info_view); | |
107 | 133 | full_description_view.setText(d.getFullDescription()); | |
108 | 134 | ||
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 | - | ||
116 | 135 | ImageView icon_view = findViewById(R.id.icon_view); | |
117 | 136 | Drawable icon = d.getDrawable(getApplicationContext()); | |
118 | 137 | if (icon != null) { | |
119 | 138 | icon_view.setImageDrawable(icon); | |
120 | 139 | } | |
121 | 140 | ||
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 | + | ||
128 | 157 | setIcon(download_button, drawableResId); | |
129 | 158 | download_button.setEnabled(true); | |
130 | 159 | ||
… | |||
133 | 162 | setIcon(trash_button, drawableResId); | |
134 | 163 | ||
135 | 164 | 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); | |
137 | 166 | ||
138 | 167 | TextView size_view = findViewById(R.id.size_view); | |
139 | 168 | int size = d.getSize(); | |
… | |||
144 | 173 | else | |
145 | 174 | size_view.setText(String.format(getResources().getString(R.string.dictionary_size_mb), size/1000000)); | |
146 | 175 | ||
147 | - | download_button.setOnClickListener(download_click_listener); | |
148 | - | ||
149 | 176 | trash_button.setOnClickListener(new View.OnClickListener() { | |
150 | 177 | @Override | |
151 | 178 | public void onClick(View v) { | |
152 | 179 | d.remove(); | |
153 | - | updateLayout(d); | |
180 | + | updateLayout(isDownloading); | |
154 | 181 | } | |
155 | 182 | }); | |
156 | 183 | } | |
… | |||
164 | 191 | } | |
165 | 192 | download_button.setImageDrawable(drawable); | |
166 | 193 | } | |
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 | - | } | |
369 | 194 | } |