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 }