diff --git a/brouter-codec/src/main/java/btools/codec/MicroCache2.java b/brouter-codec/src/main/java/btools/codec/MicroCache2.java index 8cd8f30..16223a1 100644 --- a/brouter-codec/src/main/java/btools/codec/MicroCache2.java +++ b/brouter-codec/src/main/java/btools/codec/MicroCache2.java @@ -15,7 +15,7 @@ public final class MicroCache2 extends MicroCache private int latBase; private int cellsize; - public MicroCache2( int size, byte[] databuffer, int lonIdx, int latIdx, int divisor ) throws Exception + public MicroCache2( int size, byte[] databuffer, int lonIdx, int latIdx, int divisor ) { super( databuffer ); // sets ab=databuffer, aboffset=0 @@ -34,7 +34,7 @@ public final class MicroCache2 extends MicroCache return b; } - public MicroCache2( StatCoderContext bc, DataBuffers dataBuffers, int lonIdx, int latIdx, int divisor, TagValueValidator wayValidator, WaypointMatcher waypointMatcher ) throws Exception + public MicroCache2( StatCoderContext bc, DataBuffers dataBuffers, int lonIdx, int latIdx, int divisor, TagValueValidator wayValidator, WaypointMatcher waypointMatcher ) { super( null ); cellsize = 1000000 / divisor; diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/DirectWeaver.java b/brouter-mapaccess/src/main/java/btools/mapaccess/DirectWeaver.java index ed25fd8..dc7191e 100644 --- a/brouter-mapaccess/src/main/java/btools/mapaccess/DirectWeaver.java +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/DirectWeaver.java @@ -20,7 +20,7 @@ public final class DirectWeaver extends ByteDataWriter private int size = 0; - public DirectWeaver( StatCoderContext bc, DataBuffers dataBuffers, int lonIdx, int latIdx, int divisor, TagValueValidator wayValidator, WaypointMatcher waypointMatcher, OsmNodesMap hollowNodes ) throws Exception + public DirectWeaver( StatCoderContext bc, DataBuffers dataBuffers, int lonIdx, int latIdx, int divisor, TagValueValidator wayValidator, WaypointMatcher waypointMatcher, OsmNodesMap hollowNodes ) { super( null ); int cellsize = 1000000 / divisor; diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/OsmFile.java b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmFile.java index bf03490..f2c481b 100644 --- a/brouter-mapaccess/src/main/java/btools/mapaccess/OsmFile.java +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmFile.java @@ -35,7 +35,7 @@ final class OsmFile private int ncaches; private int indexsize; - public OsmFile( PhysicalFile rafile, int lonDegree, int latDegree, DataBuffers dataBuffers ) throws Exception + public OsmFile( PhysicalFile rafile, int lonDegree, int latDegree, DataBuffers dataBuffers ) throws IOException { this.lonDegree = lonDegree; this.latDegree = latDegree; @@ -111,7 +111,7 @@ final class OsmFile return idx == -1 ? indexsize : posIdx[idx]; } - public int getDataInputForSubIdx( int subIdx, byte[] iobuffer ) throws Exception + public int getDataInputForSubIdx( int subIdx, byte[] iobuffer ) throws IOException { int startPos = getPosIdx( subIdx - 1 ); int endPos = getPosIdx( subIdx ); @@ -128,7 +128,7 @@ final class OsmFile } public MicroCache createMicroCache( int lonIdx, int latIdx, DataBuffers dataBuffers, TagValueValidator wayValidator, - WaypointMatcher waypointMatcher, boolean reallyDecode, OsmNodesMap hollowNodes ) throws Exception + WaypointMatcher waypointMatcher, boolean reallyDecode, OsmNodesMap hollowNodes ) throws IOException { int subIdx = ( latIdx - divisor * latDegree ) * divisor + ( lonIdx - divisor * lonDegree ); diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/PhysicalFile.java b/brouter-mapaccess/src/main/java/btools/mapaccess/PhysicalFile.java index 1c378e0..1798fd3 100644 --- a/brouter-mapaccess/src/main/java/btools/mapaccess/PhysicalFile.java +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/PhysicalFile.java @@ -31,13 +31,12 @@ final public class PhysicalFile { MicroCache.debug = true; - String message = checkFileIntegrity( new File( args[0] ) ); - - if ( message != null ) - { - System.out.println( "************************************" ); - System.out.println( message ); - System.out.println( "************************************" ); + try { + checkFileIntegrity( new File( args[0] ) ); + } catch (IOException e) { + System.err.println( "************************************" ); + e.printStackTrace(); + System.err.println( "************************************" ); } } @@ -46,7 +45,7 @@ final public class PhysicalFile * * @return the error message if file corrupt, else null */ - public static String checkFileIntegrity( File f ) + public static String checkFileIntegrity( File f ) throws IOException { PhysicalFile pf = null; try @@ -66,14 +65,6 @@ final public class PhysicalFile } } } - catch (IllegalArgumentException iae) - { - return iae.getMessage(); - } - catch (Exception e) - { - return e.toString(); - } finally { if ( pf != null ) @@ -88,7 +79,7 @@ final public class PhysicalFile return null; } - public PhysicalFile( File f, DataBuffers dataBuffers, int lookupVersion, int lookupMinorVersion ) throws Exception + public PhysicalFile( File f, DataBuffers dataBuffers, int lookupVersion, int lookupMinorVersion ) throws IOException { fileName = f.getName(); byte[] iobuffer = dataBuffers.iobuffer; @@ -102,7 +93,7 @@ final public class PhysicalFile short readVersion = (short)(lv >> 48); if ( i == 0 && lookupVersion != -1 && readVersion != lookupVersion ) { - throw new IllegalArgumentException( "lookup version mismatch (old rd5?) lookups.dat=" + throw new IOException( "lookup version mismatch (old rd5?) lookups.dat=" + lookupVersion + " " + f. getAbsolutePath() + "=" + readVersion ); } fileIndex[i] = lv & 0xffffffffffffL; diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/Rd5DiffManager.java b/brouter-mapaccess/src/main/java/btools/mapaccess/Rd5DiffManager.java index 86ff392..3b33904 100644 --- a/brouter-mapaccess/src/main/java/btools/mapaccess/Rd5DiffManager.java +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/Rd5DiffManager.java @@ -8,8 +8,10 @@ package btools.mapaccess; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; +import java.io.IOException; import java.security.DigestInputStream; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; final public class Rd5DiffManager { @@ -93,30 +95,31 @@ final public class Rd5DiffManager } } - public static String getMD5( File f ) throws Exception + public static String getMD5( File f ) throws IOException { - MessageDigest md = MessageDigest.getInstance("MD5"); - BufferedInputStream bis = new BufferedInputStream( new FileInputStream( f ) ); - DigestInputStream dis = new DigestInputStream(bis, md); - byte[] buf = new byte[8192]; - for(;;) - { - int len = dis.read( buf ); - if ( len <= 0 ) - { - break; + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(f)); + DigestInputStream dis = new DigestInputStream(bis, md); + byte[] buf = new byte[8192]; + for (; ; ) { + int len = dis.read(buf); + if (len <= 0) { + break; + } } - } - dis.close(); - byte[] bytes = md.digest(); + dis.close(); + byte[] bytes = md.digest(); - StringBuilder sb = new StringBuilder(); - for (int j = 0; j < bytes.length; j++) - { - int v = bytes[j] & 0xff; - sb.append( hexChar( v >>> 4 ) ).append( hexChar( v & 0xf ) ); + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xff; + sb.append(hexChar(v >>> 4)).append(hexChar(v & 0xf)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + throw new IOException("MD5 algorithm not available", e); } - return sb.toString(); } private static char hexChar( int v ) diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/Rd5DiffTool.java b/brouter-mapaccess/src/main/java/btools/mapaccess/Rd5DiffTool.java index ac2f1d9..c5ffbf1 100644 --- a/brouter-mapaccess/src/main/java/btools/mapaccess/Rd5DiffTool.java +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/Rd5DiffTool.java @@ -12,6 +12,7 @@ import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; import java.util.Arrays; import btools.codec.DataBuffers; @@ -49,9 +50,8 @@ final public class Rd5DiffTool implements ProgressListener } @Override - public void updateProgress( String progress ) - { - System.out.println( progress ); + public void updateProgress(String task, int progress) { + System.out.println(task + ": " + progress + "%"); } @Override @@ -60,7 +60,7 @@ final public class Rd5DiffTool implements ProgressListener return false; } - private static long[] readFileIndex( DataInputStream dis, DataOutputStream dos ) throws Exception + private static long[] readFileIndex( DataInputStream dis, DataOutputStream dos ) throws IOException { long[] fileIndex = new long[25]; for( int i=0; i<25; i++ ) @@ -85,7 +85,7 @@ final public class Rd5DiffTool implements ProgressListener return index[tileIndex]; } - private static int[] readPosIndex( DataInputStream dis, DataOutputStream dos ) throws Exception + private static int[] readPosIndex( DataInputStream dis, DataOutputStream dos ) throws IOException { int[] posIndex = new int[1024]; for( int i=0; i<1024; i++ ) @@ -105,7 +105,7 @@ final public class Rd5DiffTool implements ProgressListener return idx == -1 ? 4096 : posIdx[idx]; } - private static byte[] createMicroCache( int[] posIdx, int tileIdx, DataInputStream dis, boolean deltaMode ) throws Exception + private static byte[] createMicroCache( int[] posIdx, int tileIdx, DataInputStream dis, boolean deltaMode ) throws IOException { if ( posIdx == null ) { @@ -125,7 +125,7 @@ final public class Rd5DiffTool implements ProgressListener return ab; } - private static MicroCache createMicroCache( byte[] ab, DataBuffers dataBuffers ) throws Exception + private static MicroCache createMicroCache( byte[] ab, DataBuffers dataBuffers ) { if ( ab == null || ab.length == 0 ) { @@ -286,7 +286,7 @@ final public class Rd5DiffTool implements ProgressListener } - public static void recoverFromDelta( File f1, File f2, File outFile, ProgressListener progress /* , File cmpFile */ ) throws Exception + public static void recoverFromDelta( File f1, File f2, File outFile, ProgressListener progress /* , File cmpFile */ ) throws IOException { if ( f2.length() == 0L ) { @@ -341,7 +341,7 @@ final public class Rd5DiffTool implements ProgressListener int pct = (int)(100. * bytesProcessed / getTileEnd( fileIndex1, 24 ) + 0.5 ); if ( pct != lastPct ) { - progress.updateProgress( "Applying delta: " + pct + "%" ); + progress.updateProgress("Applying delta", pct); lastPct = pct; } @@ -468,7 +468,7 @@ final public class Rd5DiffTool implements ProgressListener } } - public static void copyFile( File f1, File outFile, ProgressListener progress ) throws Exception + public static void copyFile( File f1, File outFile, ProgressListener progress ) throws IOException { boolean canceled = false; DataInputStream dis1 = new DataInputStream( new BufferedInputStream( new FileInputStream( f1 ) ) ); @@ -489,7 +489,7 @@ final public class Rd5DiffTool implements ProgressListener int pct = (int)( (100. * sizeRead) / (sizeTotal+1) + 0.5 ); if ( pct != lastPct ) { - progress.updateProgress( "Copying: " + pct + "%" ); + progress.updateProgress("Copying", pct); lastPct = pct; } int len = dis1.read( buf ); @@ -756,7 +756,7 @@ final public class Rd5DiffTool implements ProgressListener this.dataBuffers = dataBuffers; } - public MicroCache readMC() throws Exception + public MicroCache readMC() throws IOException { if (skips < 0 ) { @@ -775,7 +775,7 @@ final public class Rd5DiffTool implements ProgressListener return mc; } - public void finish() throws Exception + public void finish() { skips = -1; } diff --git a/brouter-routing-app/build.gradle b/brouter-routing-app/build.gradle index 4c88be6..d254021 100644 --- a/brouter-routing-app/build.gradle +++ b/brouter-routing-app/build.gradle @@ -5,7 +5,7 @@ plugins { } android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { applicationId "btools.routingapp" @@ -14,16 +14,16 @@ android { versionName project.version resValue('string', 'app_version', defaultConfig.versionName) - setProperty("archivesBaseName","BRouterApp." + defaultConfig.versionName) + setProperty("archivesBaseName", "BRouterApp." + defaultConfig.versionName) minSdkVersion 14 - + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } sourceSets.main.assets.srcDirs += new File(project.buildDir, 'assets') - if(project.hasProperty("RELEASE_STORE_FILE") && RELEASE_STORE_FILE.length() > 0) { + if (project.hasProperty("RELEASE_STORE_FILE") && RELEASE_STORE_FILE.length() > 0) { signingConfigs { // this uses a file ~/.gradle/gradle.properties // with content: @@ -33,17 +33,17 @@ android { // RELEASE_KEY_PASSWORD=***** // release { - // enable signingConfig in buildTypes to get a signed apk file - storeFile file(RELEASE_STORE_FILE) - storePassword RELEASE_STORE_PASSWORD - keyAlias RELEASE_KEY_ALIAS - keyPassword RELEASE_KEY_PASSWORD + // enable signingConfig in buildTypes to get a signed apk file + storeFile file(RELEASE_STORE_FILE) + storePassword RELEASE_STORE_PASSWORD + keyAlias RELEASE_KEY_ALIAS + keyPassword RELEASE_KEY_PASSWORD - // Optional, specify signing versions used - v1SigningEnabled true - v2SigningEnabled true + // Optional, specify signing versions used + v1SigningEnabled true + v2SigningEnabled true - } + } } } @@ -51,7 +51,7 @@ android { release { minifyEnabled false debuggable false - if(project.hasProperty("RELEASE_STORE_FILE") && RELEASE_STORE_FILE.length() > 0) { + if (project.hasProperty("RELEASE_STORE_FILE") && RELEASE_STORE_FILE.length() > 0) { signingConfig signingConfigs.release } proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' @@ -64,7 +64,8 @@ android { } lintOptions { disable 'InvalidPackage' - checkReleaseBuilds false //added this line to the build.gradle under the /android/app/build.gradle + checkReleaseBuilds false + //added this line to the build.gradle under the /android/app/build.gradle } compileOptions { @@ -93,9 +94,10 @@ android { } dependencies { - - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation "androidx.constraintlayout:constraintlayout:2.1.2" + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation "androidx.constraintlayout:constraintlayout:2.1.3" + implementation 'androidx.work:work-runtime:2.7.1' + implementation 'com.google.android.material:material:1.5.0' implementation project(':brouter-mapaccess') implementation project(':brouter-core') @@ -106,6 +108,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) { @@ -115,8 +118,7 @@ task generateProfiles(type: Exec) { task generateProfilesZip(type: Zip) { if (DefaultNativePlatform.getCurrentOperatingSystem().isWindows()) { logger.warn("Note: On Windows run script '../misc/scripts/generate_profile_variants.sh' manually to include all profiles") - } - else { + } else { dependsOn generateProfiles } archiveFileName = "profiles2.zip" diff --git a/brouter-routing-app/src/androidTest/java/btools/routingapp/DownloadWorkerTest.java b/brouter-routing-app/src/androidTest/java/btools/routingapp/DownloadWorkerTest.java new file mode 100644 index 0000000..a9004c0 --- /dev/null +++ b/brouter-routing-app/src/androidTest/java/btools/routingapp/DownloadWorkerTest.java @@ -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())); + } +} diff --git a/brouter-routing-app/src/main/AndroidManifest.xml b/brouter-routing-app/src/main/AndroidManifest.xml index c1ffbdf..76f6b51 100644 --- a/brouter-routing-app/src/main/AndroidManifest.xml +++ b/brouter-routing-app/src/main/AndroidManifest.xml @@ -3,7 +3,9 @@ - @@ -11,17 +13,16 @@ + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:preserveLegacyExternalStorage="true" + android:roundIcon="@mipmap/ic_launcher_round" + android:theme="@style/Theme.App"> + android:screenOrientation="unspecified"> @@ -29,20 +30,18 @@ + android:launchMode="singleTask" + android:screenOrientation="landscape" /> - + android:label="Import Profile"> + + @@ -88,15 +87,10 @@ - diff --git a/brouter-routing-app/src/main/java/btools/routingapp/BImportActivity.java b/brouter-routing-app/src/main/java/btools/routingapp/BImportActivity.java index 3d5f085..2550bf9 100644 --- a/brouter-routing-app/src/main/java/btools/routingapp/BImportActivity.java +++ b/brouter-routing-app/src/main/java/btools/routingapp/BImportActivity.java @@ -1,6 +1,5 @@ package btools.routingapp; -import android.app.Activity; import android.content.Intent; import android.database.Cursor; import android.net.Uri; @@ -12,6 +11,7 @@ import android.widget.EditText; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import java.io.BufferedReader; import java.io.File; @@ -20,7 +20,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -public class BImportActivity extends Activity { +public class BImportActivity extends AppCompatActivity { // profile size is generally < 30 kb, so set max size to 100 kb private static final int MAX_PROFILE_SIZE = 100000; private EditText mTextFilename; @@ -76,8 +76,8 @@ public class BImportActivity extends Activity { try (Cursor cursor = this.getContentResolver().query(intent.getData(), new String[]{ OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { - filename = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); - filesize = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE)); + filename = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)); + filesize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)); } } // is the file extention ".brf" in the file name diff --git a/brouter-routing-app/src/main/java/btools/routingapp/BInstallerActivity.java b/brouter-routing-app/src/main/java/btools/routingapp/BInstallerActivity.java index 286142e..c12ba62 100644 --- a/brouter-routing-app/src/main/java/btools/routingapp/BInstallerActivity.java +++ b/brouter-routing-app/src/main/java/btools/routingapp/BInstallerActivity.java @@ -5,23 +5,29 @@ import static btools.routingapp.BInstallerView.MASK_DELETED_RD5; import static btools.routingapp.BInstallerView.MASK_INSTALLED_RD5; import static btools.routingapp.BInstallerView.MASK_SELECTED_RD5; -import android.app.Activity; 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; import android.os.Bundle; import android.os.StatFs; import android.text.format.Formatter; -import android.view.View; import android.widget.Button; import android.widget.TextView; +import android.widget.Toast; + +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 com.google.android.material.progressindicator.LinearProgressIndicator; import java.io.File; import java.util.ArrayList; @@ -29,20 +35,15 @@ import java.util.Locale; import btools.router.RoutingHelper; -public class BInstallerActivity extends Activity { +public class BInstallerActivity extends AppCompatActivity { - public static final String DOWNLOAD_ACTION = "btools.routingapp.download"; private static final int DIALOG_CONFIRM_DELETE_ID = 1; public static boolean downloadCanceled = false; private File mBaseDir; private BInstallerView mBInstallerView; - private DownloadReceiver downloadReceiver; - private View mDownloadInfo; - private TextView mDownloadInfoText; - private Button mButtonDownloadCancel; private Button mButtonDownload; private TextView mSummaryInfo; - private View mSegmentsView; + private LinearProgressIndicator mProgressIndicator; public static long getAvailableSpace(String baseDir) { StatFs stat = new StatFs(baseDir); @@ -63,7 +64,6 @@ public class BInstallerActivity extends Activity { setContentView(R.layout.activity_binstaller); mSummaryInfo = findViewById(R.id.textViewSegmentSummary); - mSegmentsView = findViewById(R.id.view_segments); mBInstallerView = findViewById(R.id.BInstallerView); mBInstallerView.setOnSelectListener( () -> { @@ -82,12 +82,7 @@ public class BInstallerActivity extends Activity { } } ); - mDownloadInfo = findViewById(R.id.view_download_progress); - mDownloadInfoText = findViewById(R.id.textViewDownloadProgress); - mButtonDownloadCancel = findViewById(R.id.buttonDownloadCancel); - mButtonDownloadCancel.setOnClickListener(view -> { - cancelDownload(); - }); + mProgressIndicator = findViewById(R.id.progressDownload); mBaseDir = ConfigHelper.getBaseDir(this); scanExistingFiles(); @@ -136,53 +131,77 @@ public class BInstallerActivity extends Activity { } } - private void cancelDownload() { - downloadCanceled = true; - mDownloadInfoText.setText(getString(R.string.download_info_cancel)); - } - public void downloadAll(ArrayList downloadList) { ArrayList urlparts = new ArrayList<>(); for (Integer i : downloadList) { urlparts.add(baseNameForTile(i)); } - mSegmentsView.setVisibility(View.GONE); - mDownloadInfo.setVisibility(View.VISIBLE); 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); + + workManager + .getWorkInfoByIdLiveData(downloadWorkRequest.getId()) + .observe(this, workInfo -> { + if (workInfo != null) { + if (workInfo.getState() == WorkInfo.State.ENQUEUED) { + Toast.makeText(this, "Download scheduled. Check internet connection if it doesn't start.", Toast.LENGTH_LONG).show(); + mProgressIndicator.hide(); + mProgressIndicator.setIndeterminate(true); + mProgressIndicator.show(); + } + + 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 (percent > 0) { + mProgressIndicator.setIndeterminate(false); + } + mProgressIndicator.setProgress(percent); + } + + if (workInfo.getState().isFinished()) { + String result; + switch (workInfo.getState()) { + case FAILED: + result = "failed."; + break; + case CANCELLED: + result = "cancelled"; + break; + case SUCCEEDED: + result = "succeeded"; + break; + default: + result = ""; + } + Toast.makeText(this, "Download " + result + ".", Toast.LENGTH_SHORT).show(); + mProgressIndicator.hide(); + 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); - } - @Override protected Dialog onCreateDialog(int id) { AlertDialog.Builder builder; @@ -278,21 +297,4 @@ public class BInstallerActivity extends Activity { 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); - } - } - } } diff --git a/brouter-routing-app/src/main/java/btools/routingapp/BRouterActivity.java b/brouter-routing-app/src/main/java/btools/routingapp/BRouterActivity.java index 5fb8e04..c3253ce 100644 --- a/brouter-routing-app/src/main/java/btools/routingapp/BRouterActivity.java +++ b/brouter-routing-app/src/main/java/btools/routingapp/BRouterActivity.java @@ -1,7 +1,24 @@ package btools.routingapp; +import android.app.ActivityManager; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.StatFs; +import android.widget.EditText; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.os.EnvironmentCompat; + import java.io.File; -import java.lang.reflect.Method; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; @@ -9,36 +26,9 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import android.app.Activity; -import android.app.ActivityManager; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.ConnectivityManager; -import android.net.Network; -import android.net.NetworkCapabilities; -import android.net.NetworkInfo; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.os.PowerManager; -import android.os.PowerManager.WakeLock; -import android.os.StatFs; -import android.speech.tts.TextToSpeech.OnInitListener; -import android.util.Log; -import android.view.KeyEvent; -import android.widget.EditText; - - -import androidx.core.app.ActivityCompat; -import androidx.core.os.EnvironmentCompat; - import btools.router.OsmNodeNamed; -public class BRouterActivity extends Activity implements ActivityCompat.OnRequestPermissionsResultCallback { +public class BRouterActivity extends AppCompatActivity implements ActivityCompat.OnRequestPermissionsResultCallback { private static final int DIALOG_SELECTPROFILE_ID = 1; private static final int DIALOG_EXCEPTION_ID = 2; @@ -133,7 +123,7 @@ public class BRouterActivity extends Activity implements ActivityCompat.OnReques public void onClick(DialogInterface dialog, int id) { Intent intent = new Intent(BRouterActivity.this, BInstallerActivity.class); startActivity(intent); - // finish(); + showNewDialog(DIALOG_MAINACTION_ID); } }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { @@ -391,39 +381,12 @@ public class BRouterActivity extends Activity implements ActivityCompat.OnReques private String maptoolDirCandidate; - public boolean isOnline(Context context) { - boolean result = false; - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Network nw = connectivityManager.getActiveNetwork(); - if (nw == null) return false; - NetworkCapabilities nwc = connectivityManager.getNetworkCapabilities(nw); - if (nwc == null) return false; - result = nwc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) | - nwc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) | - nwc.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET); - - } else { - NetworkInfo ni = connectivityManager.getActiveNetworkInfo(); - if (ni == null) return false; - result = ni.getType() == ConnectivityManager.TYPE_WIFI || - ni.getType() == ConnectivityManager.TYPE_MOBILE || - ni.getType() == ConnectivityManager.TYPE_ETHERNET; - } - - return result; - } - @SuppressWarnings("deprecation") public void selectProfile(String[] items) { availableProfiles = items; - // if we have internet access, first show the main action dialog - if (isOnline(this)) { - showDialog(DIALOG_MAINACTION_ID); - } else { - showDialog(DIALOG_SELECTPROFILE_ID); - } + // show main dialog + showDialog(DIALOG_MAINACTION_ID); } @SuppressWarnings("deprecation") @@ -626,6 +589,7 @@ public class BRouterActivity extends Activity implements ActivityCompat.OnReques @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == 0) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { mBRouterView.startSetup(null, true); diff --git a/brouter-routing-app/src/main/java/btools/routingapp/BRouterView.java b/brouter-routing-app/src/main/java/btools/routingapp/BRouterView.java index fa02aae..c21af97 100644 --- a/brouter-routing-app/src/main/java/btools/routingapp/BRouterView.java +++ b/brouter-routing-app/src/main/java/btools/routingapp/BRouterView.java @@ -1,5 +1,21 @@ package btools.routingapp; +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.os.Build; +import android.os.Environment; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -12,36 +28,13 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; -import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Set; -import java.util.StringTokenizer; import java.util.TreeMap; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -import android.Manifest; -import android.app.Activity; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.res.AssetManager; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.os.Build; -import android.os.Environment; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.View; -import android.widget.Toast; - - -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; - -import btools.expressions.BExpressionContextWay; import btools.expressions.BExpressionMetaData; import btools.mapaccess.OsmNode; import btools.router.OsmNodeNamed; @@ -108,11 +101,6 @@ public class BRouterView extends View { public void init() { try { - DisplayMetrics metrics = new DisplayMetrics(); - ((Activity) getContext()).getWindowManager().getDefaultDisplay().getMetrics(metrics); - imgw = metrics.widthPixels; - imgh = metrics.heightPixels; - // get base dir from private file File baseDir = ConfigHelper.getBaseDir(getContext()); // check if valid @@ -703,6 +691,8 @@ public class BRouterView extends View { @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { + imgw = w; + imgh = h; } private void toast(String msg) { diff --git a/brouter-routing-app/src/main/java/btools/routingapp/DownloadService.java b/brouter-routing-app/src/main/java/btools/routingapp/DownloadService.java deleted file mode 100644 index 7457a84..0000000 --- a/brouter-routing-app/src/main/java/btools/routingapp/DownloadService.java +++ /dev/null @@ -1,487 +0,0 @@ -package btools.routingapp; - -import android.app.NotificationManager; -import android.app.Service; -import android.content.Intent; -import android.net.TrafficStats; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.util.Log; -import android.widget.Toast; - -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 java.util.List; - -import btools.mapaccess.PhysicalFile; -import btools.mapaccess.Rd5DiffManager; -import btools.mapaccess.Rd5DiffTool; -import btools.util.ProgressListener; - -public class DownloadService extends Service implements ProgressListener { - - private static final boolean DEBUG = false; - - private ServerConfig mServerConfig; - - private NotificationHelper mNotificationHelper; - private List mUrlList; - private String baseDir; - - private volatile String newDownloadAction = ""; - private volatile String currentDownloadOperation = ""; - private long availableSize; - - private Looper mServiceLooper; - private ServiceHandler mServiceHandler; - private NotificationManager mNM; - String downloadUrl; - public static boolean serviceState = false; - private boolean bIsDownloading; - - // Handler that receives messages from the thread - private final class ServiceHandler extends Handler { - public ServiceHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message msg) { - bIsDownloading = true; - downloadFiles(); - - stopForeground(true); - stopSelf(msg.arg1); - mNotificationHelper.stopNotification(); - } - } - - - @Override - public void onCreate() { - if (DEBUG) Log.d("SERVICE", "onCreate"); - serviceState = true; - mServerConfig = new ServerConfig(getApplicationContext()); - - HandlerThread thread = new HandlerThread("ServiceStartArguments", 1); - thread.start(); - - // Get the HandlerThread's Looper and use it for our Handler - mServiceLooper = thread.getLooper(); - mServiceHandler = new ServiceHandler(mServiceLooper); - - availableSize = -1; - try { - availableSize = BInstallerActivity.getAvailableSpace(baseDir); - //StatFs stat = new StatFs(baseDir); - //availableSize = (long)stat.getAvailableBlocksLong()*stat.getBlockSizeLong(); - } catch (Exception e) { /* ignore */ } - - } - - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (DEBUG) Log.d("SERVICE", "onStartCommand"); - - mNotificationHelper = new NotificationHelper(this); - Bundle extra = intent.getExtras(); - if (extra != null) { - String dir = extra.getString("dir"); - List urlparts = extra.getStringArrayList("urlparts"); - mUrlList = urlparts; - baseDir = dir; - } - - mNotificationHelper.startNotification(this); - - Message msg = mServiceHandler.obtainMessage(); - msg.arg1 = startId; - mServiceHandler.sendMessage(msg); - - // If we get killed, after returning from here, restart - return START_STICKY; - } - - - @Override - public void onDestroy() { - if (DEBUG) Log.d("SERVICE", "onDestroy"); - serviceState = false; - super.onDestroy(); - } - - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - - public void downloadFiles() { - - // first check lookup table and profiles - String result = checkScripts(); - if (result != null) { - if (DEBUG) Log.d("BR", "error: " + result); - bIsDownloading = false; - updateProgress("finished "); - - Toast.makeText(this, result, Toast.LENGTH_LONG).show(); - return; - } - - - int count = 1; - int size = mUrlList.size(); - for (String part : mUrlList) { - String url = mServerConfig.getSegmentUrl() + part + ".rd5"; - if (DEBUG) Log.d("BR", "downlaod " + url); - - result = download(count, size, url); - if (result != null) { - if (DEBUG) Log.d("BR", "" + result); - Toast.makeText(this, result, Toast.LENGTH_LONG).show(); - break; - } else { - updateProgress("Download " + part + " " + count + "/" + size + " finshed"); - } - count++; - } - - bIsDownloading = false; - updateProgress("finished "); - } - - - public void updateProgress(String progress) { - if (!newDownloadAction.equals(progress)) { - if (DEBUG) Log.d("BR", "up " + progress); - Intent intent = new Intent(BInstallerActivity.DOWNLOAD_ACTION); - intent.putExtra("txt", progress); - intent.putExtra("ready", bIsDownloading); - sendBroadcast(intent); - ; - newDownloadAction = progress; - mNotificationHelper.progressUpdate(newDownloadAction); - } - - } - - private String download(int counter, int size, String surl) { - InputStream input = null; - OutputStream output = null; - HttpURLConnection connection = null; - File fname = null; - File tmp_file = null; - try { - try { - TrafficStats.setThreadStatsTag(1); - - int slidx = surl.lastIndexOf("segments4/"); - String name = surl.substring(slidx + 10); - String surlBase = surl.substring(0, slidx + 10); - fname = new File(baseDir, "segments4/" + name); - - boolean delta = true; - - // if (!targetFile.getParentFile().exists()) targetFile.getParentFile().mkdirs(); - if (fname.exists()) { - updateProgress("Calculating local checksum.."); - - // first check for a delta file - String md5 = Rd5DiffManager.getMD5(fname); - String surlDelta = surlBase + "diff/" + name.replace(".rd5", "/" + md5 + ".df5"); - - URL urlDelta = new URL(surlDelta); - - connection = (HttpURLConnection) urlDelta.openConnection(); - connection.setConnectTimeout(5000); - connection.connect(); - - // 404 kind of expected here, means there's no delta file - if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { - connection = null; - } else { - updateProgress("Connecting.." + surlDelta); - } - } - - if (connection == null) { - updateProgress("Connecting.." + name); - - delta = false; - URL url = new URL(surl); - connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(5000); - connection.connect(); - } - - updateProgress("Connecting.." + counter + "/" + size); - - // expect HTTP 200 OK, so we don't mistakenly save error report - // instead of the file - if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { - return "Server returned HTTP " + connection.getResponseCode() - + " " + connection.getResponseMessage(); - } - - // this will be useful to display download percentage - // might be -1: server did not report the length - int fileLength = connection.getContentLength(); - long currentDownloadSize = fileLength; - if (availableSize >= 0 && fileLength > availableSize) return "not enough space on sd-card"; - - currentDownloadOperation = delta ? "Updating" : "Loading"; - updateProgress(currentDownloadOperation); - - // download the file - input = connection.getInputStream(); - - tmp_file = new File(fname.getAbsolutePath() + (delta ? "_diff" : "_tmp")); - output = new FileOutputStream(tmp_file); - - byte[] data = new byte[4096]; - long total = 0; - long t0 = System.currentTimeMillis(); - int count; - while ((count = input.read(data)) != -1) { - if (isCanceled()) { - return "Download canceled!"; - } - total += count; - // publishing the progress.... - if (fileLength > 0) // only if total length is known - { - int pct = (int) (total * 100 / fileLength); - updateProgress("Progress " + counter + "/" + size + " .. " + pct + "%"); - } else { - updateProgress("Progress (unnown size)"); - } - - output.write(data, 0, count); - - // enforce < 2 Mbit/s - long dt = t0 + total / 524 - System.currentTimeMillis(); - if (dt > 0) { - try { - Thread.sleep(dt); - } catch (InterruptedException ie) { - } - } - } - output.close(); - output = null; - - if (delta) { - updateProgress("Applying delta.."); - File diffFile = tmp_file; - tmp_file = new File(fname + "_tmp"); - Rd5DiffTool.recoverFromDelta(fname, diffFile, tmp_file, this); - diffFile.delete(); - } - if (isCanceled()) { - return "Canceled!"; - } - if (tmp_file != null) { - updateProgress("Verifying integrity.."); - String check_result = PhysicalFile.checkFileIntegrity(tmp_file); - if (check_result != null) { - if (check_result.startsWith("version old lookups.dat")) { - - } - return check_result; - } - if (fname.exists()) fname.delete(); - if (!tmp_file.renameTo(fname)) { - return "Could not rename to " + fname.getAbsolutePath(); - } - - } - return null; - } catch (Exception e) { - //e.printStackTrace(); ; - return e.toString(); - } finally { - try { - if (output != null) - output.close(); - if (input != null) - input.close(); - } catch (IOException ignored) { - } - - if (connection != null) - connection.disconnect(); - } - } finally { - if (tmp_file != null) tmp_file.delete(); // just to be sure - } - } - - private String checkScripts() { - - String[] sa = mServerConfig.getLookups(); - for (String f : sa) { - if (f.length() > 0) { - File file = new File(baseDir + "profiles2", f); - checkOrDownloadLookup(f, file); - } - } - - sa = mServerConfig.getProfiles(); - for (String f : sa) { - if (f.length() > 0) { - File file = new File(baseDir + "profiles2", f); - if (file.exists()) { - String result = checkOrDownloadScript(f, file); - if (result != null) { - return result; - } - } - } - } - return null; - } - - private String checkOrDownloadLookup(String fileName, File f) { - String url = mServerConfig.getLookupUrl() + fileName; - return downloadScript(url, f); - } - - private String checkOrDownloadScript(String fileName, File f) { - String url = mServerConfig.getProfilesUrl() + fileName; - return downloadScript(url, f); - } - - private String downloadScript(String surl, File f) { - long size = 0L; - if (f.exists()) { - size = f.length(); - } - - InputStream input = null; - OutputStream output = null; - HttpURLConnection connection = null; - File tmp_file = null; - File targetFile = f; - - try { - try { - TrafficStats.setThreadStatsTag(1); - - URL url = new URL(surl); - connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(5000); - connection.connect(); - - // expect HTTP 200 OK, so we don't mistakenly save error report - // instead of the file - if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { - return null; - } - if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { - return "Server returned HTTP " + connection.getResponseCode() - + " " + connection.getResponseMessage() + " " + f.getName(); - } - - - // this will be useful to display download percentage - // might be -1: server did not report the length - long fileLength = (long) connection.getContentLength(); - if (DEBUG) Log.d("BR", "file size " + size + " == " + fileLength + " " + f.getName()); - if (fileLength != size) { - long currentDownloadSize = fileLength; - if (availableSize >= 0 && fileLength > availableSize) - return "not enough space on sd-card"; - - currentDownloadOperation = "Updating"; - - // download the file - input = connection.getInputStream(); - - tmp_file = new File(f.getAbsolutePath() + "_tmp"); - output = new FileOutputStream(tmp_file); - - byte data[] = new byte[4096]; - long total = 0; - long t0 = System.currentTimeMillis(); - int count; - while ((count = input.read(data)) != -1) { - if (isCanceled()) { - return "Download canceled!"; - } - total += count; - // publishing the progress.... - if (fileLength > 0) // only if total length is known - { - int pct = (int) (total * 100 / fileLength); - updateProgress("Progress " + pct + "%"); - } else { - updateProgress("Progress (unnown size)"); - } - - output.write(data, 0, count); - - // enforce < 16 Mbit/s - long dt = t0 + total / 2096 - System.currentTimeMillis(); - if (dt > 0) { - try { - Thread.sleep(dt); - } catch (InterruptedException ie) { - } - } - } - output.close(); - output = null; - } - - if (isCanceled()) { - return "Canceled!"; - } - if (tmp_file != null) { - f.delete(); - - if (!tmp_file.renameTo(f)) { - return "Could not rename to " + f.getName(); - } - if (DEBUG) Log.d("BR", "update " + f.getName()); - } - return null; - } catch (Exception e) { - return e.toString(); - } finally { - try { - if (output != null) - output.close(); - if (input != null) - input.close(); - } catch (IOException ignored) { - } - - if (connection != null) - connection.disconnect(); - - } - } finally { - if (tmp_file != null) tmp_file.delete(); // just to be sure - } - - } - - - public boolean isCanceled() { - return BInstallerActivity.downloadCanceled; - } - -} diff --git a/brouter-routing-app/src/main/java/btools/routingapp/DownloadWorker.java b/brouter-routing-app/src/main/java/btools/routingapp/DownloadWorker.java new file mode 100644 index 0000000..0b493fe --- /dev/null +++ b/brouter-routing-app/src/main/java/btools/routingapp/DownloadWorker.java @@ -0,0 +1,327 @@ +package btools.routingapp; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.os.Build; +import android.util.Log; + +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 java.util.Random; + +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 final static boolean DEBUG = false; + private static final int NOTIFICATION_ID = new Random().nextInt(); + 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 static final String LOG_TAG = "DownloadWorker"; + + private final NotificationManager notificationManager; + private final ServerConfig mServerConfig; + private final File baseDir; + private final ProgressListener diffProgressListener; + private final DownloadProgressListener downloadProgressListener; + private final Data.Builder progressBuilder = new Data.Builder(); + private final 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(); + + downloadProgressListener = new DownloadProgressListener() { + private String currentDownloadName; + private DownloadType currentDownloadType; + private int lastProgressPercent; + + @Override + public void onDownloadStart(String downloadName, DownloadType downloadType) { + currentDownloadName = downloadName; + currentDownloadType = downloadType; + if (downloadType == DownloadType.SEGMENT) { + progressBuilder.putString(PROGRESS_SEGMENT_NAME, downloadName); + notificationBuilder.setContentText(downloadName); + } + } + + @Override + public void onDownloadInfo(String info) { + notificationBuilder.setContentText(currentDownloadName + ": " + info); + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); + } + + @Override + public void onDownloadProgress(int max, int progress) { + int progressPercent = (int) (progress * 100L / max); + + // Only report segments and update if it changed to avoid hammering NotificationManager + if (currentDownloadType != DownloadType.SEGMENT || progressPercent == lastProgressPercent) { + return; + } + + if (max > 0) { + notificationBuilder.setProgress(max, progress, false); + progressBuilder.putInt(PROGRESS_SEGMENT_PERCENT, progressPercent); + } else { + notificationBuilder.setProgress(0, 0, true); + progressBuilder.putInt(PROGRESS_SEGMENT_PERCENT, -1); + } + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); + setProgressAsync(progressBuilder.build()); + + lastProgressPercent = progressPercent; + } + + @Override + public void onDownloadFinished() { + } + }; + + diffProgressListener = new ProgressListener() { + @Override + public void updateProgress(String task, int progress) { + downloadProgressListener.onDownloadInfo(task); + downloadProgressListener.onDownloadProgress(100, progress); + } + + @Override + public boolean isCanceled() { + return isStopped(); + } + }; + } + + @NonNull + @Override + public Result doWork() { + Data inputData = getInputData(); + String[] segmentNames = inputData.getStringArray(KEY_INPUT_SEGMENT_NAMES); + if (segmentNames == null) { + if (DEBUG) Log.d(LOG_TAG, "Failure: no segmentNames"); + return Result.failure(); + } + notificationBuilder.setContentText("Starting Download"); + // Mark the Worker as important + setForegroundAsync(new ForegroundInfo(NOTIFICATION_ID, notificationBuilder.build())); + try { + if (DEBUG) Log.d(LOG_TAG, "Download lookup & profiles"); + downloadLookupAndProfiles(); + + for (String segmentName : segmentNames) { + downloadProgressListener.onDownloadStart(segmentName, DownloadType.SEGMENT); + if (DEBUG) Log.d(LOG_TAG, "Download segment " + segmentName); + downloadSegment(mServerConfig.getSegmentUrl(), segmentName + SEGMENT_SUFFIX); + } + } catch (IOException e) { + Log.w(LOG_TAG, e); + return Result.failure(); + } catch (InterruptedException e) { + Log.w(LOG_TAG, e); + return Result.failure(); + } + if (DEBUG) Log.d(LOG_TAG, "doWork finished"); + 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); + downloadProgressListener.onDownloadStart(fileName, DownloadType.LOOKUP); + downloadFile(lookupUrl, lookupFile, false); + downloadProgressListener.onDownloadFinished(); + } + } + + 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); + downloadProgressListener.onDownloadStart(fileName, DownloadType.PROFILE); + downloadFile(profileUrl, profileFile, false); + downloadProgressListener.onDownloadFinished(); + } + } + } + } + + 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()) { + if (DEBUG) Log.d(LOG_TAG, "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); + if (DEBUG) Log.d(LOG_TAG, "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 { + HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection(); + connection.setConnectTimeout(5000); + connection.connect(); + + 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); + + downloadProgressListener.onDownloadProgress(fileLength, total); + + if (limitDownloadSpeed) { + // enforce < 16 Mbit/s + long dt = t0 + total / 2096 - System.currentTimeMillis(); + if (dt > 0) { + Thread.sleep(dt); + } + } + } + } + } + + @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); + } + + @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); + } + + enum DownloadType { + LOOKUP, + PROFILE, + SEGMENT + } + + interface DownloadProgressListener { + void onDownloadStart(String downloadName, DownloadType downloadType); + + void onDownloadInfo(String info); + + void onDownloadProgress(int max, int progress); + + void onDownloadFinished(); + } +} diff --git a/brouter-routing-app/src/main/java/btools/routingapp/NotificationHelper.java b/brouter-routing-app/src/main/java/btools/routingapp/NotificationHelper.java deleted file mode 100644 index a97f296..0000000 --- a/brouter-routing-app/src/main/java/btools/routingapp/NotificationHelper.java +++ /dev/null @@ -1,134 +0,0 @@ -package btools.routingapp; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.media.AudioAttributes; -import android.os.Build; -import android.util.Log; - -import androidx.core.app.NotificationCompat; - - -import static android.content.Context.NOTIFICATION_SERVICE; - -public class NotificationHelper { - - private static final boolean DEBUG = false; - - public static String BRouterNotificationChannel1 = "brouter_channel_01"; - - private Context mContext; - private int NOTIFICATION_ID = 111; - private Notification mNotification; - private NotificationManager mNotificationManager; - private PendingIntent mContentIntent; - private CharSequence mContentTitle; - - public NotificationHelper(Context context) { - if (DEBUG) Log.d("NH", "init "); - mContext = context; - createNotificationChannels(); - } - - public void startNotification(Service service) { - if (DEBUG) Log.d("NH", "startNotification "); - - mNotification = createNotification("BRouter Download", "Download some files"); - - if (service != null) service.startForeground(NOTIFICATION_ID, mNotification); - - mNotificationManager.notify(NOTIFICATION_ID, mNotification); - - } - - public void progressUpdate(String text) { - mNotification = createNotification("BRouter Download", text); - mNotification.flags = Notification.FLAG_NO_CLEAR | - Notification.FLAG_ONGOING_EVENT; - - mNotificationManager.notify(NOTIFICATION_ID, mNotification); - } - - - public Notification createNotification(String title, String desc) { - - Intent resultIntent = new Intent(mContext, BInstallerActivity.class); - - Intent notificationIntent = new Intent(); - mContentIntent = PendingIntent.getActivity(mContext, 0, resultIntent, PendingIntent.FLAG_IMMUTABLE); - - mNotificationManager = (NotificationManager) mContext.getSystemService(NOTIFICATION_SERVICE); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - - - final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, BRouterNotificationChannel1); - builder.setSmallIcon(android.R.drawable.stat_sys_download) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContentTitle(title) - .setContentText(desc) - .setTicker(desc) - .setOngoing(true) - .setAutoCancel(true) - .setOnlyAlertOnce(true) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setContentIntent(mContentIntent); - - return builder.build(); - - } else { - final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext); - builder.setSmallIcon(android.R.drawable.stat_sys_download) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContentTitle(title) - .setContentText(desc) - .setOnlyAlertOnce(true) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setContentIntent(mContentIntent); - - return builder.build(); - } - - } - - /** - * create notification channels - */ - public void createNotificationChannels() { - if (DEBUG) Log.d("NH", "createNotificationChannels "); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - - NotificationManager sNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - // Sound channel - CharSequence name = "BRouter Download"; - // The user-visible description of the channel. - String description = "BRouter Download Channel"; //getString(R.string.channel_description); - - NotificationChannel channel = new NotificationChannel(BRouterNotificationChannel1, name, NotificationManager.IMPORTANCE_LOW); - channel.setDescription(description); - AudioAttributes att = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_UNKNOWN) - .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) - .build(); - channel.setSound(null, null); - channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); - - sNotificationManager.createNotificationChannel(channel); - - } - } - - public void stopNotification() { - if (DEBUG) Log.d("NH", "stopNotification "); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - mNotificationManager.deleteNotificationChannel(BRouterNotificationChannel1); - } - mNotificationManager.cancel(NOTIFICATION_ID); - } -} diff --git a/brouter-routing-app/src/main/res/layout/activity_binstaller.xml b/brouter-routing-app/src/main/res/layout/activity_binstaller.xml index 11e4a84..5f0e7c1 100644 --- a/brouter-routing-app/src/main/res/layout/activity_binstaller.xml +++ b/brouter-routing-app/src/main/res/layout/activity_binstaller.xml @@ -1,71 +1,45 @@ - + android:layout_height="match_parent"> - + +