diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 4063b93bfd..149e6a4544 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -16,10 +16,10 @@ jobs: steps: - uses: actions/checkout@v1 - - name: set up JDK 11 + - name: set up JDK 17 uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 17 - name: Copy dummy keys2.txt shell: bash run: | diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 790e9bd3ef..3230502009 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -52,7 +52,11 @@ jobs: # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '17' + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild diff --git a/BUILDING.md b/BUILDING.md index c17633120b..e5a3a50e85 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -7,7 +7,13 @@ See [translations document](TRANSLATIONS.md) for information on how Vespucci is ## Java -Building 16.1 and later requires a Java 11 JDK, previous versions assume Java 8. +Building with gradle requires a Java JDK to be installed. + +- 19.1 and later requires Java 17 +- 16.1 - 19.0 Java 11 +- up to 16.0 Java 8 + +Note that if you are contributing to the code you should restrict yourself only supported Java 8 and Java 11 in Android, in particular the additional de-sugaring provided in later AGP versions doesn't work for Android prior to 5, see https://github.com/MarcusWolschon/osmeditor4android/pull/2131 ## Proguard @@ -15,7 +21,7 @@ All builds now require proguard to be enabled as we have gone over the limit for ## Build flavors -Due to the forced upgrade policy from google from November 1st 2018 onwards we are now supporting two build flavors: _current_ that will target a recent Android SDK and support library and _legacy_ that will support old Android versions as long as practical. +Due to the forced upgrade policy from google from November 1st 2018 onwards we supported two build flavors: _current_ that will target a recent Android SDK and support library and _legacy_ that will support old Android versions as long as practical. For version 15.0 the legacy flavour has been removed as androidx doesn't support versions older than Android 4 and as versions older than 4.1 do not support TLS 1.2 they would be largely non-functional in any case, however flavours may be reactivated if necessary. @@ -83,8 +89,6 @@ To make running individual tests simpler refreshing the gradle tasks (assuming t For the on device tests the time to run the tests can be reduced substantially by running against multiple emulators with ``marathonCurrentDebugAndroidTest`` marathon can execute the tests sharded according to the configuration and retry failed tests. An additional bonus is that the test output is much easier to consume and understand. marathon will include a video of failed tests (we migrated from ``spoon`` to `` marathon`` for 16.1). For more information see [https://marathonlabs.github.io/marathon/](https://marathonlabs.github.io/marathon/). On a 20 core/174GB machine running 20 emulators this reduces the run time to between 15 and 20 minutes. -__Important:__ currently marathon requires requesting and granting the MANAGE_EXTERNAL_STORAGE permission on Android 11 and higher to generate coverage output. A corresponding manifest files is located in src/debug/AndroidManifest.xml with the relevant element commented out. Using this however leads to tests not reflecting the conditions they would be run under in the production app, so you should consider running the tests without the permission during the actual testing and only request it once testing is completed to generate coverage stats. - Notes: * a number of the tests start with the splash screen activity and then wait for the main activity to be started. Experience shows that if one of these fails to complete in certain ways, the following tests that start via the splash screen will not be able to start the main activity. Reason unknown. diff --git a/build.gradle b/build.gradle index 2e9e7ca028..44dc28ea28 100644 --- a/build.gradle +++ b/build.gradle @@ -17,14 +17,14 @@ buildscript { } } - classpath 'com.android.tools.build:gradle:7.2.2' + classpath 'com.android.tools.build:gradle:8.1.0' classpath 'ch.poole.gradle:markdown-gradle-plugin:0.2.4' - classpath 'org.jacoco:org.jacoco.core:0.8.7' + classpath 'org.jacoco:org.jacoco.core:0.8.9' classpath "ch.poole:preset-utils:0.26.0" classpath 'com.github.ksoichiro:gradle-eclipse-aar-plugin:0.3.1' classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:3.0.1' - classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.18' - classpath 'com.malinskiy.marathon:marathon-gradle-plugin:0.8.0' + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.9.1' + classpath 'com.malinskiy.marathon:marathon-gradle-plugin:0.8.4' } } @@ -220,8 +220,10 @@ ext { } android { - + namespace "de.blau.android" + defaultConfig { + applicationId "de.blau.android" versionCode project.getVersionCode() versionName "${project.getVersionName()}" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -253,7 +255,7 @@ android { } // this, unluckily, cannot be set per flavour - compileSdkVersion 31 + compileSdkVersion 33 // Specifies one flavor dimension. flavorDimensions "api" @@ -262,7 +264,7 @@ android { dimension "api" multiDexEnabled true minSdkVersion 16 - targetSdkVersion 31 + targetSdkVersion 33 applicationIdSuffix "" versionNameSuffix "" resValue "string", "content_provider", "de.blau.android.provider" @@ -276,7 +278,9 @@ android { testOptions { unitTests.all { - jacoco { includeNoLocationClasses = true } + jacoco { + includeNoLocationClasses = true + } testLogging { events "passed", "skipped", "failed", "standardOut", "standardError" outputs.upToDateWhen {false} @@ -317,6 +321,10 @@ android { resources.srcDirs += commonTestResources } } + + androidResources { + generateLocaleConfig true + } } ext { @@ -517,6 +525,10 @@ afterEvaluate{ uninstallAll.dependsOn deinstallDummyMeasureApp } + tasks.named('marathonCurrentDebugAndroidTestGenerateMarathonfile') { + doNotTrackState('Gradle 8 compatability hack') + } + // these tasks needs to be created after the uninstall tasks // hardwired single test, this is only needed during initial developement, once the whole test suite has been run @@ -579,21 +591,23 @@ dependencies { implementation "ch.acra:acra-http:$acraVersion" implementation "ch.acra:acra-dialog:$acraVersion" // android support and other libraries - implementation "androidx.activity:activity:1.2.0" - implementation "androidx.fragment:fragment:1.3.0" - implementation "androidx.appcompat:appcompat:1.3.0" + implementation "androidx.activity:activity:1.2.4" + implementation "androidx.fragment:fragment:1.3.6" + implementation "androidx.appcompat:appcompat:1.6.1" + implementation "androidx.appcompat:appcompat-resources:1.6.1" implementation "androidx.recyclerview:recyclerview:1.1.0" implementation "androidx.preference:preference:1.1.0" implementation "com.google.android.material:material:1.3.0" implementation "androidx.annotation:annotation:1.1.0" implementation "androidx.core:core:1.7.0" - implementation "androidx.exifinterface:exifinterface:1.1.0" + implementation "androidx.exifinterface:exifinterface:1.3.6" implementation "androidx.legacy:legacy-preference-v14:1.0.0" implementation "androidx.work:work-runtime:$work_version" implementation "androidx.window:window:1.0.0" implementation 'androidx.core:core-splashscreen:1.0.0-beta02' implementation "androidx.multidex:multidex:2.0.1" implementation "androidx.cardview:cardview:1.0.0" + implementation "androidx.viewpager:viewpager:1.1.0-alpha01" // implementation 'com.heinrichreimersoftware:android-issue-reporter:1.4' @@ -602,8 +616,6 @@ dependencies { implementation 'se.akerfeldt:okhttp-signpost:1.1.0' implementation "com.squareup.okhttp3:okhttp:$okHttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okHttpVersion" - implementation "com.adobe.xmp:xmpcore:6.1.11" - implementation "com.drewnoakes:metadata-extractor:2.16.0" implementation 'com.google.protobuf:protobuf-java:3.12.2' implementation "com.google.code.gson:gson:2.8.9" implementation "com.google.protobuf:protobuf-java:$googleProtobufVersion" @@ -631,7 +643,6 @@ dependencies { implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' implementation 'io.michaelrocks:libphonenumber-android:8.12.39' implementation 'io.noties.markwon:core:4.6.2' - implementation 'com.zeugmasolutions.localehelper:locale-helper-android:1.1.4' implementation 'com.github.zedlabs:ElementHistoryDialog:1.0.6' implementation 'com.caverock:androidsvg-aar:1.4' @@ -642,9 +653,9 @@ dependencies { testImplementation "junit:junit:4.13.2" testImplementation 'xmlpull:xmlpull:1.1.3.1' testImplementation 'net.sf.kxml:kxml2:2.3.0' - testImplementation 'org.robolectric:robolectric:4.8.1' - testImplementation 'androidx.test.ext:junit:1.1.3' - testImplementation 'androidx.test:rules:1.4.0' + testImplementation 'org.robolectric:robolectric:4.10.3' + testImplementation 'androidx.test.ext:junit:1.1.5' + testImplementation 'androidx.test:rules:1.5.0' testImplementation "com.squareup.okhttp3:mockwebserver:$okHttpVersion" testImplementation "pl.droidsonroids.yaml:snakeyaml:1.18-android" testImplementation ("com.orhanobut:mockwebserverplus:1.0.0") { @@ -653,8 +664,8 @@ dependencies { testImplementation "androidx.work:work-testing:$work_version" // Instrumentation tests - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test:rules:1.4.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation "org.hamcrest:hamcrest-library:2.2" androidTestImplementation "com.squareup.okhttp3:mockwebserver:$okHttpVersion" androidTestImplementation "pl.droidsonroids.yaml:snakeyaml:1.18-android" @@ -692,7 +703,15 @@ def coverageSourceDirs = ['src/main/java'] // see https://github.com/gradle/gradle/issues/5184 tasks.withType(Test) { jacoco.includeNoLocationClasses = true - jacoco.excludes = ['jdk.internal.*'] + jacoco.excludes = ['jdk.internal.*', '*'] + // java 17 reflection workarounds for FST + jvmArgs = ["--add-opens","java.base/java.lang=ALL-UNNAMED", + "--add-opens", "java.base/java.math=ALL-UNNAMED", + "--add-opens", "java.base/java.util=ALL-UNNAMED", + "--add-opens", "java.base/java.util.concurrent=ALL-UNNAMED", + "--add-opens", "java.base/java.net=ALL-UNNAMED", + "--add-opens", "java.base/java.text=ALL-UNNAMED", + "--add-opens", "java.base/java.io=ALL-UNNAMED"] } task jacocoTestReport(type:JacocoReport, dependsOn: "testCurrentDebugUnitTest") { @@ -710,8 +729,8 @@ task jacocoTestReport(type:JacocoReport, dependsOn: "testCurrentDebugUnitTest") dir : "$buildDir", include : ['outputs/unit_test_code_coverage/currentDebugUnitTest/testCurrentDebugUnitTest.exec', 'outputs/code-coverage/connected/flavors/CURRENT/*coverage.ec', 'reports/marathon/currentDebugAndroidTest/device-files/*/coverage/*.ec']) reports { - xml.enabled = true - html.enabled = true + xml.required = true + html.required = true } sourceDirectories.from = files(coverageSourceDirs) diff --git a/documentation/docs/help/en/Advanced preferences.md b/documentation/docs/help/en/Advanced preferences.md index d20c220431..759141cd53 100644 --- a/documentation/docs/help/en/Advanced preferences.md +++ b/documentation/docs/help/en/Advanced preferences.md @@ -72,9 +72,9 @@ Select the theme to use. _Follow system_ will follow the setting in the system p Show the menu buttons at the bottom of the screen. Default: _on_. You need to restart the app for changes to this setting to take effect. -### Disable translations +### App language -Use English for the user interface. Google does not provide a supported way to switch languages for individual apps, as a result this setting relies on multiple workarounds that may, or may not, work on your device. Preset translations can be disabled in the preset configurations. +Select a language for the user interface that is different from the device default. On devices running Android 13 and later the app language can be changed in the system settings too. Preset translations can be disabled in the preset configurations. ### Max. number of inline values diff --git a/gradle.properties b/gradle.properties index 782c51e1ee..ea612aef4d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,3 +2,6 @@ android.useAndroidX=true org.gradle.jvmargs=-Xmx6144m android.enableJetifier=true android.enableResourceOptimizations=false +android.nonFinalResIds=false +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e6e5897b5..3a02907943 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/androidTest/AndroidManifest.xml b/src/androidTest/AndroidManifest.xml index 2635080f7b..df62edd638 100644 --- a/src/androidTest/AndroidManifest.xml +++ b/src/androidTest/AndroidManifest.xml @@ -7,14 +7,15 @@ - + + tree = new RTree<>(2, 5); + index.fill(tree); + List photos = new ArrayList<>(); + tree.query(photos); + for (Photo p : photos) { + try { + main.getContentResolver().delete(p.getRefUri(main), null, null); + } catch (SecurityException ex) { + // + } + } + } if (photo1 != null) { photo1.delete(); } diff --git a/src/androidTest/java/de/blau/android/prefs/GeocoderPrefTest.java b/src/androidTest/java/de/blau/android/prefs/GeocoderPrefTest.java index 8115fc60bf..d39d2763e8 100644 --- a/src/androidTest/java/de/blau/android/prefs/GeocoderPrefTest.java +++ b/src/androidTest/java/de/blau/android/prefs/GeocoderPrefTest.java @@ -108,7 +108,8 @@ public void geocoder() { fail(e.getMessage()); } assertTrue(TestUtils.clickText(device, false, main.getString(R.string.okay), true, false)); - assertTrue(TestUtils.clickHome(device, true)); + TestUtils.clickHome(device, false); + TestUtils.clickHome(device, false); try (AdvancedPrefDatabase prefDb = new AdvancedPrefDatabase(main)) { Geocoder[] geocoders = prefDb.getGeocoders(); assertEquals(3, geocoders.length); diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 113ef30baa..bfc272de4f 100755 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -25,9 +25,10 @@ - + + @@ -310,6 +311,17 @@ android:configChanges="orientation|screenSize|keyboardHidden|density|screenLayout|uiMode|fontScale" android:foregroundServiceType="location" android:label="TrackerService" /> + + + + + + = Build.VERSION_CODES.TIRAMISU ? Manifest.permission.READ_MEDIA_IMAGES + : Manifest.permission.WRITE_EXTERNAL_STORAGE; + /** * Minimum change in azimuth before we redraw */ @@ -920,18 +926,18 @@ private void checkPermissions(@NonNull Runnable whenDone) { } } synchronized (storagePermissionLock) { - if (!Util.permissionGranted(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + if (!Util.permissionGranted(this, STORAGE_PERMISSION) || (prefs.scanMediaStore() && !Util.permissionGranted(this, Manifest.permission.ACCESS_MEDIA_LOCATION))) { storagePermissionGranted = false; // Should we show an explanation? if (askedForStoragePermission) { - if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if (ActivityCompat.shouldShowRequestPermissionRationale(this, STORAGE_PERMISSION)) { // for now we just repeat the request (max once) - permissionsList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); + permissionsList.add(STORAGE_PERMISSION); permissionsList.add(Manifest.permission.ACCESS_MEDIA_LOCATION); } } else { - permissionsList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); + permissionsList.add(STORAGE_PERMISSION); permissionsList.add(Manifest.permission.ACCESS_MEDIA_LOCATION); // yes this is weird, but ask the // goog askedForStoragePermission = true; @@ -953,8 +959,8 @@ private void checkPermissions(@NonNull Runnable whenDone) { */ private void getIntentData() { synchronized (newIntentsLock) { - geoData = (GeoUrlData) getIntent().getSerializableExtra(GeoUrlActivity.GEODATA); - rcData = (RemoteControlUrlData) getIntent().getSerializableExtra(RemoteControlUrlActivity.RCDATA); + geoData = Util.getSerializableExtra(getIntent(), GeoUrlActivity.GEODATA, GeoUrlData.class); + rcData = Util.getSerializableExtra(getIntent(), RemoteControlUrlActivity.RCDATA, RemoteControlUrlData.class); shortcutExtras = getIntent().getBundleExtra(Splash.SHORTCUT_EXTRAS_KEY); Uri uri = getIntent().getData(); contentUriType = getIntent().getType(); @@ -965,7 +971,7 @@ private void getIntentData() { Bundle extras = getIntent().getExtras(); if (extras != null) { try { - Uri streamUri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM); + Uri streamUri = Util.getParcelable(extras, Intent.EXTRA_STREAM, Uri.class); Log.d(DEBUG_TAG, "getIntentData EXTRA_STREAM " + streamUri); if (streamUri != null) { contentUri = streamUri; @@ -1041,7 +1047,7 @@ private void processIntents() { case ACTION_PUSH_SELECTION: case ACTION_POP_SELECTION: if (ACTION_PUSH_SELECTION.equals(action)) { - Selection.Ids ids = (Ids) intent.getSerializableExtra(Selection.SELECTION_KEY); + Selection.Ids ids = Util.getSerializableExtra(intent, Selection.SELECTION_KEY, Ids.class); Selection selection = new Selection(); selection.fromIds(App.getDelegator(), ids); logic.pushSelection(selection); @@ -1494,7 +1500,7 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis locationPermissionGranted = true; } } // if not granted do nothing for now - if (permissions[i].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE) && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (permissions[i].equals(STORAGE_PERMISSION) && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // permission was granted :) synchronized (storagePermissionLock) { storagePermissionGranted = true; diff --git a/src/main/java/de/blau/android/layer/photos/MapOverlay.java b/src/main/java/de/blau/android/layer/photos/MapOverlay.java index f80f8785ec..84c949600d 100644 --- a/src/main/java/de/blau/android/layer/photos/MapOverlay.java +++ b/src/main/java/de/blau/android/layer/photos/MapOverlay.java @@ -4,7 +4,6 @@ import java.util.List; import java.util.concurrent.ExecutorService; -import android.Manifest; import android.content.Context; import android.content.res.Resources; import android.database.ContentObserver; @@ -24,6 +23,7 @@ import androidx.fragment.app.FragmentActivity; import de.blau.android.App; import de.blau.android.Logic; +import de.blau.android.Main; import de.blau.android.Map; import de.blau.android.PostAsyncActionHandler; import de.blau.android.R; @@ -181,7 +181,7 @@ public boolean isReadyToDraw() { @Override protected void onDraw(Canvas c, IMapView osmv) { if (isVisible) { - if (needsIndexing() && Util.permissionGranted(map.getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if (needsIndexing() && Util.permissionGranted(map.getContext(), Main.STORAGE_PERMISSION)) { indexer.execute(map::invalidate); return; } diff --git a/src/main/java/de/blau/android/photos/Photo.java b/src/main/java/de/blau/android/photos/Photo.java index e41086d230..851ea58f6b 100644 --- a/src/main/java/de/blau/android/photos/Photo.java +++ b/src/main/java/de/blau/android/photos/Photo.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.util.Objects; import android.content.Context; @@ -14,7 +15,6 @@ import de.blau.android.R; import de.blau.android.osm.BoundingBox; import de.blau.android.osm.GeoPoint; -import de.blau.android.util.ExtendedExifInterface; import de.blau.android.util.Util; import de.blau.android.util.rtree.BoundedObject; @@ -50,7 +50,28 @@ public class Photo implements BoundedObject, GeoPoint { * @throws NumberFormatException If there was a problem parsing the XML. */ public Photo(@NonNull Context context, @NonNull Uri uri, @Nullable String displayName) throws IOException, NumberFormatException { - this(new ExtendedExifInterface(context, uri), uri.toString(), displayName); + this(new ExifInterface(openInputStream(context, uri)), uri.toString(), displayName); + } + + /** + * Get an InputStream from an uri + * + * @param context Android context + * @param uri a content or file uri + * @return an InputStream + * @throws IOException if anything goes wrong + */ + @NonNull + private static InputStream openInputStream(@NonNull Context context, @NonNull Uri uri) throws IOException { + try { + return context.getContentResolver().openInputStream(uri); + } catch (Exception ex) { + // other stuff broken ... for example ArrayIndexOutOfBounds + throw new IOException(ex.getMessage()); + } catch (Error err) { // NOSONAR crashing is not an option + // other stuff broken ... for example NoSuchMethodError + throw new IOException(err.getMessage()); + } } /** @@ -62,7 +83,7 @@ public Photo(@NonNull Context context, @NonNull Uri uri, @Nullable String displa * @throws NumberFormatException If there was a problem parsing the XML. */ public Photo(@NonNull File directory, @NonNull File imageFile) throws IOException, NumberFormatException { - this(new ExtendedExifInterface(imageFile.toString()), imageFile.getAbsolutePath(), imageFile.getName()); + this(new ExifInterface(imageFile.toString()), imageFile.getAbsolutePath(), imageFile.getName()); } /** @@ -73,7 +94,7 @@ public Photo(@NonNull File directory, @NonNull File imageFile) throws IOExceptio * @param displayName a name of the image for display purposes * @throws IOException if location information is missing */ - private Photo(@NonNull ExtendedExifInterface exif, @NonNull String ref, @Nullable String displayName) throws IOException { + private Photo(@NonNull ExifInterface exif, @NonNull String ref, @Nullable String displayName) throws IOException { this.ref = ref; this.displayName = displayName; @@ -87,14 +108,14 @@ private Photo(@NonNull ExtendedExifInterface exif, @NonNull String ref, @Nullabl float lonf = convertToDegree(lonStr); String lonRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF); - if (lonRef != null && !ExtendedExifInterface.EAST.equals(lonRef)) { // deal with the negative degrees + if (lonRef != null && !ExifInterface.LONGITUDE_EAST.equals(lonRef)) { // deal with the negative degrees lonf = -lonf; } float latf = convertToDegree(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)); String latRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF); - if (latRef != null && !ExtendedExifInterface.NORTH.equals(latRef)) { + if (latRef != null && !ExifInterface.LATITUDE_NORTH.equals(latRef)) { latf = -latf; } if (!(Util.notZero(lonf) && Util.notZero(latf))) { @@ -104,11 +125,15 @@ private Photo(@NonNull ExtendedExifInterface exif, @NonNull String ref, @Nullabl lat = (int) (latf * 1E7d); lon = (int) (lonf * 1E7d); Log.d(DEBUG_TAG, "lat: " + lat + " lon: " + lon); - - String dir = exif.getAttribute(ExtendedExifInterface.TAG_GPS_IMG_DIRECTION); + String dir = exif.getAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION); if (dir != null) { - direction = (int) Double.parseDouble(dir); - directionRef = exif.getAttribute(ExtendedExifInterface.TAG_GPS_IMG_DIRECTION_REF); + String[] r = dir.split("/"); + if (r.length != 2) { + return; + } + direction = (int) (Double.valueOf(r[0]) / Double.valueOf(r[1])); + Log.d(DEBUG_TAG, ExifInterface.TAG_GPS_IMG_DIRECTION + " " + dir); + directionRef = exif.getAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION_REF); Log.d(DEBUG_TAG, "dir " + dir + " direction " + direction + " ref " + directionRef); } } @@ -141,7 +166,7 @@ public Photo(int lat, int lon, int direction, @NonNull String ref, @Nullable Str this.lat = lat; this.lon = lon; this.direction = direction; - this.directionRef = ExtendedExifInterface.MAGNETIC_NORTH; + this.directionRef = ExifInterface.GPS_DIRECTION_MAGNETIC; this.ref = ref; this.displayName = displayName; } diff --git a/src/main/java/de/blau/android/photos/PhotoIndex.java b/src/main/java/de/blau/android/photos/PhotoIndex.java index 30e5724823..f484762f9d 100644 --- a/src/main/java/de/blau/android/photos/PhotoIndex.java +++ b/src/main/java/de/blau/android/photos/PhotoIndex.java @@ -46,7 +46,8 @@ public class PhotoIndex extends SQLiteOpenHelper { private static final int DATA_VERSION = 6; - private static final String DEBUG_TAG = "PhotoIndex"; + public static final String DB_NAME = PhotoIndex.class.getSimpleName(); + private static final String DEBUG_TAG = DB_NAME; private static final String NOVESPUCCI = ".novespucci"; @@ -291,16 +292,15 @@ private void indexDirectories() { } finally { close(dbresult2); } - } else { - Log.d(DEBUG_TAG, "Directory " + indir.getAbsolutePath() + " doesn't exist"); - // remove all entries for this directory - db.delete(PHOTOS_TABLE, URI_WHERE, new String[] { indir.getAbsolutePath() }); - db.delete(PHOTOS_TABLE, "dir LIKE ?", new String[] { indir.getAbsolutePath() + "/%" }); + continue; } + Log.d(DEBUG_TAG, "Directory " + indir.getAbsolutePath() + " doesn't exist"); + // remove all entries for this directory + db.delete(PHOTOS_TABLE, URI_WHERE, new String[] { indir.getAbsolutePath() }); + db.delete(PHOTOS_TABLE, "dir LIKE ?", new String[] { indir.getAbsolutePath() + "/%" }); } dbresult.moveToNext(); } - } catch (SQLiteException ex) { // Don't crash just report ACRAHelper.nocrashReport(ex, ex.getMessage()); diff --git a/src/main/java/de/blau/android/prefs/AdvancedPrefEditorFragment.java b/src/main/java/de/blau/android/prefs/AdvancedPrefEditorFragment.java index cf945488b3..b875a56c0d 100644 --- a/src/main/java/de/blau/android/prefs/AdvancedPrefEditorFragment.java +++ b/src/main/java/de/blau/android/prefs/AdvancedPrefEditorFragment.java @@ -5,18 +5,16 @@ import java.util.List; import java.util.Locale; -import com.zeugmasolutions.localehelper.LocaleAwareCompatActivity; - -import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.Bundle; import android.util.Log; import androidx.annotation.NonNull; -import androidx.preference.CheckBoxPreference; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.os.LocaleListCompat; import androidx.preference.ListPreference; import androidx.preference.Preference; -import androidx.preference.PreferenceManager; +import androidx.preference.Preference.OnPreferenceChangeListener; import de.blau.android.R; import de.blau.android.util.LocaleUtils; import de.blau.android.util.Util; @@ -63,6 +61,11 @@ public void onResume() { setupCameraPref(cameraAppPref); } + ListPreference appLocalePref = (ListPreference) getPreferenceScreen().findPreference(r.getString(R.string.config_appLocale_key)); + if (appLocalePref != null) { + setupAppLocalePref(appLocalePref); + } + setListPreferenceSummary(R.string.config_selectCameraApp_key, false); setListPreferenceSummary(R.string.config_theme_key, true); setListPreferenceSummary(R.string.config_fullscreenMode_key, true); @@ -87,6 +90,43 @@ public void onResume() { setTitle(); } + /** + * Setup the app local preference + * + * @param appLocalePref the preference + * + */ + private void setupAppLocalePref(@NonNull ListPreference appLocalePref) { + Locale currentLocale = Locale.getDefault(); + LocaleListCompat appLocales = LocaleUtils.getSupportedLocales(getContext()); + LocaleListCompat currentLocales = AppCompatDelegate.getApplicationLocales(); + if (!currentLocales.isEmpty()) { + LocaleListCompat temp = LocaleListCompat.getAdjustedDefault(); + if (!temp.isEmpty()) { + currentLocale = temp.get(0); + } + } + String[] entries = new String[appLocales.size()]; + String[] values = new String[appLocales.size()]; + for (int i = 0; i < appLocales.size(); i++) { + Locale l = appLocales.get(i); + entries[i] = l.getDisplayName(currentLocale); + values[i] = l.toString(); + } + appLocalePref.setEntryValues(values); + appLocalePref.setEntries(entries); + appLocalePref.setDefaultValue(currentLocale.toString()); + OnPreferenceChangeListener p = (preference, newValue) -> { + Log.d(DEBUG_TAG, "onPreferenceChange appLocale " + newValue); + LocaleListCompat newDefaultList = LocaleListCompat.forLanguageTags((String) newValue); + Locale newDefault = newDefaultList.get(0); + AppCompatDelegate.setApplicationLocales(newDefaultList); + preference.setSummary(newDefault.getDisplayName(newDefault)); + return true; + }; + appLocalePref.setOnPreferenceChangeListener(p); + } + /** * Setup the possible camera apps for selection * @@ -134,23 +174,5 @@ private void setOnPreferenceClickListeners() { return true; }); } - - Preference disableTranslationsPref = getPreferenceScreen().findPreference(r.getString(R.string.config_disableTranslations_key)); - if (disableTranslationsPref != null) { - disableTranslationsPref.setOnPreferenceClickListener(preference -> { - LocaleAwareCompatActivity lac = ((LocaleAwareCompatActivity) getActivity()); - String savedLocaleKey = lac.getString(R.string.config_savedLocale_key); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(lac); - if (((CheckBoxPreference) disableTranslationsPref).isChecked()) { - if (!prefs.contains(savedLocaleKey)) { - prefs.edit().putString(savedLocaleKey, LocaleUtils.toLanguageTag(Locale.getDefault())).commit(); - } - lac.updateLocale(Locale.ENGLISH); - } else { - lac.updateLocale(LocaleUtils.forLanguageTag(prefs.getString(savedLocaleKey, Locale.ENGLISH.toString()))); - } - return true; - }); - } } } diff --git a/src/main/java/de/blau/android/prefs/VespucciURLActivity.java b/src/main/java/de/blau/android/prefs/VespucciURLActivity.java index 32bb6c29d6..74a53cb9fe 100644 --- a/src/main/java/de/blau/android/prefs/VespucciURLActivity.java +++ b/src/main/java/de/blau/android/prefs/VespucciURLActivity.java @@ -4,8 +4,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import com.zeugmasolutions.localehelper.LocaleAwareCompatActivity; - import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -19,6 +17,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; import de.blau.android.Authorize; import de.blau.android.R; import de.blau.android.net.OAuthHelper; @@ -41,7 +40,7 @@ * @author Simon * */ -public class VespucciURLActivity extends LocaleAwareCompatActivity implements OnClickListener { +public class VespucciURLActivity extends AppCompatActivity implements OnClickListener { private static final String DEBUG_TAG = VespucciURLActivity.class.getSimpleName(); private static final int REQUEST_PRESETEDIT = 0; diff --git a/src/main/java/de/blau/android/propertyeditor/PropertyEditorActivity.java b/src/main/java/de/blau/android/propertyeditor/PropertyEditorActivity.java index a7ebe8e37b..c7c33a3f43 100644 --- a/src/main/java/de/blau/android/propertyeditor/PropertyEditorActivity.java +++ b/src/main/java/de/blau/android/propertyeditor/PropertyEditorActivity.java @@ -6,8 +6,6 @@ import org.acra.ACRA; -import com.zeugmasolutions.localehelper.LocaleAwareCompatActivity; - import android.app.Activity; import android.content.Intent; import android.content.res.Configuration; @@ -17,6 +15,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; @@ -45,7 +44,7 @@ * @author simon */ public class PropertyEditorActivity & Serializable, L extends List & Serializable, T extends List> & Serializable> - extends LocaleAwareCompatActivity implements ControlListener { + extends AppCompatActivity implements ControlListener { private static final String DEBUG_TAG = PropertyEditorActivity.class.getSimpleName(); @@ -155,13 +154,16 @@ public void onTopResumedActivityChanged(boolean topResumed) { private void addFromIntent(@NonNull final Intent intent) { Log.d(DEBUG_TAG, "Adding from intent"); - PropertyEditorData[] loadData = PropertyEditorData.deserializeArray(intent.getSerializableExtra(PropertyEditorFragment.TAGEDIT_DATA)); - boolean applyLastAddressTags = getPrimitiveBoolean((Boolean) intent.getSerializableExtra(PropertyEditorFragment.TAGEDIT_LAST_ADDRESS_TAGS)); - boolean showPresets = getPrimitiveBoolean((Boolean) intent.getSerializableExtra(PropertyEditorFragment.TAGEDIT_SHOW_PRESETS)); + PropertyEditorData[] loadData = PropertyEditorData + .deserializeArray(de.blau.android.util.Util.getSerializableExtra(intent, PropertyEditorFragment.TAGEDIT_DATA, PropertyEditorData[].class)); + boolean applyLastAddressTags = getPrimitiveBoolean( + de.blau.android.util.Util.getSerializableExtra(intent, PropertyEditorFragment.TAGEDIT_LAST_ADDRESS_TAGS, Boolean.class)); + boolean showPresets = getPrimitiveBoolean( + de.blau.android.util.Util.getSerializableExtra(intent, PropertyEditorFragment.TAGEDIT_SHOW_PRESETS, Boolean.class)); M extraTags = (M) intent.getSerializableExtra(PropertyEditorFragment.TAGEDIT_EXTRA_TAGS); L presetsToApply = (L) intent.getSerializableExtra(PropertyEditorFragment.TAGEDIT_PRESETSTOAPPLY); - Boolean usePaneLayout = (Boolean) intent.getSerializableExtra(PropertyEditorFragment.PANELAYOUT); + Boolean usePaneLayout = de.blau.android.util.Util.getSerializableExtra(intent, PropertyEditorFragment.PANELAYOUT, Boolean.class); // if we have a preset to auto apply it doesn't make sense to show the Preset tab except if a group is // selected diff --git a/src/main/java/de/blau/android/util/ConfigurationChangeAwareActivity.java b/src/main/java/de/blau/android/util/ConfigurationChangeAwareActivity.java index 5ed42a9ee3..3e67af70cf 100644 --- a/src/main/java/de/blau/android/util/ConfigurationChangeAwareActivity.java +++ b/src/main/java/de/blau/android/util/ConfigurationChangeAwareActivity.java @@ -1,13 +1,12 @@ package de.blau.android.util; -import com.zeugmasolutions.localehelper.LocaleAwareCompatActivity; - import android.content.res.Configuration; import android.util.Log; +import androidx.appcompat.app.AppCompatActivity; import de.blau.android.App; -public abstract class ConfigurationChangeAwareActivity extends LocaleAwareCompatActivity { - +public abstract class ConfigurationChangeAwareActivity extends AppCompatActivity { + private static final String DEBUG_TAG = ConfigurationChangeAwareActivity.class.getSimpleName(); @Override @@ -20,6 +19,6 @@ public void onConfigurationChanged(Configuration newConfig) { Log.d(DEBUG_TAG, "recreating activity " + this.getClass().getCanonicalName()); recreate(); } - Util.clearCaches(this, oldConfig, newConfig); + Util.clearCaches(this, oldConfig, newConfig); } } diff --git a/src/main/java/de/blau/android/util/ExtendedExifInterface.java b/src/main/java/de/blau/android/util/ExtendedExifInterface.java deleted file mode 100644 index 225947d4b2..0000000000 --- a/src/main/java/de/blau/android/util/ExtendedExifInterface.java +++ /dev/null @@ -1,142 +0,0 @@ -package de.blau.android.util; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; - -import com.drew.imaging.jpeg.JpegMetadataReader; -import com.drew.imaging.jpeg.JpegProcessingException; -import com.drew.metadata.Metadata; -import com.drew.metadata.exif.GpsDirectory; - -import android.content.Context; -import android.net.Uri; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.exifinterface.media.ExifInterface; - -/** - * Workaround android SDK brokeness While it is possible to write the direction values, it is not possible to read them - * with the standard android library - * - * @author simon - * - */ -public class ExtendedExifInterface extends ExifInterface { - private static final String DEBUG_TAG = "ExtendedExifInterface"; - - private Metadata metadata; - - public static final String TAG_GPS_IMG_DIRECTION = "GPSImgDirection"; - public static final String TAG_GPS_IMG_DIRECTION_REF = "GPSImgDirectionRef"; - public static final String MAGNETIC_NORTH = "M"; - public static final String NORTH = "N"; - public static final String EAST = "E"; - - /** - * Construct a new instance - * - * @param filename full path to the file - * @throws IOException if something goes wrong - */ - public ExtendedExifInterface(@NonNull String filename) throws IOException { - super(filename); - - File jpegFile = new File(filename); - try { - metadata = JpegMetadataReader.readMetadata(jpegFile); - } catch (JpegProcessingException e) { - // broken Jpeg, ignore - throw new IOException(e.getMessage()); - } catch (Exception ex) { - // other stuff broken ... for example ArrayIndexOutOfBounds - throw new IOException(ex.getMessage()); - } catch (Error err) { // NOSONAR crashing is not an option - // other stuff broken ... for example NoSuchMethodError - throw new IOException(err.getMessage()); - } - } - - /** - * Construct a new instance - * - * Note this variant reads the file twice - * - * @param context Android context - * @param uri a content or file uri - * @throws FileNotFoundException if the file can't be found - * @throws IOException any other kind of error - */ - public ExtendedExifInterface(@NonNull Context context, @NonNull Uri uri) throws IOException { - this(openInputStream(context, uri)); - try (InputStream is = openInputStream(context, uri)) { - metadata = JpegMetadataReader.readMetadata(is); - } catch (JpegProcessingException e) { - // broken Jpeg, ignore - throw new IOException(e.getMessage()); - } - } - - /** - * Get an InputStream from an uri - * - * @param context Android context - * @param uri a content or file uri - * @return an InputStream - * @throws IOException if anything goes wrong - */ - @NonNull - private static InputStream openInputStream(@NonNull Context context, @NonNull Uri uri) throws IOException { - try { - return context.getContentResolver().openInputStream(uri); - } catch (Exception ex) { - // other stuff broken ... for example ArrayIndexOutOfBounds - throw new IOException(ex.getMessage()); - } catch (Error err) { // NOSONAR crashing is not an option - // other stuff broken ... for example NoSuchMethodError - throw new IOException(err.getMessage()); - } - } - - /** - * Hack so that we can close the InputStream - * - * @param is an InputStream, this will be closed in the constructor - * @throws IOException any other kind of error - */ - private ExtendedExifInterface(@NonNull InputStream is) throws IOException { - super(is); - is.close(); - } - - @Override - public String getAttribute(String tag) { - if (!tag.equals(TAG_GPS_IMG_DIRECTION) && !tag.equals(TAG_GPS_IMG_DIRECTION_REF)) { - return super.getAttribute(tag); - } else if (metadata != null) { - // obtain the Exif directory - GpsDirectory directory = metadata.getFirstDirectoryOfType(GpsDirectory.class); - - // query the tag's value - if (tag.equals(TAG_GPS_IMG_DIRECTION) && directory.containsTag(GpsDirectory.TAG_IMG_DIRECTION)) { - String[] r = directory.getString(GpsDirectory.TAG_IMG_DIRECTION).split("/"); - if (r.length != 2) { - return null; - } - double d = Double.valueOf(r[0]) / Double.valueOf(r[1]); - Log.d(DEBUG_TAG, GpsDirectory.TAG_IMG_DIRECTION + " " + d); - return (Double.toString(d)); - } else if (directory.containsTag(GpsDirectory.TAG_IMG_DIRECTION_REF)) { - Log.d(DEBUG_TAG, GpsDirectory.TAG_IMG_DIRECTION_REF + " " + directory.getString(GpsDirectory.TAG_IMG_DIRECTION_REF)); - return directory.getString(GpsDirectory.TAG_IMG_DIRECTION_REF); - } else { - Log.d(DEBUG_TAG, "No direction information"); - return null; - } - } else { - Log.d(DEBUG_TAG, "No valid metadata"); - return null; - } - } -} diff --git a/src/main/java/de/blau/android/util/LocaleUtils.java b/src/main/java/de/blau/android/util/LocaleUtils.java index 3febc19c8f..8c327bc4dc 100644 --- a/src/main/java/de/blau/android/util/LocaleUtils.java +++ b/src/main/java/de/blau/android/util/LocaleUtils.java @@ -1,15 +1,28 @@ package de.blau.android.util; +import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Set; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.app.LocaleConfig; +import android.content.Context; import android.os.Build; +import android.util.Log; import androidx.annotation.NonNull; +import androidx.core.os.LocaleListCompat; +import de.blau.android.R; public final class LocaleUtils { + private static final String DEBUG_TAG = LocaleUtils.class.getSimpleName(); + // list of languages that use Latin script from https://gist.github.com/phil-brown/8056700 private static Set latin = new HashSet<>(Arrays.asList("aa", "ace", "ach", "ada", "af", "agq", "ak", "ale", "amo", "an", "arn", "arp", "arw", "asa", "ast", "ay", "az", "bal", "ban", "bas", "bbc", "bem", "bez", "bi", "bik", "bin", "bku", "bla", "bm", "bqv", "br", "bs", "buc", "bug", "bya", "ca", @@ -171,4 +184,32 @@ public static Locale forLanguageTag(@NonNull String languageTag) { public static boolean usesLatinScript(@NonNull Locale locale) { return latin.contains(locale.getLanguage()); } + + /** + * Get a list of supported locales for the app + * + * For devices prior to Android 13 this reads and parses locales_config.xml directly, note that since we are using + * automatic generation of the file it has a different name. + * + * @param context an Android Context + * @return a LocaleListCompat + */ + public static LocaleListCompat getSupportedLocales(@NonNull Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return LocaleListCompat.wrap(new LocaleConfig(context).getSupportedLocales()); + } + List locales = new ArrayList<>(); + try { + XmlPullParser parser = context.getResources().getXml(R.xml._generated_res_locale_config); + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + if (parser.getEventType() == XmlPullParser.START_TAG && "locale".equals(parser.getName())) { + locales.add(parser.getAttributeValue(0)); + } + parser.next(); + } + } catch (XmlPullParserException | IOException e) { + Log.e(DEBUG_TAG, "Error reading locales_config " + e.getMessage()); + } + return LocaleListCompat.forLanguageTags(String.join(",", locales)); + } } diff --git a/src/main/java/de/blau/android/util/Util.java b/src/main/java/de/blau/android/util/Util.java index 163703e353..5fe99c2cc6 100644 --- a/src/main/java/de/blau/android/util/Util.java +++ b/src/main/java/de/blau/android/util/Util.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.InputStream; +import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; @@ -31,6 +32,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Parcel; +import android.os.Parcelable; import android.text.Editable; import android.text.Html; import android.text.Spanned; @@ -746,4 +748,34 @@ public static boolean isRtlScript(@NonNull Context context) { public static void setCompoundDrawableWithIntrinsicBounds(boolean rtl, @NonNull final TextView textView, @Nullable final Drawable drawable) { textView.setCompoundDrawablesWithIntrinsicBounds(!rtl ? drawable : null, null, rtl ? drawable : null, null); } + + /** + * Wrapper for getSerializableExtra + * + * @param the Serializable type + * @param intent the Intent + * @param key the key + * @param clazz the class we want to retrieve + * @return an instance of clazz or null + */ + @SuppressWarnings({ "deprecation", "unchecked" }) + @Nullable + public static T getSerializableExtra(@NonNull Intent intent, @NonNull String key, @NonNull Class clazz) { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ? intent.getSerializableExtra(key, clazz) : (T) intent.getSerializableExtra(key); + } + + /** + * Wrapper for getParcelable + * + * @param the Parceable type + * @param bundle the Bundle + * @param key the key + * @param clazz the class we want to retrieve + * @return an instance of clazz or null + */ + @SuppressWarnings({ "deprecation", "unchecked" }) + @Nullable + public static T getParcelable(@NonNull Bundle bundle, @NonNull String key, @NonNull Class clazz) { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ? bundle.getParcelable(key, clazz) : (T) bundle.getParcelable(key); + } } diff --git a/src/main/java/de/blau/android/views/ExtendedPagerTabStrip.java b/src/main/java/de/blau/android/views/ExtendedPagerTabStrip.java index 6b2e0303eb..7613a3a104 100644 --- a/src/main/java/de/blau/android/views/ExtendedPagerTabStrip.java +++ b/src/main/java/de/blau/android/views/ExtendedPagerTabStrip.java @@ -7,7 +7,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.viewpager.widget.PagerTabStrip; +import androidx.viewpager.widget.ViewPager; +// this undocumented annotation is required for ViewPAger to detect that the strip is present +@ViewPager.DecorView public class ExtendedPagerTabStrip extends PagerTabStrip { private static final String DEBUG_TAG = ExtendedPagerTabStrip.class.getName(); diff --git a/src/main/res/drawable/vespucci_splash.png b/src/main/res/drawable/vespucci_splash.png index e75d39bec4..6db64f0ab5 100644 Binary files a/src/main/res/drawable/vespucci_splash.png and b/src/main/res/drawable/vespucci_splash.png differ diff --git a/src/main/res/resources.properties b/src/main/res/resources.properties new file mode 100644 index 0000000000..d5a3ddc92a --- /dev/null +++ b/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en-US \ No newline at end of file diff --git a/src/main/res/values/prefkeys.xml b/src/main/res/values/prefkeys.xml index 75ade909f7..e1ce79bc66 100644 --- a/src/main/res/values/prefkeys.xml +++ b/src/main/res/values/prefkeys.xml @@ -89,8 +89,7 @@ useBarometricHeight useUrlForFeedback preferRemovableStorage - disableTranslations - savedLocale + appLocale nameCap indexMediaStore wayNodeDragging diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 3b5d130d60..d32bfd30eb 100755 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1174,8 +1174,8 @@ Select app styling. Enable split action bar Show the menu buttons at the bottom of the screen. Requires starting Vespucci again, and Android 4.0 or later. - Disable translations - Use English for the user interface + App language + Select non-standard app language Max. number of inline values Maximum number of inline values displayed in tag form. %1$d values diff --git a/src/main/res/xml-v19/advancedpreferences.xml b/src/main/res/xml-v19/advancedpreferences.xml index 3f3bf4443f..3f6a12bd6e 100644 --- a/src/main/res/xml-v19/advancedpreferences.xml +++ b/src/main/res/xml-v19/advancedpreferences.xml @@ -97,11 +97,11 @@ android:key="@string/config_splitActionBarEnabled_key" android:summary="@string/config_splitActionBarEnabled_summary" android:title="@string/config_splitActionBarEnabled_title" /> - + - + - + - + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + +