taler-android

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

MainActivity.java (12181B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2024 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 
     13   You should have received a copy of the GNU General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 package net.taler.donauverificator;
     18 
     19 import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
     20 
     21 import android.Manifest;
     22 import android.app.AlertDialog;
     23 import android.content.DialogInterface;
     24 import android.content.Intent;
     25 import android.content.pm.PackageManager;
     26 import android.content.SharedPreferences;
     27 import android.os.Bundle;
     28 import android.provider.Settings;
     29 import android.net.Uri;
     30 import android.view.Menu;
     31 import android.view.MenuInflater;
     32 import android.view.MenuItem;
     33 import android.widget.Toast;
     34 
     35 import com.budiyev.android.codescanner.CodeScanner;
     36 import com.budiyev.android.codescanner.CodeScannerView;
     37 import com.budiyev.android.codescanner.DecodeCallback;
     38 import com.google.zxing.Result;
     39 
     40 import androidx.annotation.NonNull;
     41 import androidx.appcompat.app.AppCompatActivity;
     42 import androidx.core.app.ActivityCompat;
     43 import androidx.core.content.ContextCompat;
     44 import androidx.preference.PreferenceManager;
     45 
     46 import net.taler.donauverificator.databinding.ActivityMainBinding;
     47 
     48 public class MainActivity extends AppCompatActivity {
     49 //    private static final String DEBUG_DONATION_STATEMENT =
     50 //            "donau://example.com/megacharity/1234/2025/7560001010000/1234?total=EUR:15&sig=ED25519:H9PM3BW3P8MEKB34GZ0G1F7JSNVX7B8AHXRFFMS37QZM7TXZ5MWPXTEDZZGN1QRB1AFPKNCFXJB39NJHP3BAFGCZSCXHEYPHA1YJY28&pub=K641W1CZM7DRSV184M8CPM3Z8MZRBYYJMNYMJK70FTYJHBPX21J0";
     51 //    private static final String DEBUG_DONATION_STATEMENT =
     52 //            "donau://donau.test.taler.net/2025/123%2F456%2F789/AWNFDRFT0WX45W4Y32A9DJA03S1EF66GFQZ9EV5EF9JTHWZ37WR0?total=TESTKUDOS:1&sig=ED25519:B14WGS43FFPEB8JMSR6W1H8M6KH9AV33JFH376R6PM2MNH4GR24FP1C93C4ZPDG21W5WY4SASZQ4CRS427F4WJZJFZMQ5Y4HZNXGY30";
     53     private static final int REQ_CAMERA_PERMISSION = 100;
     54     private static final int REQ_APP_SETTINGS = 101;
     55     private static final String PREFS_PERMISSIONS = "permissions_prefs";
     56     private static final String KEY_CAMERA_DENIALS = "camera_denials";
     57     private ActivityMainBinding binding;
     58     private CodeScanner mCodeScanner;
     59 
     60 
     61     @Override
     62     protected void onCreate(Bundle savedInstanceState) {
     63         super.onCreate(savedInstanceState);
     64 
     65         binding = ActivityMainBinding.inflate(getLayoutInflater());
     66         setContentView(binding.getRoot());
     67 
     68         // Check if CAMERA permission is granted; otherwise start permission flow
     69         if (!hasCameraPermission()) {
     70             askForCameraPermission();
     71         }
     72         CodeScannerView scannerView = binding.scannerView;
     73         mCodeScanner = new CodeScanner(this, scannerView);
     74         mCodeScanner.setDecodeCallback(new DecodeCallback() {
     75             @Override
     76             public void onDecoded(@NonNull final Result result) {
     77                 runOnUiThread(() -> handleScannedText(result.getText()));
     78             }
     79         });
     80 
     81         //temporary for debugging (valid donation statement from sample QR code)
     82         //sendRequestDialog(DEBUG_DONATION_STATEMENT);
     83 
     84         binding.settingsButton.setOnClickListener(v -> {
     85             Intent intent = new Intent(this, SettingsActivity.class);
     86             startActivity(intent);
     87         });
     88 
     89     }
     90 
     91     @Override
     92     public void onResume() {
     93         super.onResume();
     94         if (hasCameraPermission()) {
     95             mCodeScanner.startPreview();
     96         }
     97     }
     98 
     99     @Override
    100     public void onPause() {
    101         mCodeScanner.releaseResources();
    102         super.onPause();
    103     }
    104 
    105     @Override
    106     public void onDestroy() {
    107         super.onDestroy();
    108         binding = null;
    109     }
    110     /**
    111      * Asks user if app should proceed
    112      * @param qrstring
    113      */
    114     private void sendRequestDialog(String qrstring) {
    115         DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
    116             @Override
    117             public void onClick(DialogInterface dialog, int which) {
    118                 switch (which) {
    119                     case DialogInterface.BUTTON_POSITIVE:
    120                         //Yes button clicked
    121                         startResults(qrstring);
    122                         break;
    123 
    124                     case DialogInterface.BUTTON_NEGATIVE:
    125                         //No button clicked
    126                         if (mCodeScanner != null) {
    127                             mCodeScanner.startPreview();
    128                         }
    129                         break;
    130                     default:
    131                         throw new IllegalStateException("Unexpected value: " + which);
    132                 }
    133             }
    134         };
    135         AlertDialog.Builder builder = new AlertDialog.Builder(this);
    136         builder.setMessage("QR scanned: '" + qrstring + "'\nWould you like to proceed?").setPositiveButton("Yes", dialogClickListener)
    137                 .setNegativeButton("No", dialogClickListener).show();
    138     }
    139 
    140     /**
    141      * Handle scanned QR content: if it's a valid Donau URI, proceed directly to Results.
    142      * Otherwise, show a brief message and resume scanning.
    143      */
    144     private void handleScannedText(String qrString) {
    145         if (isValidDonauUri(qrString)) {
    146             boolean autoOpen = PreferenceManager.getDefaultSharedPreferences(this)
    147                     .getBoolean(SettingsActivity.KEY_AUTO_OPEN_DONAU, true);
    148             if (autoOpen) {
    149                 startResults(qrString);
    150             } else {
    151                 sendRequestDialog(qrString);
    152             }
    153             return;
    154         }
    155         Toast.makeText(this, R.string.scan_not_donau_link, Toast.LENGTH_SHORT).show();
    156         if (mCodeScanner != null) {
    157             mCodeScanner.startPreview();
    158         }
    159     }
    160 
    161     private void startResults(String qrString) {
    162         Intent intent = new Intent(getApplicationContext(), Results.class);
    163         intent.putExtra("QR-String", qrString);
    164         startActivity(intent);
    165     }
    166 
    167     private boolean isValidDonauUri(String raw) {
    168         if (raw == null) return false;
    169         Uri uri;
    170         try {
    171             uri = Uri.parse(raw);
    172         } catch (Exception e) {
    173             return false;
    174         }
    175         if (uri == null) return false;
    176         String scheme = uri.getScheme();
    177         if (scheme == null) return false;
    178         String lowered = scheme.toLowerCase();
    179         if (!("donau".equals(lowered) || "donau+http".equals(lowered))) {
    180             return false;
    181         }
    182         if (uri.getHost() == null || uri.getHost().trim().isEmpty()) {
    183             return false;
    184         }
    185         java.util.List<String> segments = uri.getPathSegments();
    186         if (segments == null || segments.size() < 3) {
    187             return false;
    188         }
    189         String year = segments.get(segments.size() - 3);
    190         if (!isFourDigitYear(year)) {
    191             return false;
    192         }
    193         String taxId = segments.get(segments.size() - 2);
    194         String salt = segments.get(segments.size() - 1);
    195         if (taxId == null || taxId.isEmpty()) return false;
    196         if (salt == null || salt.trim().isEmpty()) return false;
    197         return true;
    198     }
    199 
    200     private boolean isFourDigitYear(String value) {
    201         if (value == null || value.length() != 4) return false;
    202         for (int i = 0; i < value.length(); i++) {
    203             if (!Character.isDigit(value.charAt(i))) return false;
    204         }
    205         return true;
    206     }
    207 
    208     private boolean hasCameraPermission() {
    209         return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;
    210     }
    211 
    212     /**
    213      * Request camera permission with rationale and fallback to app settings after repeated denials.
    214      */
    215     private void askForCameraPermission() {
    216         if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
    217             showPermissionRationaleDialog();
    218         } else {
    219             requestCameraPermission();
    220         }
    221     }
    222 
    223     private void requestCameraPermission() {
    224         ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQ_CAMERA_PERMISSION);
    225     }
    226 
    227     private void showPermissionRationaleDialog() {
    228         new AlertDialog.Builder(this)
    229                 .setTitle(R.string.permission_camera_title)
    230                 .setMessage(getString(R.string.permission_camera_message))
    231                 .setPositiveButton(R.string.permission_continue, (d, w) -> requestCameraPermission())
    232                 .setNegativeButton(android.R.string.cancel, null)
    233                 .show();
    234     }
    235 
    236     private void showGoToSettingsDialog() {
    237         new AlertDialog.Builder(this)
    238                 .setTitle(R.string.permission_camera_denied_title)
    239                 .setMessage(getString(R.string.permission_camera_denied_message))
    240                 .setPositiveButton(R.string.permission_open_settings, (d, w) -> openAppSettings())
    241                 .setNegativeButton(android.R.string.cancel, null)
    242                 .setIcon(android.R.drawable.ic_dialog_alert)
    243                 .show();
    244     }
    245 
    246     private void openAppSettings() {
    247         Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
    248         Uri uri = Uri.fromParts("package", getPackageName(), null);
    249         intent.setData(uri);
    250         startActivity(intent);
    251     }
    252 
    253     private void incrementCameraDenialCount() {
    254         SharedPreferences prefs = getSharedPreferences(PREFS_PERMISSIONS, MODE_PRIVATE);
    255         int count = prefs.getInt(KEY_CAMERA_DENIALS, 0);
    256         prefs.edit().putInt(KEY_CAMERA_DENIALS, count + 1).apply();
    257     }
    258 
    259     private int getCameraDenialCount() {
    260         SharedPreferences prefs = getSharedPreferences(PREFS_PERMISSIONS, MODE_PRIVATE);
    261         return prefs.getInt(KEY_CAMERA_DENIALS, 0);
    262     }
    263 
    264     private void resetCameraDenialCount() {
    265         SharedPreferences prefs = getSharedPreferences(PREFS_PERMISSIONS, MODE_PRIVATE);
    266         prefs.edit().remove(KEY_CAMERA_DENIALS).apply();
    267     }
    268 
    269     @Override
    270     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    271         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    272         if (requestCode == REQ_CAMERA_PERMISSION) {
    273             boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
    274             if (granted) {
    275                 resetCameraDenialCount();
    276                 Toast.makeText(this, R.string.permission_granted, Toast.LENGTH_SHORT).show();
    277                 if (mCodeScanner != null) {
    278                     mCodeScanner.startPreview();
    279                 }
    280             } else {
    281                 incrementCameraDenialCount();
    282                 boolean showRationale = ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA);
    283                 if (!showRationale || getCameraDenialCount() >= 2) {
    284                     // User has denied twice or selected "Don't ask again" — guide to Settings
    285                     showGoToSettingsDialog();
    286                 } else {
    287                     // Show rationale again for clarity
    288                     showPermissionRationaleDialog();
    289                 }
    290             }
    291         }
    292     }
    293 
    294     @Override
    295     public boolean onCreateOptionsMenu(Menu menu) {
    296         MenuInflater inflater = getMenuInflater();
    297         inflater.inflate(R.menu.main_menu, menu);
    298         return true;
    299     }
    300 
    301     @Override
    302     public boolean onOptionsItemSelected(@NonNull MenuItem item) {
    303         if (item.getItemId() == R.id.action_settings) {
    304             Intent intent = new Intent(this, SettingsActivity.class);
    305             startActivity(intent);
    306             return true;
    307         }
    308         return super.onOptionsItemSelected(item);
    309     }
    310 
    311 }