UsageExamples.kt (18640B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2024 Taler Systems S.A. 4 * 5 * GNU 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 * GNU 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 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 package net.taler.anastasistestdriverapp.components 18 19 import android.app.PendingIntent 20 import android.content.ClipData 21 import android.content.Intent 22 import android.net.Uri 23 import androidx.activity.compose.rememberLauncherForActivityResult 24 import androidx.activity.result.contract.ActivityResultContracts 25 import androidx.compose.foundation.layout.Arrangement 26 import androidx.compose.foundation.layout.Column 27 import androidx.compose.foundation.layout.fillMaxSize 28 import androidx.compose.foundation.layout.fillMaxWidth 29 import androidx.compose.foundation.layout.padding 30 import androidx.compose.foundation.text.selection.SelectionContainer 31 import androidx.compose.material3.Button 32 import androidx.compose.material3.MaterialTheme 33 import androidx.compose.material3.OutlinedButton 34 import androidx.compose.material3.Text 35 import androidx.compose.material3.TextField 36 import androidx.compose.runtime.Composable 37 import androidx.compose.runtime.derivedStateOf 38 import androidx.compose.runtime.getValue 39 import androidx.compose.runtime.mutableStateOf 40 import androidx.compose.runtime.remember 41 import androidx.compose.runtime.setValue 42 import androidx.compose.ui.Alignment 43 import androidx.compose.ui.Modifier 44 import androidx.compose.ui.graphics.Color 45 import androidx.compose.ui.platform.LocalContext 46 import androidx.compose.ui.text.TextStyle 47 import androidx.compose.ui.text.font.FontWeight 48 import androidx.compose.ui.text.style.TextAlign 49 import androidx.compose.ui.unit.dp 50 import androidx.compose.ui.unit.sp 51 import androidx.core.content.FileProvider 52 import net.taler.anastasistestdriverapp.utils.Utils 53 import java.io.File 54 55 @Composable 56 fun TextExample(modifier: Modifier = Modifier) { 57 var appId by remember { 58 mutableStateOf("net.taler.anastasistestdriverapp") 59 } 60 61 var appName by remember { 62 mutableStateOf("Gmail App") 63 } 64 65 var secretName by remember { 66 mutableStateOf("Account password") 67 } 68 69 var secret by remember { 70 mutableStateOf("123456") 71 } 72 73 var expirationDateTime by remember { 74 mutableStateOf("2030-01-01T08:30:15") 75 } 76 77 val uri by remember { 78 derivedStateOf { 79 val uriBuilder = Uri.Builder() 80 .scheme("anastasis") 81 .authority("backup") 82 .appendEncodedPath(appId) // appId is actually not encoded and we don't want to. 83 .appendPath(secretName) 84 .appendQueryParameter( 85 "secret", 86 Utils.encodeSecretToBase32(secret.toByteArray()) 87 ) 88 if (appName.isNotBlank()) { 89 uriBuilder.appendQueryParameter("app_name", appName) 90 } 91 if (expirationDateTime.isNotBlank()) { 92 uriBuilder.appendQueryParameter("expiration_datetime", expirationDateTime) 93 } 94 uriBuilder.build() 95 } 96 } 97 98 var backupResult by remember { mutableStateOf("") } 99 val anastasisLauncher = rememberLauncherForActivityResult( 100 contract = ActivityResultContracts.StartActivityForResult() 101 ) { activityResult -> 102 val data = activityResult.data?.data?.pathSegments 103 when (activityResult.resultCode) { 104 0 -> backupResult = "User aborted the process" 105 1 -> backupResult = "Invalid input: ${data!![1]}" 106 2 -> backupResult = "Internal error occurred" 107 100 -> backupResult = "Backup succeeded: \n" + 108 "(secret name: ${data!![1]}) \n" + 109 "(expiration date: ${data[2]})" 110 } 111 } 112 113 Column( 114 horizontalAlignment = Alignment.CenterHorizontally, 115 verticalArrangement = Arrangement.spacedBy(16.dp), 116 modifier = modifier.fillMaxSize() 117 ) { 118 Text( 119 "Text Example", 120 style = MaterialTheme.typography.headlineMedium, 121 fontWeight = FontWeight.Bold, 122 color = Color.Blue, 123 modifier = Modifier.padding(16.dp), 124 ) 125 126 TextField( 127 label = { 128 Text(text = "App ID") 129 }, 130 value = appId, 131 onValueChange = { appId = it }, 132 textStyle = TextStyle(fontSize = 16.sp), 133 modifier = Modifier.fillMaxWidth(), 134 ) 135 136 TextField( 137 label = { 138 Text(text = "App Name (Optional)") 139 }, 140 value = appName, 141 onValueChange = { appName = it }, 142 textStyle = TextStyle(fontSize = 16.sp), 143 modifier = Modifier.fillMaxWidth(), 144 ) 145 146 TextField( 147 label = { 148 Text(text = "Secret Name") 149 }, 150 value = secretName, 151 onValueChange = { secretName = it }, 152 textStyle = TextStyle(fontSize = 16.sp), 153 modifier = Modifier.fillMaxWidth(), 154 ) 155 156 TextField( 157 label = { 158 Text(text = "Secret") 159 }, 160 value = secret, 161 onValueChange = { secret = it }, 162 textStyle = TextStyle(fontSize = 16.sp), 163 modifier = Modifier.fillMaxWidth(), 164 ) 165 166 TextField( 167 label = { 168 Text(text = "Expiration date-time (Optional)") 169 }, 170 value = expirationDateTime, 171 onValueChange = { expirationDateTime = it }, 172 textStyle = TextStyle(fontSize = 16.sp), 173 modifier = Modifier.fillMaxWidth(), 174 ) 175 176 Text( 177 "URI:", 178 textAlign = TextAlign.Center, 179 fontWeight = FontWeight.Bold, 180 modifier = Modifier.padding(horizontal = 8.dp) 181 ) 182 Text( 183 "$uri", 184 textAlign = TextAlign.Center, 185 modifier = Modifier.padding(horizontal = 8.dp) 186 ) 187 188 val context = LocalContext.current 189 Button( 190 onClick = { 191 val intent = Intent("anastasis.intent.action.backup").run { 192 data = uri 193 val pendingIntent = PendingIntent.getActivity( 194 context, 195 0, 196 Intent(), 197 PendingIntent.FLAG_IMMUTABLE 198 ) 199 putExtra("pending_intent", pendingIntent) 200 Intent.createChooser(this, "") 201 } 202 anastasisLauncher.launch(intent) 203 }, 204 ) { 205 Text("Back up using Anastasis") 206 } 207 208 Text( 209 text = backupResult, 210 fontSize = 16.sp, 211 textAlign = TextAlign.Center, 212 modifier = Modifier 213 .padding(horizontal = 16.dp) 214 .fillMaxWidth() 215 ) 216 } 217 } 218 219 @Composable 220 fun ExternalFileExample(modifier: Modifier = Modifier) { 221 var secretFileContentUri by remember { mutableStateOf<Uri?>(null) } 222 val uri by remember { 223 derivedStateOf { 224 Uri.Builder() 225 .scheme("anastasis") 226 .authority("backup") 227 .appendEncodedPath("net.taler.anastasistestdriverapp") // appId is actually not encoded and we don't want to. 228 .appendPath("Account Credentials") 229 .build() 230 } 231 } 232 233 var backupResult by remember { mutableStateOf("") } 234 val anastasisLauncher = rememberLauncherForActivityResult( 235 contract = ActivityResultContracts.StartActivityForResult() 236 ) { activityResult -> 237 val data = activityResult.data?.data?.pathSegments 238 when (activityResult.resultCode) { 239 0 -> backupResult = "User aborted the process" 240 1 -> backupResult = "Invalid input: ${data!![1]}" 241 2 -> backupResult = "Internal error occurred" 242 100 -> backupResult = "Backup succeeded: \n" + 243 "(secret name: ${data!![1]}) \n" + 244 "(expiration date: ${data[2]})" 245 } 246 } 247 val filePickerLauncher = rememberLauncherForActivityResult( 248 contract = ActivityResultContracts.GetContent(), 249 onResult = { 250 it?.let { uri -> secretFileContentUri = uri } 251 } 252 ) 253 254 Column( 255 horizontalAlignment = Alignment.CenterHorizontally, 256 verticalArrangement = Arrangement.spacedBy(16.dp), 257 modifier = modifier.fillMaxWidth() 258 ) { 259 260 Text( 261 "External File Example", 262 style = MaterialTheme.typography.headlineMedium, 263 fontWeight = FontWeight.Bold, 264 color = Color.Blue, 265 modifier = Modifier.padding(horizontal = 16.dp), 266 ) 267 268 OutlinedButton( 269 onClick = { filePickerLauncher.launch("*/*") }, 270 shape = MaterialTheme.shapes.small, 271 modifier = Modifier.padding(top = 16.dp) 272 ) { 273 Text("Pick a file") 274 } 275 276 SelectionContainer { 277 Text( 278 text = secretFileContentUri?.toString() ?: "No selected file", 279 fontSize = 16.sp, 280 textAlign = TextAlign.Center, 281 modifier = Modifier 282 .padding(horizontal = 16.dp) 283 .fillMaxWidth() 284 ) 285 } 286 287 val context = LocalContext.current 288 Button( 289 onClick = { 290 val intent = Intent("anastasis.intent.action.backup").run { 291 data = uri 292 clipData = ClipData.newRawUri("", secretFileContentUri) 293 addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 294 val pendingIntent = PendingIntent.getActivity( 295 context, 296 0, 297 Intent(), 298 PendingIntent.FLAG_IMMUTABLE 299 ) 300 putExtra("pending_intent", pendingIntent) 301 Intent.createChooser(this, "") 302 } 303 anastasisLauncher.launch(intent) 304 }, 305 ) { 306 Text("Back up using Anastasis") 307 } 308 309 Text( 310 text = backupResult, 311 fontSize = 16.sp, 312 textAlign = TextAlign.Center, 313 modifier = Modifier 314 .padding(horizontal = 16.dp) 315 .fillMaxWidth() 316 ) 317 } 318 } 319 320 @Composable 321 fun OwnFileExample(modifier: Modifier = Modifier) { 322 val context = LocalContext.current 323 val uri by remember { 324 derivedStateOf { 325 Uri.Builder() 326 .scheme("anastasis") 327 .authority("backup") 328 .appendEncodedPath("net.taler.anastasistestdriverapp") // appId is actually not encoded and we don't want to. 329 .appendPath("Account password") 330 .build() 331 } 332 } 333 334 var backupResult by remember { mutableStateOf("") } 335 val anastasisLauncher = rememberLauncherForActivityResult( 336 contract = ActivityResultContracts.StartActivityForResult() 337 ) { activityResult -> 338 val data = activityResult.data?.data?.pathSegments 339 when (activityResult.resultCode) { 340 0 -> backupResult = "User aborted the process" 341 1 -> backupResult = "Invalid input: ${data!![1]}" 342 2 -> backupResult = "Internal error occurred" 343 100 -> backupResult = "Backup succeeded: \n" + 344 "(secret name: ${data!![1]}) \n" + 345 "(expiration date: ${data!![2]})" 346 } 347 } 348 349 Column( 350 horizontalAlignment = Alignment.CenterHorizontally, 351 verticalArrangement = Arrangement.spacedBy(16.dp), 352 modifier = modifier.fillMaxWidth() 353 ) { 354 Text( 355 "Own File Example", 356 style = MaterialTheme.typography.headlineMedium, 357 fontWeight = FontWeight.Bold, 358 color = Color.Blue, 359 modifier = Modifier.padding(horizontal = 16.dp), 360 ) 361 362 val file = File(context.cacheDir, "example-secret-file.txt") 363 file.writeText( 364 "Long secret data included in the file.\n" + 365 "Email: TestAcc@gmail.com\n" + 366 "Password: 123456789" 367 ) 368 369 val secretFileContentUri = FileProvider.getUriForFile( 370 context, 371 "net.taler.anastasistestdriverapp.fileprovider", 372 file, 373 ) 374 375 SelectionContainer { 376 Text( 377 text = secretFileContentUri!!.toString(), 378 fontSize = 16.sp, 379 textAlign = TextAlign.Center, 380 modifier = Modifier 381 .padding(16.dp) 382 .fillMaxWidth() 383 ) 384 } 385 386 Button( 387 onClick = { 388 val intent = Intent("anastasis.intent.action.backup").run { 389 data = uri 390 clipData = ClipData.newRawUri("", secretFileContentUri) 391 addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 392 val pendingIntent = PendingIntent.getActivity( 393 context, 394 0, 395 Intent(), 396 PendingIntent.FLAG_IMMUTABLE 397 ) 398 putExtra("pending_intent", pendingIntent) 399 Intent.createChooser(this, "") 400 } 401 anastasisLauncher.launch(intent) 402 }, 403 ) { 404 Text("Back up using Anastasis") 405 } 406 407 Text( 408 text = backupResult, 409 fontSize = 16.sp, 410 textAlign = TextAlign.Center, 411 modifier = Modifier 412 .padding(horizontal = 16.dp) 413 .fillMaxWidth() 414 ) 415 } 416 } 417 418 @Composable 419 fun RecoveryExample(modifier: Modifier = Modifier) { 420 var appId by remember { 421 mutableStateOf("net.taler.anastasistestdriverapp") 422 } 423 424 var appName by remember { 425 mutableStateOf("Gmail App") 426 } 427 428 var secretName by remember { 429 mutableStateOf("Account password") 430 } 431 432 val uri by remember { 433 derivedStateOf { 434 val uriBuilder = Uri.Builder() 435 .scheme("anastasis") 436 .authority("recovery") 437 if (appId.isNotBlank()) { 438 uriBuilder.appendEncodedPath(appId) // appId is actually not encoded and we don't want to. 439 } 440 if (appId.isNotBlank() && secretName.isNotBlank()) { 441 uriBuilder.appendPath(secretName) 442 } 443 if (appName.isNotBlank()) { 444 uriBuilder.appendQueryParameter("app_name", appName) 445 } 446 uriBuilder.build() 447 } 448 } 449 450 var recoveryResult by remember { mutableStateOf("") } 451 val anastasisLauncher = rememberLauncherForActivityResult( 452 contract = ActivityResultContracts.StartActivityForResult() 453 ) { activityResult -> 454 val data = activityResult.data?.data?.pathSegments 455 when (activityResult.resultCode) { 456 0 -> recoveryResult = "User aborted the process" 457 1 -> recoveryResult = "Invalid input: ${data!![1]}" 458 2 -> recoveryResult = "Internal error occurred" 459 3 -> recoveryResult = "Secret not found." 460 101 -> { 461 val secretUri = activityResult.data?.clipData?.getItemAt(0)?.uri 462 recoveryResult = "Recovered secret Content URI: $secretUri\n" + 463 "File name: ${Utils.resolveDocFilename(secretUri!!)}\n" 464 } 465 } 466 } 467 468 Column( 469 horizontalAlignment = Alignment.CenterHorizontally, 470 verticalArrangement = Arrangement.spacedBy(16.dp), 471 modifier = modifier.fillMaxSize() 472 ) { 473 Text( 474 "Recovery Example", 475 style = MaterialTheme.typography.headlineMedium, 476 fontWeight = FontWeight.Bold, 477 color = Color.Blue, 478 modifier = Modifier.padding(16.dp), 479 ) 480 481 TextField( 482 label = { 483 Text(text = "App ID") 484 }, 485 value = appId, 486 onValueChange = { appId = it }, 487 textStyle = TextStyle(fontSize = 16.sp), 488 modifier = Modifier.fillMaxWidth(), 489 ) 490 491 TextField( 492 label = { 493 Text(text = "App Name (Optional)") 494 }, 495 value = appName, 496 onValueChange = { appName = it }, 497 textStyle = TextStyle(fontSize = 16.sp), 498 modifier = Modifier.fillMaxWidth(), 499 ) 500 501 TextField( 502 label = { 503 Text(text = "Secret Name") 504 }, 505 value = secretName, 506 onValueChange = { secretName = it }, 507 textStyle = TextStyle(fontSize = 16.sp), 508 modifier = Modifier.fillMaxWidth(), 509 ) 510 511 Text( 512 "URI:", 513 textAlign = TextAlign.Center, 514 fontWeight = FontWeight.Bold, 515 modifier = Modifier.padding(horizontal = 8.dp) 516 ) 517 Text( 518 "$uri", 519 textAlign = TextAlign.Center, 520 modifier = Modifier.padding(horizontal = 8.dp) 521 ) 522 523 val context = LocalContext.current 524 Button( 525 onClick = { 526 val intent = Intent("anastasis.intent.action.recovery").run { 527 data = uri 528 val pendingIntent = PendingIntent.getActivity( 529 context, 530 0, 531 Intent(), 532 PendingIntent.FLAG_IMMUTABLE 533 ) 534 putExtra("pending_intent", pendingIntent) 535 Intent.createChooser(this, "") 536 } 537 anastasisLauncher.launch(intent) 538 }, 539 ) { 540 Text("Recover using Anastasis") 541 } 542 543 Text( 544 text = recoveryResult, 545 fontSize = 16.sp, 546 textAlign = TextAlign.Center, 547 modifier = Modifier 548 .padding(horizontal = 16.dp) 549 .fillMaxWidth() 550 ) 551 } 552 }