Add download service

Julien LepillerWed Jun 10 19:29:21+0200 2020

a460385

Add download service

app/src/main/AndroidManifest.xml

8383
                <category android:name="android.intent.category.LAUNCHER" />
8484
            </intent-filter>
8585
        </activity>
86+
        <service android:name=".DictionaryDownloadService" />
8687
    </application>
8788
8889
</manifest>
8889=
8990=
\ No newline at end of file

app/src/main/java/eu/lepiller/nani/DictionaryDownloadService.java unknown status 1

1+
package eu.lepiller.nani;
2+
3+
import android.app.PendingIntent;
4+
import android.app.Service;
5+
import android.content.Intent;
6+
import android.os.Handler;
7+
import android.os.IBinder;
8+
import android.os.Looper;
9+
import android.util.Log;
10+
import android.util.Pair;
11+
12+
import androidx.annotation.Nullable;
13+
import androidx.core.app.NotificationCompat;
14+
15+
import java.io.File;
16+
import java.io.FileOutputStream;
17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.net.HttpURLConnection;
20+
import java.net.URL;
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.concurrent.locks.ReentrantLock;
25+
26+
import eu.lepiller.nani.dictionary.Dictionary;
27+
import eu.lepiller.nani.dictionary.DictionaryFactory;
28+
29+
public class DictionaryDownloadService extends Service {
30+
    private NotificationCompat.Builder builder;
31+
    private DownloadQueue downloadQueue;
32+
33+
    public static final String DOWNLOAD_ACTION = "eu.lepiller.nani.action.DOWNLOAD";
34+
    public static final String PAUSE_ACTION = "eu.lepiller.nani.action.PAUSE";
35+
    private static final String TAG = "DicoDownloadService";
36+
37+
    @Override
38+
    public void onCreate() {
39+
        Log.d(TAG, "onCreate");
40+
        super.onCreate();
41+
        builder =  new NotificationCompat.Builder(this, App.DICTIONARY_DOWNLOAD_NOTIFICATION_CHANNEL)
42+
                .setSmallIcon(R.drawable.ic_launcher_foreground)
43+
                .setContentText(getString(R.string.downloading))
44+
                .setOnlyAlertOnce(true);
45+
        downloadQueue = new DownloadQueue();
46+
        Downloader downloadThread = new Downloader(new Runnable() {
47+
            @Override
48+
            public void run() {
49+
                stopSelf();
50+
            }
51+
        });
52+
        new Thread(downloadThread).start();
53+
    }
54+
55+
    @Override
56+
    public int onStartCommand(Intent intent, int flags, int startId) {
57+
        String action = intent.getAction();
58+
        Log.d(TAG, "onStartCommand: " + action);
59+
        if(action == null) {
60+
            return START_NOT_STICKY;
61+
        } else if(action.equals(DOWNLOAD_ACTION)) {
62+
            String name = intent.getStringExtra(DictionaryDownloadActivity.EXTRA_DICTIONARY);
63+
            downloadQueue.addJob(name);
64+
            updateNotification(-1, downloadQueue.nextJob());
65+
        } else if(action.equals(PAUSE_ACTION)) {
66+
            downloadQueue.stop();
67+
        }
68+
69+
        return START_NOT_STICKY;
70+
    }
71+
72+
    @Override
73+
    public void onDestroy() {
74+
        Log.d(TAG, "onDestroy");
75+
        downloadQueue = null;
76+
        super.onDestroy();
77+
    }
78+
79+
    void updateNotification(int progress, String name) {
80+
        Intent notificationIntent = new Intent(this, DictionaryDownloadActivity.class);
81+
        notificationIntent.putExtra(DictionaryDownloadActivity.EXTRA_DICTIONARY, name);
82+
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
83+
84+
        builder.setContentTitle(name)
85+
               .setContentIntent(pendingIntent)
86+
               .setProgress(100, progress, (progress < 0));
87+
        startForeground(1, builder.build());
88+
    }
89+
90+
    private static class DownloadQueue {
91+
        private ArrayList<String> downloadQueue = new ArrayList<>();
92+
        private boolean waitsForFirstWork = true;
93+
        private boolean stopAsked = false;
94+
        private ReentrantLock mutex = new ReentrantLock();
95+
96+
        String popNextJob() {
97+
            try {
98+
                mutex.lock();
99+
                if(downloadQueue.size() > 0) {
100+
                    String name = downloadQueue.get(0);
101+
                    downloadQueue.remove(0);
102+
                    return name;
103+
                }
104+
                return null;
105+
            } finally {
106+
                mutex.unlock();
107+
            }
108+
        }
109+
110+
        String nextJob() {
111+
            try {
112+
                mutex.lock();
113+
                if(downloadQueue.size() > 0)
114+
                    return downloadQueue.get(0);
115+
                return null;
116+
            } finally {
117+
                mutex.unlock();
118+
            }
119+
        }
120+
121+
        void addJob(String name) {
122+
            try {
123+
                mutex.lock();
124+
                waitsForFirstWork = false;
125+
                downloadQueue.add(name);
126+
            } finally {
127+
                mutex.unlock();
128+
            }
129+
        }
130+
131+
        boolean isWaitingForFirstJob() {
132+
            try {
133+
                mutex.lock();
134+
                return waitsForFirstWork;
135+
            } finally {
136+
                mutex.unlock();
137+
            }
138+
        }
139+
140+
        void stop() {
141+
            try {
142+
                mutex.lock();
143+
                stopAsked = true;
144+
                downloadQueue.clear();
145+
            } finally {
146+
                mutex.unlock();
147+
            }
148+
        }
149+
150+
        boolean wantsStop() {
151+
            try {
152+
                mutex.lock();
153+
                return stopAsked;
154+
            } finally {
155+
                mutex.unlock();
156+
            }
157+
        }
158+
159+
        boolean isDownloading(String name) {
160+
            try {
161+
                mutex.lock();
162+
                for(String n: downloadQueue) {
163+
                    if(n.equals(name))
164+
                        return true;
165+
                }
166+
                return false;
167+
            } finally {
168+
                mutex.unlock();
169+
            }
170+
        }
171+
    }
172+
173+
    private class Downloader implements Runnable {
174+
        private Runnable onStop;
175+
176+
        Downloader(Runnable onStop) {
177+
            this.onStop = onStop;
178+
        }
179+
180+
        @Override
181+
        public void run() {
182+
            while(downloadQueue.isWaitingForFirstJob()) {
183+
                try {
184+
                    Thread.sleep(100);
185+
                } catch (InterruptedException e) {
186+
                    e.printStackTrace();
187+
                }
188+
            }
189+
            String name;
190+
            while((name = downloadQueue.popNextJob()) != null) {
191+
                doDownload(name);
192+
            }
193+
194+
            Handler threadHandler = new Handler(Looper.getMainLooper());
195+
            threadHandler.post(onStop);
196+
        }
197+
198+
        private void publishProgress(int progress, String name) {
199+
            updateNotification(progress, name);
200+
        }
201+
202+
        private void doDownload(String name) {
203+
            Dictionary d = DictionaryFactory.getByName(DictionaryDownloadService.this, name);
204+
            if(d == null)
205+
                return;
206+
207+
            for (Map.Entry<String, Pair<File, File>> e : d.getDownloads().entrySet()) {
208+
                try {
209+
                    String uri = e.getKey();
210+
                    File temporaryFile = e.getValue().first;
211+
                    File cacheFile = e.getValue().second;
212+
213+
                    boolean newFile = downloadSha256(new URL(uri + ".sha256"), new File(cacheFile + ".sha256"));
214+
                    if(newFile) {
215+
                        d.remove();
216+
                    }
217+
218+
                    long expectedFileLength = getRange(new URL(uri));
219+
                    downloadFile(new URL(uri), temporaryFile, expectedFileLength, name);
220+
                } catch (Exception ignored) {
221+
                }
222+
            }
223+
        }
224+
225+
        private boolean downloadSha256(URL url, File dest) throws IOException {
226+
            createParent(dest);
227+
            byte[] data = new byte[4096];
228+
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
229+
            connection.connect();
230+
            if(connection.getResponseCode() != HttpURLConnection.HTTP_OK)
231+
                return true;
232+
233+
            InputStream input = connection.getInputStream();
234+
            File old_file = new File(dest + ".old");
235+
            deleteIfExists(old_file);
236+
            if(dest.exists())
237+
                rename(dest, old_file);
238+
239+
            FileOutputStream output = new FileOutputStream(dest);
240+
241+
            int count;
242+
            while((count = input.read(data)) != -1) {
243+
                if (isCancelled()) {
244+
                    input.close();
245+
                    return false;
246+
                }
247+
                output.write(data, 0, count);
248+
            }
249+
250+
            if(old_file.exists()) {
251+
                // Check that we continue to download the same file
252+
                String old_hash = Dictionary.readSha256FromFile(old_file);
253+
                String new_hash = Dictionary.readSha256FromFile(dest);
254+
                return old_hash.compareTo(new_hash) != 0;
255+
            }
256+
            return true;
257+
        }
258+
259+
        private long getRange(URL url) throws IOException {
260+
            long expectedLength;
261+
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
262+
            connection.setRequestMethod("HEAD");
263+
            connection.connect();
264+
            if(connection.getResponseCode() != HttpURLConnection.HTTP_OK)
265+
                return -1;
266+
267+
            boolean acceptRanges = connection.getHeaderFields().containsKey("accept-ranges");
268+
            expectedLength = connection.getContentLength();
269+
            List<String> headers = connection.getHeaderFields().get("accept-ranges");
270+
            if(headers != null) {
271+
                for (String h : headers) {
272+
                    if (h.toLowerCase().compareTo("none") == 0)
273+
                        acceptRanges = false;
274+
                }
275+
            }
276+
277+
            if(acceptRanges)
278+
                return expectedLength;
279+
            return -1;
280+
        }
281+
282+
        private void downloadFile(URL url, File dest, long expectedLength, String name) throws IOException {
283+
            createParent(dest);
284+
            long total = 0;
285+
            byte[] data = new byte[4096];
286+
            FileOutputStream output;
287+
288+
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
289+
            if(expectedLength > 0 && dest.length() < expectedLength) {
290+
                connection.addRequestProperty("Range", "bytes=" + dest.length() + "-" + (expectedLength-1));
291+
                total = dest.length();
292+
                output = new FileOutputStream(dest, true);
293+
            } else {
294+
                output = new FileOutputStream(dest);
295+
            }
296+
            connection.connect();
297+
298+
            // expect HTTP 200 OK, so we don't mistakenly save error report
299+
            // instead of the file
300+
            if (connection.getResponseCode() != HttpURLConnection.HTTP_OK &&
301+
                    connection.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) {
302+
                Log.e(TAG, "Server returned HTTP " + connection.getResponseCode()
303+
                        + " " + connection.getResponseMessage());
304+
                return;
305+
            }
306+
307+
            int fileLength = connection.getContentLength();
308+
            InputStream input = connection.getInputStream();
309+
310+
            int count;
311+
            int lastNotifiedProgress = 0;
312+
            publishProgress(0, name);
313+
            while ((count = input.read(data)) != -1) {
314+
                // allow canceling with back button
315+
                if (isCancelled()) {
316+
                    input.close();
317+
                    return;
318+
                }
319+
                total += count;
320+
                output.write(data, 0, count);
321+
                // publishing the progress....
322+
                if (fileLength > 0) {// only if total length is known
323+
                    float progress = (int) (total * 100 / fileLength);
324+
                    if(lastNotifiedProgress < progress - 2) {
325+
                        lastNotifiedProgress = (int)progress;
326+
                        publishProgress((int) progress, name);
327+
                        output.flush();
328+
                    }
329+
                }
330+
            }
331+
        }
332+
333+
        private boolean isCancelled() {
334+
            return downloadQueue.wantsStop();
335+
        }
336+
337+
        private void createParent(File file) {
338+
            if(file.getParentFile() == null || (!file.getParentFile().exists() && !file.getParentFile().mkdirs()))
339+
                Log.w(TAG, "could not create parent of " + file);
340+
        }
341+
342+
        private void deleteIfExists(File file) {
343+
            if(file.exists()) {
344+
                if(!file.delete())
345+
                    Log.w(TAG, "could not delete file " + file);
346+
            }
347+
        }
348+
349+
        private void rename(File file, File dest) {
350+
            if(file.exists()) {
351+
                if(!file.renameTo(dest))
352+
                    Log.w(TAG, "could not rename "+ file + " to " + dest);
353+
            }
354+
        }
355+
    }
356+
357+
    @Nullable
358+
    @Override
359+
    public IBinder onBind(Intent intent) {
360+
        return null;
361+
    }
362+
}