taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

commit d41c9b30baa545b352a8e2ff68fdf00a01421e6d
parent 1e558f482d0459aa452f121edbca2e8d749b39e0
Author: Bohdan Potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Tue,  7 Oct 2025 19:55:07 +0200

[donau-verificator] Small fix

Diffstat:
Mdonau-verificator/README.md | 4+++-
Mdonau-verificator/build.gradle | 27++++++++++++++++++---------
Mdonau-verificator/src/main/AndroidManifest.xml | 9+++++++--
Mdonau-verificator/src/main/java/net/taler/donauverificator/MainActivity.java | 25+++++++++++++++++++++++++
Mdonau-verificator/src/main/java/net/taler/donauverificator/Results.java | 434++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Adonau-verificator/src/main/java/net/taler/donauverificator/SettingsActivity.java | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adonau-verificator/src/main/res/drawable/divider_line.xml | 5+++++
Adonau-verificator/src/main/res/drawable/ic_arrow_back.xml | 10++++++++++
Adonau-verificator/src/main/res/drawable/ic_info.xml | 10++++++++++
Adonau-verificator/src/main/res/drawable/ic_settings.xml | 10++++++++++
Adonau-verificator/src/main/res/drawable/ic_warning.xml | 13+++++++++++++
Mdonau-verificator/src/main/res/layout/activity_main.xml | 48+++++++++++++++++++++++++++++++++++++++++-------
Adonau-verificator/src/main/res/layout/activity_settings.xml | 29+++++++++++++++++++++++++++++
Mdonau-verificator/src/main/res/layout/fragment_results.xml | 316++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Adonau-verificator/src/main/res/menu/main_menu.xml | 9+++++++++
Adonau-verificator/src/main/res/values-night/colors.xml | 20++++++++++++++++++++
Mdonau-verificator/src/main/res/values-night/themes.xml | 31++++++++++++++++++++-----------
Mdonau-verificator/src/main/res/values/colors.xml | 28+++++++++++++++-------------
Mdonau-verificator/src/main/res/values/strings.xml | 20++++++++++++++++++++
Mdonau-verificator/src/main/res/values/themes.xml | 31++++++++++++++++++++-----------
Adonau-verificator/src/main/res/xml/preferences.xml | 13+++++++++++++
21 files changed, 979 insertions(+), 184 deletions(-)

diff --git a/donau-verificator/README.md b/donau-verificator/README.md @@ -4,7 +4,7 @@ The app verifies the donation statement made by a Donau. ## Testing 1. With provided QR-Codes in the root app directory 2. For test purposes, a string of a valid donation statement is already hard coded. -3. With the defined URI scheme following command can be used: +3. With the defined URI scheme following command can be used (developer mode must be enabled for `donau+http` URIs): ```bash adb shell am start -a android.intent.action.VIEW -d "donau://example.com/megacharity/1234/2024/7560001010000/1234?total=EUR:15&sig=ED25519:SAAM5BA1F9H4VT6T78CFC3X63HAMY2TXB597XBVZ0EMXEZ90QPJ3000BXDBJ3ECHGB8AEX9FFQ5BAXVSF6X6NXM98PY353F2R99PP1R&pub=E24CDJHGSPZG20ZSSTMTBREGCCP495WKETQYCYA9C93EPMZN4FEG" ``` @@ -23,3 +23,4 @@ Mac OS, Linux: Windows: - gradlew.bat +0 +\ No newline at end of file diff --git a/donau-verificator/build.gradle b/donau-verificator/build.gradle @@ -26,6 +26,10 @@ android { versionCode 1 versionName "1.0" + buildConfigField "boolean", "ENABLE_DEVELOPER_MODE", "true" + buildConfigField "boolean", "ALLOW_INSECURE_HTTP", "true" + manifestPlaceholders = [usesCleartextTraffic: "true"] + archivesBaseName = "donau-verificator" externalNativeBuild { cmake { @@ -39,6 +43,9 @@ android { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } + debug { + // inherit defaults; specific behavior controlled via product flavors + } } flavorDimensions = ["distributionChannel"] @@ -64,6 +71,7 @@ android { buildFeatures { viewBinding true prefab true + buildConfig true } packagingOptions { @@ -110,14 +118,16 @@ android { dependencies { // TODO: do not vendor pre-built libsodium implementation fileTree(dir: 'src/main/libs/', include: ['libsodium-1.0.19.0.aar']) - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.10.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' - implementation 'androidx.navigation:navigation-fragment:2.7.5' - implementation 'androidx.navigation:navigation-ui:2.7.5' + implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'com.google.android.material:material:1.13.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.1' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.4' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4' + implementation 'androidx.navigation:navigation-fragment:2.9.5' + implementation 'androidx.navigation:navigation-ui:2.9.5' implementation 'com.github.yuriy-budiyev:code-scanner:2.3.2' // https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk18on implementation group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.70' -} -\ No newline at end of file + implementation 'androidx.preference:preference-ktx:1.2.1' + implementation 'androidx.preference:preference:1.2.1' +} diff --git a/donau-verificator/src/main/AndroidManifest.xml b/donau-verificator/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.CAMERA" android:required="true"/> @@ -18,6 +19,7 @@ android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.Verifaction" + android:usesCleartextTraffic="${usesCleartextTraffic}" tools:targetApi="31"> <activity android:name=".MainActivity" @@ -29,6 +31,10 @@ <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> + <activity + android:name=".SettingsActivity" + android:label="@string/menu_settings" + android:exported="false" /> <activity android:name=".Results" android:exported="true"> <intent-filter> @@ -40,4 +46,4 @@ </activity> </application> -</manifest> -\ No newline at end of file +</manifest> diff --git a/donau-verificator/src/main/java/net/taler/donauverificator/MainActivity.java b/donau-verificator/src/main/java/net/taler/donauverificator/MainActivity.java @@ -24,6 +24,9 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.widget.Toast; import com.budiyev.android.codescanner.CodeScanner; @@ -74,6 +77,11 @@ public class MainActivity extends AppCompatActivity { //temporary for debugging (valid donation statement from sample QR code) sendRequestDialog(DEBUG_DONATION_STATEMENT); + binding.settingsButton.setOnClickListener(v -> { + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + }); + } @Override @@ -157,4 +165,21 @@ public class MainActivity extends AppCompatActivity { } } + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.main_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_settings) { + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + return true; + } + return super.onOptionsItemSelected(item); + } + } diff --git a/donau-verificator/src/main/java/net/taler/donauverificator/Results.java b/donau-verificator/src/main/java/net/taler/donauverificator/Results.java @@ -17,43 +17,84 @@ package net.taler.donauverificator; import android.content.Intent; +import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; +import android.util.Log; import android.view.View; -import android.widget.TableLayout; import android.widget.TextView; +import androidx.annotation.ColorRes; +import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatActivity; - +import androidx.core.content.ContextCompat; +import androidx.preference.PreferenceManager; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.card.MaterialCardView; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.zip.GZIPInputStream; +import java.util.zip.InflaterInputStream; public class Results extends AppCompatActivity { static { System.loadLibrary("verification"); } + private static final String TAG = "Results"; + // lsd0013 format: donau://host/year/taxid/salt?total=...&sig=ED25519:... // CrockfordBase32 encoded: SIGNATURE, PUBLICKEY // TODO: Salt and taxId should maybe also be encoded + private String uriScheme; + private String host; + private int port = -1; + private final List<String> authorityPathSegments = new ArrayList<>(); private String year; private String totalAmount; private String taxId; + private String hostDisplay; private String salt; private String eddsaSignature; private String publicKey; TextView sigStatusView; - TextView yearView; - TextView taxidView; - TextView totalView; - TableLayout tableLayout; + View summaryContainer; + TextView hostLabelView; + TextView hostValueView; + TextView yearLabelView; + TextView yearValueView; + TextView taxLabelView; + TextView taxValueView; + TextView amountLabelView; + TextView amountValueView; + MaterialCardView statusCard; + MaterialButton signatureButton; public enum SignatureStatus { INVALID_SCHEME, INVALID_NUMBER_OF_ARGUMENTS, MALFORMED_ARGUMENT, + INSECURE_HTTP_UNSUPPORTED, + INSECURE_HTTP_DISABLED, + KEY_DOWNLOAD_FAILED, + KEY_NOT_FOUND, SIGNATURE_INVALID, SIGNATURE_VALID; } @@ -64,11 +105,28 @@ public class Results extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.fragment_results); sigStatusView = findViewById(R.id.sigStatus); - yearView = findViewById(R.id.year); - taxidView = findViewById(R.id.taxid); - totalView = findViewById(R.id.total); - tableLayout = findViewById(R.id.tableLayout); - tableLayout.setVisibility(View.INVISIBLE); + statusCard = findViewById(R.id.statusCard); + summaryContainer = findViewById(R.id.summaryContainer); + hostLabelView = findViewById(R.id.hostLabel); + hostValueView = findViewById(R.id.hostValue); + yearLabelView = findViewById(R.id.yearLabel); + yearValueView = findViewById(R.id.yearValue); + taxLabelView = findViewById(R.id.taxLabel); + taxValueView = findViewById(R.id.taxValue); + amountLabelView = findViewById(R.id.amountLabel); + amountValueView = findViewById(R.id.amountValue); + signatureButton = findViewById(R.id.signatureButton); + if (signatureButton != null) { + signatureButton.setVisibility(View.GONE); + signatureButton.setEnabled(false); + signatureButton.setOnClickListener(v -> showSignatureDialog()); + } + if (statusCard != null) { + statusCard.setCardBackgroundColor(ContextCompat.getColor(this, R.color.validation_surface_neutral)); + } + sigStatusView.setTextColor(ContextCompat.getColor(this, R.color.text_primary)); + setSummaryLabels(); + setupBackNavigation(); Intent intent = getIntent(); Uri uri = resolveUri(intent); @@ -77,8 +135,8 @@ public class Results extends AppCompatActivity { return; } - String scheme = uri.getScheme(); - if (!isSupportedScheme(scheme)) { + uriScheme = uri.getScheme(); + if (!isSupportedScheme(uriScheme)) { statusHandling(SignatureStatus.INVALID_SCHEME); return; } @@ -89,13 +147,7 @@ public class Results extends AppCompatActivity { statusHandling(parseStatus); return; } - - try { - checkSignature(); - } catch (Exception e) { - throw new RuntimeException(e); - } - + startVerificationAsync(); } private void checkSignature() throws Exception{ @@ -110,36 +162,156 @@ public class Results extends AppCompatActivity { } } + private void startVerificationAsync() { + new Thread(() -> { + SignatureStatus status = ensurePublicKeyAvailable(); + runOnUiThread(() -> { + if (isFinishing() || isDestroyed()) { + return; + } + if (status != null) { + statusHandling(status); + return; + } + try { + checkSignature(); + } catch (Exception e) { + Log.e(TAG, "Signature verification failed", e); + statusHandling(SignatureStatus.SIGNATURE_INVALID); + } + }); + }).start(); + } + private void statusHandling(SignatureStatus es) { - View rootView = findViewById(R.id.root_view); switch (es) { case INVALID_SCHEME: - sigStatusView.setText(R.string.invalid_scheme); - rootView.setBackgroundResource(R.color.red); + updateStatusCard(R.string.invalid_scheme, R.color.validation_surface_error, R.color.red, false); break; case INVALID_NUMBER_OF_ARGUMENTS: - sigStatusView.setText(R.string.invalid_number_of_arguments); - rootView.setBackgroundResource(R.color.red); + updateStatusCard(R.string.invalid_number_of_arguments, R.color.validation_surface_error, R.color.red, false); break; case MALFORMED_ARGUMENT: - sigStatusView.setText(R.string.malformed_argument); - rootView.setBackgroundResource(R.color.red); + updateStatusCard(R.string.malformed_argument, R.color.validation_surface_error, R.color.red, false); + break; + case INSECURE_HTTP_UNSUPPORTED: + updateStatusCard(R.string.status_insecure_http_unsupported, R.color.validation_surface_neutral, R.color.text_primary, false); + break; + case INSECURE_HTTP_DISABLED: + updateStatusCard(R.string.status_insecure_http_disabled, R.color.validation_surface_neutral, R.color.text_primary, false); + break; + case KEY_DOWNLOAD_FAILED: + updateStatusCard(R.string.status_key_download_failed, R.color.validation_surface_error, R.color.red, false); + break; + case KEY_NOT_FOUND: + updateStatusCard(R.string.status_key_not_found, R.color.validation_surface_info, R.color.colorSecondary, false); break; case SIGNATURE_INVALID: - sigStatusView.setText(R.string.invalid_signature); - rootView.setBackgroundResource(R.color.red); + updateStatusCard(R.string.invalid_signature, R.color.validation_surface_error, R.color.red, false); break; case SIGNATURE_VALID: - tableLayout.setVisibility(View.VISIBLE); - sigStatusView.setText(R.string.valid_signature); - yearView.setText(year); - taxidView.setText(taxId); - totalView.setText(totalAmount); - rootView.setBackgroundResource(R.color.green); + updateStatusCard(R.string.valid_signature, R.color.validation_surface_success, R.color.green, true); + setSummaryValues(year, taxId, totalAmount); break; } } + private void updateStatusCard(@StringRes int messageRes, + @ColorRes int backgroundColorRes, + @ColorRes int textColorRes, + boolean showDetails) { + sigStatusView.setText(messageRes); + if (statusCard != null) { + statusCard.setCardBackgroundColor(ContextCompat.getColor(this, backgroundColorRes)); + } + sigStatusView.setTextColor(ContextCompat.getColor(this, textColorRes)); + if (summaryContainer != null) { + summaryContainer.setVisibility(showDetails ? View.VISIBLE : View.GONE); + } + if (signatureButton != null) { + signatureButton.setVisibility(showDetails ? View.VISIBLE : View.GONE); + signatureButton.setEnabled(showDetails); + } + if (showDetails) { + setSummaryValues(year, taxId, totalAmount); + } else { + clearSummaryValues(); + } + } + + private void setSummaryLabels() { + if (hostLabelView != null) { + hostLabelView.setText(R.string.label_host); + hostLabelView.setTextColor(ContextCompat.getColor(this, R.color.text_secondary)); + } + if (yearLabelView != null) { + yearLabelView.setText(R.string.label_year); + yearLabelView.setTextColor(ContextCompat.getColor(this, R.color.text_secondary)); + } + if (taxLabelView != null) { + taxLabelView.setText(R.string.label_tax_id); + taxLabelView.setTextColor(ContextCompat.getColor(this, R.color.text_secondary)); + } + if (amountLabelView != null) { + amountLabelView.setText(R.string.label_amount); + amountLabelView.setTextColor(ContextCompat.getColor(this, R.color.text_secondary)); + } + clearSummaryValues(); + } + + private void clearSummaryValues() { + if (hostValueView != null) { + hostValueView.setText(""); + } + if (yearValueView != null) { + yearValueView.setText(""); + } + if (taxValueView != null) { + taxValueView.setText(""); + } + if (amountValueView != null) { + amountValueView.setText(""); + } + } + + private void setSummaryValues(String yearValue, String taxValue, String amountValue) { + if (hostValueView != null) { + hostValueView.setText(valueOrUnknown(hostDisplay)); + } + if (yearValueView != null) { + yearValueView.setText(valueOrUnknown(yearValue)); + } + if (taxValueView != null) { + taxValueView.setText(valueOrUnknown(taxValue)); + } + if (amountValueView != null) { + amountValueView.setText(valueOrUnknown(amountValue)); + } + } + + private void setupBackNavigation() { + View backButton = findViewById(R.id.backButton); + if (backButton != null) { + backButton.setOnClickListener(v -> finish()); + } + } + + private void showSignatureDialog() { + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.signature_info_title) + .setMessage(getString( + R.string.signature_info_message, + valueOrUnknown(salt), + valueOrUnknown(eddsaSignature), + valueOrUnknown(publicKey))) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + + private String valueOrUnknown(String candidate) { + return isEmpty(candidate) ? getString(R.string.value_unknown) : candidate; + } + public native int ed25519_verify(String year, String totalAmount, String taxId, String salt, String eddsaSignature, String publicKey); @@ -165,16 +337,19 @@ public class Results extends AppCompatActivity { } private void resetParsedFields() { + authorityPathSegments.clear(); year = null; totalAmount = null; taxId = null; + hostDisplay = null; salt = null; eddsaSignature = null; publicKey = null; } private SignatureStatus parseDonauUri(Uri uri) { - String host = uri.getHost(); + host = uri.getHost(); + port = uri.getPort(); if (isEmpty(host)) { return SignatureStatus.MALFORMED_ARGUMENT; } @@ -184,15 +359,26 @@ public class Results extends AppCompatActivity { return SignatureStatus.INVALID_NUMBER_OF_ARGUMENTS; } + authorityPathSegments.clear(); + if (segments.size() < 3) { return SignatureStatus.INVALID_NUMBER_OF_ARGUMENTS; } - int lastIndex = segments.size() - 1; - String saltCandidate = segments.get(lastIndex); - String taxIdCandidate = segments.get(lastIndex - 1); - String yearCandidate = segments.get(lastIndex - 2); + int yearIndex = segments.size() - 3; + for (int i = 0; i < yearIndex; i++) { + String segment = segments.get(i); + String trimmed = segment != null ? segment.trim() : null; + if (!isEmpty(trimmed)) { + authorityPathSegments.add(trimmed); + } + } + hostDisplay = buildHostDisplay(); + + hostDisplay = buildHostDisplay(); + + String yearCandidate = segments.get(yearIndex); if (yearCandidate != null) { yearCandidate = yearCandidate.trim(); } @@ -202,6 +388,7 @@ public class Results extends AppCompatActivity { year = yearCandidate; + String taxIdCandidate = segments.get(yearIndex + 1); if (taxIdCandidate != null) { taxIdCandidate = taxIdCandidate.trim(); } @@ -210,6 +397,7 @@ public class Results extends AppCompatActivity { } taxId = taxIdCandidate; + String saltCandidate = segments.get(yearIndex + 2); if (saltCandidate != null) { saltCandidate = saltCandidate.trim(); } @@ -233,17 +421,171 @@ public class Results extends AppCompatActivity { return SignatureStatus.MALFORMED_ARGUMENT; } - //TODO: Remove to follow the lsd0013 - // we can do it, when we have a donau instance in open web String publicKeyParam = uri.getQueryParameter("pub"); - if (isEmpty(publicKeyParam)) { - return SignatureStatus.MALFORMED_ARGUMENT; - } - publicKey = publicKeyParam.trim(); + publicKey = isEmpty(publicKeyParam) ? null : publicKeyParam.trim(); return null; } + private SignatureStatus ensurePublicKeyAvailable() { + boolean insecureScheme = isInsecureScheme(); + boolean hasEmbeddedPub = !isEmpty(publicKey); + if (insecureScheme) { + if (!BuildConfig.ALLOW_INSECURE_HTTP) { + return SignatureStatus.INSECURE_HTTP_UNSUPPORTED; + } + if (!isDeveloperModeEnabled()) { + return SignatureStatus.INSECURE_HTTP_DISABLED; + } + if (hasEmbeddedPub && isTestingHost()) { + return null; + } + } + + if (!insecureScheme && hasEmbeddedPub) { + return null; + } + + try { + String fetchedKey = fetchSigningKey(insecureScheme); + if (isEmpty(fetchedKey)) { + return SignatureStatus.KEY_NOT_FOUND; + } + publicKey = fetchedKey; + return null; + } catch (IOException | JSONException e) { + Log.e(TAG, "Failed to download Donau signing keys", e); + return SignatureStatus.KEY_DOWNLOAD_FAILED; + } + } + + private String buildHostDisplay() { + if (isEmpty(host)) { + return null; + } + StringBuilder builder = new StringBuilder(host); + if (port != -1) { + builder.append(":").append(port); + } + for (String segment : authorityPathSegments) { + if (!isEmpty(segment)) { + builder.append("/").append(segment); + } + } + return builder.toString(); + } + + private String fetchSigningKey(boolean insecure) throws IOException, JSONException { + URL keysUrl = buildKeysUrl(insecure); + if (keysUrl == null) { + return null; + } + HttpURLConnection connection = (HttpURLConnection) keysUrl.openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + connection.setInstanceFollowRedirects(false); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("Accept-Encoding", "gzip, deflate"); + try { + int status = connection.getResponseCode(); + if (status != HttpURLConnection.HTTP_OK) { + throw new IOException("HTTP " + status); + } + InputStream input = connection.getInputStream(); + String encoding = connection.getHeaderField("Content-Encoding"); + if (encoding != null) { + if ("gzip".equalsIgnoreCase(encoding)) { + input = new GZIPInputStream(input); + } else if ("deflate".equalsIgnoreCase(encoding)) { + input = new InflaterInputStream(input); + } + } + String body; + try { + body = readStream(input); + } finally { + input.close(); + } + JSONObject json = new JSONObject(body); + JSONArray signkeys = json.optJSONArray("signkeys"); + if (signkeys == null) { + return null; + } + for (int i = 0; i < signkeys.length(); i++) { + JSONObject entry = signkeys.optJSONObject(i); + if (entry != null) { + String keyCandidate = entry.optString("key", null); + if (isEmpty(keyCandidate) && entry.has("key")) { + JSONObject keyObj = entry.optJSONObject("key"); + if (keyObj != null) { + keyCandidate = keyObj.optString("eddsa_pub", null); + } + } + if (!isEmpty(keyCandidate)) { + return keyCandidate.trim(); + } + } else { + String keyCandidate = signkeys.optString(i, null); + if (!isEmpty(keyCandidate)) { + return keyCandidate.trim(); + } + } + } + return null; + } finally { + connection.disconnect(); + } + } + + private URL buildKeysUrl(boolean insecure) { + if (isEmpty(host)) { + return null; + } + try { + Uri.Builder builder = new Uri.Builder() + .scheme(insecure ? "http" : "https") + .encodedAuthority(port != -1 ? host + ":" + port : host); + for (String segment : authorityPathSegments) { + if (!isEmpty(segment)) { + builder.appendPath(segment.trim()); + } + } + builder.appendPath("keys"); + return new URL(builder.build().toString()); + } catch (MalformedURLException | IllegalArgumentException e) { + Log.e(TAG, "Invalid /keys URL", e); + return null; + } + } + + private boolean isInsecureScheme() { + return uriScheme != null && "donau+http".equalsIgnoreCase(uriScheme); + } + + private boolean isDeveloperModeEnabled() { + if (!BuildConfig.ENABLE_DEVELOPER_MODE) { + return false; + } + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(this); + return prefs.getBoolean(SettingsActivity.KEY_DEVELOPER_MODE, false); + } + + private boolean isTestingHost() { + return host != null && host.equalsIgnoreCase("example.com"); + } + + private String readStream(InputStream input) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(input, java.nio.charset.StandardCharsets.UTF_8)); + StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + } + return builder.toString(); + } + private boolean isFourDigitYear(String value) { if (value == null || value.length() != 4) { return false; diff --git a/donau-verificator/src/main/java/net/taler/donauverificator/SettingsActivity.java b/donau-verificator/src/main/java/net/taler/donauverificator/SettingsActivity.java @@ -0,0 +1,71 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +package net.taler.donauverificator; + +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import com.google.android.material.appbar.MaterialToolbar; +import androidx.preference.PreferenceFragmentCompat; + +public class SettingsActivity extends AppCompatActivity { + + public static final String KEY_DEVELOPER_MODE = "pref_developer_mode"; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + + MaterialToolbar toolbar = findViewById(R.id.settingsToolbar); + setSupportActionBar(toolbar); + toolbar.setNavigationOnClickListener(v -> finish()); + + if (savedInstanceState == null) { + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.settings_container, new SettingsFragment()) + .commit(); + } + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + public static class SettingsFragment extends PreferenceFragmentCompat { + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.preferences, rootKey); + + } + } +} diff --git a/donau-verificator/src/main/res/drawable/divider_line.xml b/donau-verificator/src/main/res/drawable/divider_line.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <size android:height="1dp" /> + <solid android:color="@color/validation_surface_neutral" /> +</shape> diff --git a/donau-verificator/src/main/res/drawable/ic_arrow_back.xml b/donau-verificator/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/colorPrimary" + android:pathData="M20,11H7.83l4.58-4.59L11,5l-7,7 7,7 1.41-1.41L7.83,13H20v-2z"/> +</vector> diff --git a/donau-verificator/src/main/res/drawable/ic_info.xml b/donau-verificator/src/main/res/drawable/ic_info.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/colorPrimary" + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm0,3c0.83,0 1.5,0.67 1.5,1.5S12.83,8 12,8s-1.5,-0.67 -1.5,-1.5S11.17,5 12,5zm2,14h-4v-2h1v-4h-1v-2h3v6h1v2z"/> +</vector> diff --git a/donau-verificator/src/main/res/drawable/ic_settings.xml b/donau-verificator/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/colorPrimary" + android:pathData="M19.14,12.94c0.04,-0.31 0.06,-0.63 0.06,-0.94s-0.02,-0.63 -0.06,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.4 0.12,-0.61l-1.92,-3.32c-0.11,-0.2 -0.36,-0.28 -0.58,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.5,2.81c-0.03,-0.22 -0.22,-0.39 -0.44,-0.39h-3.12c-0.22,0 -0.41,0.16 -0.44,0.39L10.3,5.35c-0.59,0.24 -1.13,0.56 -1.62,0.94L6.29,5.33c-0.21,-0.08 -0.47,0.02 -0.58,0.22L3.79,8.87c-0.11,0.21 -0.07,0.47 0.12,0.61l2.03,1.58c-0.04,0.31 -0.06,0.63 -0.06,0.94s0.02,0.63 0.06,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.4 -0.12,0.61l1.92,3.32c0.11,0.2 0.36,0.28 0.58,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.2,2.54c0.03,0.22 0.22,0.39 0.44,0.39h3.12c0.22,0 0.41,-0.16 0.44,-0.39l0.2,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.21,0.08 0.47,-0.02 0.58,-0.22l1.92,-3.32c0.11,-0.21 0.07,-0.47 -0.12,-0.61l-2.03,-1.58zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/> +</vector> diff --git a/donau-verificator/src/main/res/drawable/ic_warning.xml b/donau-verificator/src/main/res/drawable/ic_warning.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/colorPrimary" + android:pathData="M1,21h22L12,2 1,21z" /> + <path + android:fillColor="@color/colorOnPrimary" + android:pathData="M13,18h-2v-2h2v2zM13,14h-2v-4h2v4z" /> +</vector> diff --git a/donau-verificator/src/main/res/layout/activity_main.xml b/donau-verificator/src/main/res/layout/activity_main.xml @@ -4,12 +4,47 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingTop="?attr/actionBarSize"> + android:padding="16dp" + android:background="@color/colorBackground"> + <TextView + android:id="@+id/appTitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/title_donau_verifier" + android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" + android:textColor="@color/text_primary" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/settingsButton" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/scanner_view" /> - <com.budiyev.android.codescanner.CodeScannerView - android:id="@+id/scanner_view" - android:layout_width="match_parent" - android:layout_height="match_parent"/> + <com.google.android.material.button.MaterialButton + android:id="@+id/settingsButton" + style="@style/Widget.MaterialComponents.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/menu_settings" + android:textAllCaps="false" + android:textColor="@color/colorPrimary" + app:icon="@drawable/ic_settings" + app:iconGravity="textStart" + app:iconPadding="4dp" + app:iconTint="@color/colorPrimary" + app:layout_constraintStart_toEndOf="@id/appTitle" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/appTitle" + app:layout_constraintBottom_toBottomOf="@id/appTitle" /> -</androidx.constraintlayout.widget.ConstraintLayout> -\ No newline at end of file + <com.budiyev.android.codescanner.CodeScannerView + android:id="@+id/scanner_view" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginTop="12dp" + app:layout_constraintTop_toBottomOf="@id/appTitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/donau-verificator/src/main/res/layout/activity_settings.xml b/donau-verificator/src/main/res/layout/activity_settings.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/colorBackground"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/settingsToolbar" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:background="@android:color/transparent" + android:title="@string/menu_settings" + android:titleTextColor="@color/text_primary" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:navigationIcon="@drawable/ic_arrow_back"/> + + <FrameLayout + android:id="@+id/settings_container" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintTop_toBottomOf="@id/settingsToolbar" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/donau-verificator/src/main/res/layout/fragment_results.xml b/donau-verificator/src/main/res/layout/fragment_results.xml @@ -1,100 +1,249 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" -android:id="@+id/root_view" -xmlns:tools="http://schemas.android.com/tools" -android:layout_width="match_parent" -android:layout_height="match_parent" -android:paddingStart="10dp" -android:paddingEnd="10dp" -android:orientation="vertical" -tools:context=".MainActivity"> - -<ImageView - android:id="@+id/image" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:maxWidth="200dp" - android:adjustViewBounds="true" - android:paddingBottom="30dp" - android:src="@drawable/ngi_taler_logo" - android:contentDescription="@string/NGI_TALER_logo" /> - -<TableLayout - android:id="@+id/tableLayout" - android:layout_below="@+id/image" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:stretchColumns="1"> - - <!-- Row 1 --> - <TableRow - android:layout_width="match_parent" - android:layout_height="wrap_content"> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/root_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="16dp" + android:background="@color/colorBackground" + tools:context=".Results"> - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:padding="8dp" - android:textStyle="bold" - android:text="Year: " - android:textSize="18sp" /> + <com.google.android.material.button.MaterialButton + android:id="@+id/backButton" + style="@style/Widget.MaterialComponents.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/action_back" + android:textAllCaps="false" + android:textColor="@color/colorPrimary" + app:icon="@drawable/ic_arrow_back" + app:iconTint="@color/colorPrimary" + app:iconGravity="textStart" + app:iconPadding="4dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - <TextView - android:id="@+id/year" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:padding="8dp" - android:textSize="18sp" /> - </TableRow> + <com.google.android.material.button.MaterialButton + android:id="@+id/signatureButton" + style="@style/Widget.MaterialComponents.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/action_signature_info" + android:textAllCaps="false" + android:textColor="@color/colorPrimary" + android:visibility="visible" + app:icon="@drawable/ic_info" + app:iconTint="@color/colorPrimary" + app:iconGravity="textStart" + app:iconPadding="4dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/backButton" + app:layout_constraintBaseline_toBaselineOf="@id/backButton" /> - <!-- Row 2 --> - <TableRow - android:layout_width="match_parent" - android:layout_height="wrap_content"> + <ImageView + android:id="@+id/headerLogo" + android:layout_width="200dp" + android:layout_height="60dp" + android:layout_marginTop="6dp" + android:contentDescription="@string/NGI_TALER_logo" + android:src="@drawable/ngi_taler_logo" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/backButton" /> + + <com.google.android.material.card.MaterialCardView + android:id="@+id/statusCard" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + app:cardUseCompatPadding="true" + app:cardCornerRadius="12dp" + app:cardElevation="4dp" + app:layout_constraintTop_toBottomOf="@id/headerLogo" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"> <TextView + android:id="@+id/sigStatus" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="8dp" - android:textStyle="bold" - android:text="Tax Id: " - android:textSize="18sp" /> + android:padding="24dp" + android:textAlignment="center" + android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6" + android:textColor="@color/colorOnPrimary" + tools:text="Donation statement signature is valid!" /> + </com.google.android.material.card.MaterialCardView> - <TextView - android:id="@+id/taxid" + <LinearLayout + android:id="@+id/summaryContainer" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:orientation="vertical" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/statusCard"> + + <com.google.android.material.card.MaterialCardView + android:id="@+id/hostCard" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="8dp" - android:textSize="18sp" /> - </TableRow> + android:layout_margin="2dp" + app:cardCornerRadius="12dp" + app:cardElevation="2dp" + app:cardUseCompatPadding="true"> - <!-- Row 3 --> - <TableRow - android:layout_width="match_parent" - android:layout_height="wrap_content"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="16dp"> - <TextView - android:layout_width="wrap_content" + <TextView + android:id="@+id/hostLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/label_host" + android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" + android:textColor="@color/text_secondary" + android:textStyle="bold" /> + + <TextView + android:id="@+id/hostValue" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" + android:textColor="@color/text_primary" + android:textIsSelectable="true" + tools:text="example.com" /> + + </LinearLayout> + </com.google.android.material.card.MaterialCardView> + + <com.google.android.material.card.MaterialCardView + android:id="@+id/taxCard" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="8dp" - android:textStyle="bold" - android:text="Total Amount: " - android:textSize="18sp" /> + android:layout_margin="2dp" + app:cardCornerRadius="12dp" + app:cardElevation="2dp" + app:cardUseCompatPadding="true"> - <TextView - android:id="@+id/total" + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="16dp"> + + <TextView + android:id="@+id/taxLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/label_tax_id" + android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" + android:textColor="@color/text_secondary" + android:textStyle="bold" /> + + <TextView + android:id="@+id/taxValue" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" + android:textColor="@color/text_primary" + android:textIsSelectable="true" + tools:text="7560001010000" /> + + </LinearLayout> + </com.google.android.material.card.MaterialCardView> + + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="8dp" - android:textSize="18sp" /> - </TableRow> - -</TableLayout> - -<TextView - android:id="@+id/sigStatus" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textSize="20sp" - android:layout_centerInParent="true"/> -</RelativeLayout> -\ No newline at end of file + android:baselineAligned="false" + android:orientation="horizontal"> + + <com.google.android.material.card.MaterialCardView + android:id="@+id/amountCard" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="2dp" + android:layout_weight="1" + app:cardCornerRadius="12dp" + app:cardElevation="2dp" + app:cardUseCompatPadding="true"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="16dp"> + + <TextView + android:id="@+id/amountLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/label_amount" + android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" + android:textColor="@color/text_secondary" + android:textStyle="bold" /> + + <TextView + android:id="@+id/amountValue" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" + android:textColor="@color/text_primary" + android:textIsSelectable="true" + tools:text="EUR 15" /> + + </LinearLayout> + </com.google.android.material.card.MaterialCardView> + + <com.google.android.material.card.MaterialCardView + android:id="@+id/yearCard" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:layout_weight="1" + app:cardCornerRadius="12dp" + app:cardElevation="2dp" + app:cardUseCompatPadding="true"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="16dp"> + + <TextView + android:id="@+id/yearLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/label_year" + android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" + android:textColor="@color/text_secondary" + android:textStyle="bold" /> + + <TextView + android:id="@+id/yearValue" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" + android:textColor="@color/text_primary" + android:textIsSelectable="true" + tools:text="2024" /> + + </LinearLayout> + </com.google.android.material.card.MaterialCardView> + + </LinearLayout> + + </LinearLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/donau-verificator/src/main/res/menu/main_menu.xml b/donau-verificator/src/main/res/menu/main_menu.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/action_settings" + android:title="@string/menu_settings" + android:orderInCategory="100" + app:showAsAction="never" /> +</menu> diff --git a/donau-verificator/src/main/res/values-night/colors.xml b/donau-verificator/src/main/res/values-night/colors.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="colorPrimary">#B4C5FF</color> + <color name="colorPrimaryVariant">#0042B3</color> + <color name="colorOnPrimary">#002A78</color> + <color name="colorSecondary">#A4C9FF</color> + <color name="colorSecondaryVariant">#72A3E5</color> + <color name="colorOnSecondary">#00315D</color> + <color name="colorBackground">#11131A</color> + <color name="black">#FF000000</color> + <color name="white">#FFFFFFFF</color> + <color name="red">#FFB4AA</color> + <color name="green">#81C784</color> + <color name="validation_surface_error">#5B1A1F</color> + <color name="validation_surface_success">#173227</color> + <color name="validation_surface_info">#0F274F</color> + <color name="validation_surface_neutral">#1E2029</color> + <color name="text_primary">#E2E2EB</color> + <color name="text_secondary">#C6C6CB</color> +</resources> diff --git a/donau-verificator/src/main/res/values-night/themes.xml b/donau-verificator/src/main/res/values-night/themes.xml @@ -1,16 +1,26 @@ <resources xmlns:tools="http://schemas.android.com/tools"> <!-- Base application theme. --> - <style name="Theme.Verifaction" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> + <style name="Theme.Verifaction" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <!-- Primary brand color. --> - <item name="colorPrimary">@color/purple_200</item> - <item name="colorPrimaryVariant">@color/purple_700</item> - <item name="colorOnPrimary">@color/black</item> + <item name="colorPrimary">@color/colorPrimary</item> + <item name="colorPrimaryVariant">@color/colorPrimaryVariant</item> + <item name="colorOnPrimary">@color/colorOnPrimary</item> <!-- Secondary brand color. --> - <item name="colorSecondary">@color/teal_200</item> - <item name="colorSecondaryVariant">@color/teal_200</item> - <item name="colorOnSecondary">@color/black</item> - <!-- Status bar color. --> - <item name="android:statusBarColor">?attr/colorPrimaryVariant</item> + <item name="colorSecondary">@color/colorSecondary</item> + <item name="colorSecondaryVariant">@color/colorSecondaryVariant</item> + <item name="colorOnSecondary">@color/colorOnSecondary</item> + <!-- System bar colors. --> + <item name="android:statusBarColor">@color/colorPrimary</item> + <item name="android:navigationBarColor">@color/colorBackground</item> + <item name="colorControlActivated">@color/colorPrimary</item> + <item name="android:colorControlActivated">@color/colorPrimary</item> + <item name="colorControlHighlight">@color/colorPrimaryVariant</item> + <item name="android:colorControlHighlight">@color/colorPrimaryVariant</item> + <item name="colorSwitchThumbNormal">@color/colorPrimary</item> + <item name="android:windowBackground">@color/colorBackground</item> + <item name="colorOnBackground">@color/text_primary</item> + <item name="android:textColorPrimary">@color/text_primary</item> + <item name="android:textColorSecondary">@color/text_secondary</item> <!-- Customize your theme here. --> </style> -</resources> -\ No newline at end of file +</resources> diff --git a/donau-verificator/src/main/res/values/colors.xml b/donau-verificator/src/main/res/values/colors.xml @@ -1,18 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <color name="purple_200">#FFBB86FC</color> - <color name="purple_500">#FF6200EE</color> - <color name="purple_700">#FF3700B3</color> - <color name="teal_200">#FF03DAC5</color> - <color name="teal_700">#FF018786</color> + <color name="colorPrimary">#0042B3</color> + <color name="colorPrimaryVariant">#00379C</color> + <color name="colorOnPrimary">#FFFFFFFF</color> + <color name="colorSecondary">#586A88</color> + <color name="colorSecondaryVariant">#445670</color> + <color name="colorOnSecondary">#FFFFFFFF</color> + <color name="colorBackground">#FDFDFF</color> <color name="black">#FF000000</color> <color name="white">#FFFFFFFF</color> - <color name="red">#870C0C</color> - <color name="green">#045F06</color> - <color name="validation_surface_error">#FFF3E0</color> - <color name="validation_surface_success">#E8F5E9</color> - <color name="validation_surface_info">#E3F2FD</color> - <color name="validation_surface_neutral">#F5F5F5</color> - <color name="text_primary">#212121</color> - <color name="text_secondary">#616161</color> + <color name="red">#BB1D24</color> + <color name="green">#388E3C</color> + <color name="validation_surface_error">#F9DEDC</color> + <color name="validation_surface_success">#E6F4EA</color> + <color name="validation_surface_info">#D3DEFF</color> + <color name="validation_surface_neutral">#F5F7FC</color> + <color name="text_primary">#1A1C1F</color> + <color name="text_secondary">#45474F</color> </resources> diff --git a/donau-verificator/src/main/res/values/strings.xml b/donau-verificator/src/main/res/values/strings.xml @@ -8,4 +8,24 @@ <string name="valid_signature">Donation statement signature is valid!</string> <string name="invalid_scheme">Invalid scheme!</string> <string name="content_description_validation_icon">Validation status indicator</string> + <string name="menu_settings">Settings</string> + <string name="pref_developer_mode_title">Developer mode</string> + <string name="pref_developer_mode_summary_on">Developer features enabled; insecure Donau URIs allowed.</string> + <string name="pref_developer_mode_summary_off">Developer features disabled.</string> + <string name="status_insecure_http_disabled">Insecure Donau URI not allowed. Enable developer mode to proceed.</string> + <string name="status_insecure_http_unsupported">This build does not support insecure Donau URIs.</string> + <string name="status_key_download_failed">Unable to fetch Donau signing keys.</string> + <string name="status_key_not_found">Signing key for this donation statement not found.</string> + <string name="label_host">Host</string> + <string name="label_year">Year</string> + <string name="label_tax_id">Tax ID</string> + <string name="label_amount">Total amount</string> + <string name="action_back">Back</string> + <string name="action_signature_info">Signature info</string> + <string name="signature_info_title">Signature details</string> + <string name="signature_info_message">Salt: %1$s +Signature: %2$s +Public key: %3$s</string> + <string name="value_unknown">Unknown</string> + <string name="title_donau_verifier">Donau Verifier</string> </resources> diff --git a/donau-verificator/src/main/res/values/themes.xml b/donau-verificator/src/main/res/values/themes.xml @@ -1,16 +1,26 @@ <resources xmlns:tools="http://schemas.android.com/tools"> <!-- Base application theme. --> - <style name="Theme.Verifaction" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> + <style name="Theme.Verifaction" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <!-- Primary brand color. --> - <item name="colorPrimary">@color/purple_500</item> - <item name="colorPrimaryVariant">@color/purple_700</item> - <item name="colorOnPrimary">@color/white</item> + <item name="colorPrimary">@color/colorPrimary</item> + <item name="colorPrimaryVariant">@color/colorPrimaryVariant</item> + <item name="colorOnPrimary">@color/colorOnPrimary</item> <!-- Secondary brand color. --> - <item name="colorSecondary">@color/teal_200</item> - <item name="colorSecondaryVariant">@color/teal_700</item> - <item name="colorOnSecondary">@color/black</item> - <!-- Status bar color. --> - <item name="android:statusBarColor">?attr/colorPrimaryVariant</item> + <item name="colorSecondary">@color/colorSecondary</item> + <item name="colorSecondaryVariant">@color/colorSecondaryVariant</item> + <item name="colorOnSecondary">@color/colorOnSecondary</item> + <!-- System bar colors. --> + <item name="android:statusBarColor">@color/colorPrimary</item> + <item name="android:navigationBarColor">@color/colorBackground</item> + <item name="colorControlActivated">@color/colorPrimary</item> + <item name="android:colorControlActivated">@color/colorPrimary</item> + <item name="colorControlHighlight">@color/colorPrimaryVariant</item> + <item name="android:colorControlHighlight">@color/colorPrimaryVariant</item> + <item name="colorSwitchThumbNormal">@color/colorPrimary</item> + <item name="android:windowBackground">@color/colorBackground</item> + <item name="colorOnBackground">@color/text_primary</item> + <item name="android:textColorPrimary">@color/text_primary</item> + <item name="android:textColorSecondary">@color/text_secondary</item> <!-- Customize your theme here. --> </style> -</resources> -\ No newline at end of file +</resources> diff --git a/donau-verificator/src/main/res/xml/preferences.xml b/donau-verificator/src/main/res/xml/preferences.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <androidx.preference.SwitchPreferenceCompat + android:key="pref_developer_mode" + android:title="@string/pref_developer_mode_title" + android:summaryOn="@string/pref_developer_mode_summary_on" + android:summaryOff="@string/pref_developer_mode_summary_off" + android:defaultValue="false" + android:icon="@drawable/ic_warning" + app:iconSpaceReserved="true" + app:isPreferenceVisible="true" /> +</PreferenceScreen>