Use WorkManager for downloads

This commit is contained in:
Manuel Fuhr 2022-04-02 16:58:05 +02:00
parent 21abce0139
commit ecc4def40c
5 changed files with 428 additions and 43 deletions

View file

@ -95,6 +95,7 @@ android {
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation "androidx.constraintlayout:constraintlayout:2.1.3"
implementation 'androidx.work:work-runtime:2.7.1'
implementation project(':brouter-mapaccess')
implementation project(':brouter-core')
@ -105,6 +106,7 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.work:work-testing:2.7.1'
}
task generateProfiles(type: Exec) {

View file

@ -0,0 +1,71 @@
package btools.routingapp;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.work.Data;
import androidx.work.ListenableWorker.Result;
import androidx.work.testing.TestWorkerBuilder;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@RunWith(AndroidJUnit4.class)
public class DownloadWorkerTest {
private Context context;
private Executor executor;
@Before
public void setUp() {
context = ApplicationProvider.getApplicationContext();
executor = Executors.newSingleThreadExecutor();
}
@Test
public void testDownloadNewFile() {
Data inputData = new Data.Builder()
.putStringArray(DownloadWorker.KEY_INPUT_SEGMENT_NAMES, new String[]{"E105_N50"})
.build();
DownloadWorker worker =
TestWorkerBuilder.from(context, DownloadWorker.class, executor)
.setInputData(inputData)
.build();
Result result = worker.doWork();
assertThat(result, is(Result.success()));
}
@Test
public void testDownloadInvalidSegment() {
Data inputData = new Data.Builder()
.putStringArray(DownloadWorker.KEY_INPUT_SEGMENT_NAMES, new String[]{"X00"})
.build();
DownloadWorker worker =
TestWorkerBuilder.from(context, DownloadWorker.class, executor)
.setInputData(inputData)
.build();
Result result = worker.doWork();
assertThat(result, is(Result.failure()));
}
@Test
public void testDownloadNoSegments() {
DownloadWorker worker =
TestWorkerBuilder.from(context, DownloadWorker.class, executor)
.build();
Result result = worker.doWork();
assertThat(result, is(Result.failure()));
}
}

View file

@ -7,11 +7,7 @@ import static btools.routingapp.BInstallerView.MASK_SELECTED_RD5;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.os.Build;
@ -23,6 +19,13 @@ import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import androidx.work.WorkRequest;
import java.io.File;
import java.util.ArrayList;
@ -37,7 +40,6 @@ public class BInstallerActivity extends AppCompatActivity {
public static boolean downloadCanceled = false;
private File mBaseDir;
private BInstallerView mBInstallerView;
private DownloadReceiver downloadReceiver;
private View mDownloadInfo;
private TextView mDownloadInfoText;
private Button mButtonDownloadCancel;
@ -153,34 +155,59 @@ public class BInstallerActivity extends AppCompatActivity {
downloadCanceled = false;
mDownloadInfoText.setText(R.string.download_info_start);
Intent intent = new Intent(this, DownloadService.class);
intent.putExtra("dir", mBaseDir.getAbsolutePath() + "/brouter/");
intent.putExtra("urlparts", urlparts);
startService(intent);
Data inputData = new Data.Builder()
.putStringArray(DownloadWorker.KEY_INPUT_SEGMENT_NAMES, urlparts.toArray(new String[0]))
.build();
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
WorkRequest downloadWorkRequest =
new OneTimeWorkRequest.Builder(DownloadWorker.class)
.setInputData(inputData)
.setConstraints(constraints)
.build();
WorkManager workManager = WorkManager.getInstance(getApplicationContext());
workManager.enqueue(downloadWorkRequest);
mButtonDownloadCancel.setOnClickListener(view -> {
mDownloadInfoText.setText("Cancelling...");
workManager.cancelWorkById(downloadWorkRequest.getId());
});
workManager
.getWorkInfoByIdLiveData(downloadWorkRequest.getId())
.observe(this, workInfo -> {
if (workInfo != null) {
if (workInfo.getState() == WorkInfo.State.ENQUEUED) {
mDownloadInfoText.setText("Waiting for download to start. Check internet connection if it takes too long");
}
if (workInfo.getState() == WorkInfo.State.RUNNING) {
Data progress = workInfo.getProgress();
String segmentName = progress.getString(DownloadWorker.PROGRESS_SEGMENT_NAME);
int percent = progress.getInt(DownloadWorker.PROGRESS_SEGMENT_PERCENT, 0);
if (segmentName != null) {
mDownloadInfoText.setText(String.format("Download %s - %d%%", segmentName, percent));
}
}
if (workInfo.getState().isFinished()) {
mSegmentsView.setVisibility(View.VISIBLE);
mDownloadInfo.setVisibility(View.GONE);
scanExistingFiles();
}
}
});
deleteRawTracks(); // invalidate raw-tracks after data update
}
@Override
protected void onResume() {
super.onResume();
IntentFilter filter = new IntentFilter();
filter.addAction(DOWNLOAD_ACTION);
downloadReceiver = new DownloadReceiver();
registerReceiver(downloadReceiver, filter);
}
@Override
protected void onPause() {
super.onPause();
}
@Override
public void onDestroy() {
super.onDestroy();
if (downloadReceiver != null) unregisterReceiver(downloadReceiver);
System.exit(0);
}
@ -279,21 +306,4 @@ public class BInstallerActivity extends AppCompatActivity {
String slat = lat < 0 ? "S" + (-lat) : "N" + lat;
return slon + "_" + slat;
}
public class DownloadReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.hasExtra("txt")) {
String txt = intent.getStringExtra("txt");
boolean ready = intent.getBooleanExtra("ready", false);
if (!ready) {
mSegmentsView.setVisibility(View.VISIBLE);
mDownloadInfo.setVisibility(View.GONE);
scanExistingFiles();
}
mDownloadInfoText.setText(txt);
}
}
}
}

View file

@ -0,0 +1,299 @@
package btools.routingapp;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.work.Data;
import androidx.work.ForegroundInfo;
import androidx.work.WorkManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import btools.mapaccess.PhysicalFile;
import btools.mapaccess.Rd5DiffManager;
import btools.mapaccess.Rd5DiffTool;
import btools.util.ProgressListener;
public class DownloadWorker extends Worker {
public static final String KEY_INPUT_SEGMENT_NAMES = "SEGMENT_NAMES";
public static final String PROGRESS_SEGMENT_NAME = "PROGRESS_SEGMENT_NAME";
public static final String PROGRESS_SEGMENT_PERCENT = "PROGRESS_SEGMENT_PERCENT";
private static final int NOTIFICATION_ID = 1;
private static final String PROFILES_DIR = "profiles2/";
private static final String SEGMENTS_DIR = "segments4/";
private static final String SEGMENT_DIFF_SUFFIX = ".df5";
private static final String SEGMENT_SUFFIX = ".rd5";
private NotificationManager notificationManager;
private ServerConfig mServerConfig;
private File baseDir;
private ProgressListener diffProgressListener;
private DownloadProgressListener downloadProgressListener;
private Data.Builder progressBuilder = new Data.Builder();
private NotificationCompat.Builder notificationBuilder;
public DownloadWorker(
@NonNull Context context,
@NonNull WorkerParameters parameters) {
super(context, parameters);
notificationManager = (NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE);
mServerConfig = new ServerConfig(context);
baseDir = new File(ConfigHelper.getBaseDir(context), "brouter");
notificationBuilder = createNotificationBuilder();
diffProgressListener = new ProgressListener() {
@Override
public void updateProgress(String progress) {
notificationManager.notify(NOTIFICATION_ID, createNotification(progress));
}
@Override
public boolean isCanceled() {
return isStopped();
}
};
downloadProgressListener = new DownloadProgressListener() {
@Override
public void updateProgress(int max, int progress) {
if (max > 0) {
notificationBuilder.setProgress(max, progress, false);
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
progressBuilder.putInt(PROGRESS_SEGMENT_PERCENT, progress * 100 / max);
setProgressAsync(progressBuilder.build());
} else {
notificationBuilder.setProgress(0, 0, true);
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
progressBuilder.putInt(PROGRESS_SEGMENT_PERCENT, -1);
setProgressAsync(progressBuilder.build());
}
}
@Override
public void updateProgress(String content) {
notificationBuilder.setContentText(content);
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
};
progressBuilder.putInt(PROGRESS_SEGMENT_PERCENT, 0);
setProgressAsync(progressBuilder.build());
}
@NonNull
@Override
public Result doWork() {
Data inputData = getInputData();
String[] segmentNames = inputData.getStringArray(KEY_INPUT_SEGMENT_NAMES);
if (segmentNames == null) {
return Result.failure();
}
// Mark the Worker as important
setForegroundAsync(new ForegroundInfo(NOTIFICATION_ID, createNotification("Starting Download")));
try {
notificationManager.notify(NOTIFICATION_ID, createNotification("Updating profiles"));
downloadLookupAndProfiles();
int segmentIndex = 1;
for (String segmentName : segmentNames) {
notificationManager.notify(NOTIFICATION_ID, createNotification(String.format("%s (%d/%d)", segmentName, segmentIndex, segmentNames.length)));
progressBuilder.putString(PROGRESS_SEGMENT_NAME, segmentName);
setProgressAsync(progressBuilder.build());
downloadSegment(mServerConfig.getSegmentUrl(), segmentName + SEGMENT_SUFFIX);
segmentIndex++;
}
} catch (IOException e) {
return Result.failure();
} catch (InterruptedException e) {
return Result.failure();
}
return Result.success();
}
private void downloadLookupAndProfiles() throws IOException, InterruptedException {
String[] lookups = mServerConfig.getLookups();
for (String fileName : lookups) {
if (fileName.length() > 0) {
File lookupFile = new File(baseDir, PROFILES_DIR + fileName);
String lookupLocation = mServerConfig.getLookupUrl() + fileName;
URL lookupUrl = new URL(lookupLocation);
downloadFile(lookupUrl, lookupFile, false);
}
}
String[] profiles = mServerConfig.getProfiles();
for (String fileName : profiles) {
if (fileName.length() > 0) {
File profileFile = new File(baseDir, PROFILES_DIR + fileName);
if (profileFile.exists()) {
String profileLocation = mServerConfig.getProfilesUrl() + fileName;
URL profileUrl = new URL(profileLocation);
downloadFile(profileUrl, profileFile, false);
}
}
}
}
private void downloadSegment(String segmentBaseUrl, String segmentName) throws IOException, InterruptedException {
File segmentFile = new File(baseDir, SEGMENTS_DIR + segmentName);
File segmentFileTemp = new File(segmentFile.getAbsolutePath() + "_tmp");
try {
if (segmentFile.exists()) {
downloadProgressListener.updateProgress("Calculating local checksum...");
String md5 = Rd5DiffManager.getMD5(segmentFile);
String segmentDeltaLocation = segmentBaseUrl + "diff/" + segmentName.replace(SEGMENT_SUFFIX, "/" + md5 + SEGMENT_DIFF_SUFFIX);
URL segmentDeltaUrl = new URL(segmentDeltaLocation);
if (httpFileExists(segmentDeltaUrl)) {
File segmentDeltaFile = new File(segmentFile.getAbsolutePath() + "_diff");
try {
downloadFile(segmentDeltaUrl, segmentDeltaFile, true);
downloadProgressListener.updateProgress("Applying delta...");
Rd5DiffTool.recoverFromDelta(segmentFile, segmentDeltaFile, segmentFileTemp, diffProgressListener);
} catch (IOException e) {
throw new IOException("Failed to download & apply delta update", e);
} finally {
segmentDeltaFile.delete();
}
}
}
if (!segmentFileTemp.exists()) {
URL segmentUrl = new URL(segmentBaseUrl + segmentName);
downloadFile(segmentUrl, segmentFileTemp, true);
}
PhysicalFile.checkFileIntegrity(segmentFileTemp);
if (segmentFile.exists()) {
if (!segmentFile.delete()) {
throw new IOException("Failed to delete existing " + segmentFile.getAbsolutePath());
}
}
if (!segmentFileTemp.renameTo(segmentFile)) {
throw new IOException("Failed to write " + segmentFile.getAbsolutePath());
}
} finally {
segmentFileTemp.delete();
}
}
private boolean httpFileExists(URL downloadUrl) throws IOException {
HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection();
connection.setConnectTimeout(5000);
connection.setRequestMethod("HEAD");
connection.connect();
return connection.getResponseCode() == HttpURLConnection.HTTP_OK;
}
private void downloadFile(URL downloadUrl, File outputFile, boolean limitDownloadSpeed) throws IOException, InterruptedException {
// For all those small files the progress reporting is really noisy
boolean reportDownloadProgress = limitDownloadSpeed;
HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection();
connection.setConnectTimeout(5000);
connection.connect();
if (reportDownloadProgress) downloadProgressListener.updateProgress("Connecting...");
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException("HTTP Request failed");
}
int fileLength = connection.getContentLength();
try (
InputStream input = connection.getInputStream();
OutputStream output = new FileOutputStream(outputFile)
) {
byte[] buffer = new byte[4096];
int total = 0;
long t0 = System.currentTimeMillis();
int count;
while ((count = input.read(buffer)) != -1) {
if (isStopped()) {
throw new InterruptedException();
}
total += count;
output.write(buffer, 0, count);
// publishing the progress....
downloadProgressListener.updateProgress(fileLength, total);
if (limitDownloadSpeed) {
// enforce < 16 Mbit/s
long dt = t0 + total / 2096 - System.currentTimeMillis();
if (dt > 0) {
Thread.sleep(dt);
}
}
}
}
setProgressAsync(new Data.Builder().putInt(PROGRESS_SEGMENT_PERCENT, 100).build());
}
@NonNull
private NotificationCompat.Builder createNotificationBuilder() {
Context context = getApplicationContext();
String id = context.getString(R.string.notification_channel_id);
String title = context.getString(R.string.notification_title);
String cancel = context.getString(R.string.cancel_download);
// This PendingIntent can be used to cancel the worker
PendingIntent intent = WorkManager.getInstance(context)
.createCancelPendingIntent(getId());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createChannel();
}
return new NotificationCompat.Builder(context, id)
.setContentTitle(title)
.setTicker(title)
.setOnlyAlertOnce(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setOngoing(true)
// Add the cancel action to the notification which can
// be used to cancel the worker
.addAction(android.R.drawable.ic_delete, cancel, intent);
}
@NonNull
private Notification createNotification(@NonNull String content) {
notificationBuilder.setContentText(content);
// Reset progress from previous download
notificationBuilder.setProgress(0, 0, false);
return notificationBuilder.build();
}
@RequiresApi(Build.VERSION_CODES.O)
private void createChannel() {
CharSequence name = getApplicationContext().getString(R.string.channel_name);
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(getApplicationContext().getString(R.string.notification_channel_id), name, importance);
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
notificationManager.createNotificationChannel(channel);
}
interface DownloadProgressListener {
void updateProgress(int max, int progress);
void updateProgress(String content);
}
}

View file

@ -30,4 +30,7 @@
<string name="action_update">Update %s</string>
<string name="action_select">Select segments</string>
<string name="summary_segments">Size=%s\nFree=%s</string>
<string name="notification_channel_id">brouter_download</string>
<string name="notification_title">Download Segments</string>
<string name="channel_name">Downloads</string>
</resources>