diff --git a/app/build.gradle b/app/build.gradle index d49fc32..f9545d3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -143,11 +143,16 @@ dependencies { implementation Deps.AndroidX.LEGACY implementation Deps.AndroidX.PAGING implementation Deps.AndroidX.RECYCLER - implementation Deps.AndroidX.CameraX.CAMERA2 - implementation Deps.AndroidX.CameraX.CORE - implementation Deps.AndroidX.CameraX.LIFECYCLE - implementation Deps.AndroidX.CameraX.View.EXT - implementation Deps.AndroidX.CameraX.View.VIEW + + def camerax_version = "1.2.0-rc01" + implementation "androidx.camera:camera-core:${camerax_version}" + implementation "androidx.camera:camera-camera2:${camerax_version}" + implementation "androidx.camera:camera-lifecycle:${camerax_version}" + implementation "androidx.camera:camera-view:${camerax_version}" + + //WindowManager + implementation "androidx.window:window:1.1.0-alpha01" + implementation Deps.AndroidX.Lifecycle.LIFECYCLE_RUNTIME_KTX implementation Deps.AndroidX.Navigation.FRAGMENT_KTX implementation Deps.AndroidX.Navigation.UI_KTX diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index baaecf1..8a25bd8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ - + @@ -11,8 +11,12 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/ZcashTheme"> - + android:theme="@style/ZcashTheme" + tools:targetApi="31"> + diff --git a/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileFragment.kt index 5ee6ea8..c44a100 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileFragment.kt @@ -55,6 +55,38 @@ class ProfileFragment : BaseFragment() { tapped(PROFILE_RESCAN) onRescanWallet() } + + // Website + binding.websiteButton.setOnClickListener { + openWebsiteLink() + } + + // Telegram + binding.telegramButton.setOnClickListener { + openTelegramLink() + } + + // Matrix + binding.matrixButton.setOnClickListener { + openMatrixLink() + } + + // Mastodon + binding.mastodonButton.setOnClickListener { + openMastodonLink() + } + + // PeerTube + binding.peertubeButton.setOnClickListener { + openPeerTubeLink() + } + + // SilentDragon Gitea + binding.textBannerMessage.setOnClickListener { + openGiteaLink() + } + + // Add build version binding.textVersion.text = BuildConfig.VERSION_NAME /* @@ -67,33 +99,42 @@ class ProfileFragment : BaseFragment() { onViewDevLogs() true }*/ - - binding.iconProfile.setOnLongClickListener { - tapped(AWESOME_OPEN) - onEnterAwesomeMode() - true + } + + private fun openGiteaLink() { + getString(R.string.gitea_url).takeUnless { it.isBlank() }?.let { url -> + mainActivity?.onLaunchUrl(url) } - binding.textBannerMessage.setOnClickListener { - openPlayStoreLink() + } + + private fun openMastodonLink() { + getString(R.string.mastodon_url).takeUnless { it.isBlank() }?.let { url -> + mainActivity?.onLaunchUrl(url) } + } - if (viewModel.isEasterEggTriggered()) { - binding.iconProfile.setImageResource(R.drawable.ic_profile_zebra_02) + private fun openMatrixLink() { + getString(R.string.matrix_url).takeUnless { it.isBlank() }?.let { url -> + mainActivity?.onLaunchUrl(url) } } - private fun openPlayStoreLink() { - getString(R.string.play_store_url).takeUnless { it.isBlank() }?.let { url -> + private fun openPeerTubeLink() { + getString(R.string.peertube_url).takeUnless { it.isBlank() }?.let { url -> mainActivity?.onLaunchUrl(url) } } - private fun onEnterAwesomeMode() { - (context as? MainActivity)?.safeNavigate(R.id.action_nav_profile_to_nav_awesome) - ?: throw IllegalStateException( - "Cannot navigate from this activity. " + - "Expected MainActivity but found ${context?.javaClass?.simpleName}" - ) + private fun openTelegramLink() { + getString(R.string.telegram_url).takeUnless { it.isBlank() }?.let { url -> + mainActivity?.onLaunchUrl(url) + } + } + + private fun openWebsiteLink() { + getString(R.string.website_url).takeUnless { it.isBlank() }?.let { url -> + mainActivity?.onLaunchUrl(url) + } } override fun onResume() { diff --git a/app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt index 3f0448f..f853f5d 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt @@ -1,216 +1,341 @@ package cash.z.ecc.android.ui.scan -import android.content.Context -import android.content.pm.PackageManager import android.os.Bundle -import android.util.DisplayMetrics +import android.util.Log import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup +import android.widget.Toast import androidx.camera.core.* import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels +import androidx.window.layout.WindowInfoTracker +import androidx.window.layout.WindowMetricsCalculator import cash.z.ecc.android.R import cash.z.ecc.android.databinding.FragmentScanBinding import cash.z.ecc.android.ext.onClickNavBack -import cash.z.ecc.android.feedback.Report -import cash.z.ecc.android.feedback.Report.Tap.SCAN_BACK import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.send.SendViewModel -import cash.z.ecc.android.util.twig -import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.launch import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min -class ScanFragment : BaseFragment() { - override val screen = Report.Screen.SCAN +class ScanFragment : BaseFragment() { + private var _fragmentCameraBinding: FragmentScanBinding? = null + private val fragmentCameraBinding get() = _fragmentCameraBinding!! + private var displayId: Int = -1 + private var lensFacing: Int = CameraSelector.LENS_FACING_BACK + private var preview: Preview? = null + private var imageCapture: ImageCapture? = null + private var imageAnalyzer: ImageAnalysis? = null + private var camera: Camera? = null + private var cameraProvider: ProcessCameraProvider? = null private val viewModel: ScanViewModel by viewModels() - private val sendViewModel: SendViewModel by activityViewModels() + private lateinit var windowManager: WindowInfoTracker - private lateinit var cameraProviderFuture: ListenableFuture - - private var cameraExecutor: ExecutorService? = null + /** Blocking camera operations are performed using this executor */ + private lateinit var cameraExecutor: ExecutorService override fun inflate(inflater: LayoutInflater): FragmentScanBinding = FragmentScanBinding.inflate(inflater) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - if (cameraExecutor != null) cameraExecutor?.shutdown() + + // Initialize our background executor cameraExecutor = Executors.newSingleThreadExecutor() - binding.backButtonHitArea.onClickNavBack() { tapped(SCAN_BACK) } - } + //Initialize WindowManager to retrieve display metrics + windowManager = WindowInfoTracker.getOrCreate(view.context) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (!allPermissionsGranted()) getRuntimePermissions() - } + // Wait for the views to be properly laid out + fragmentCameraBinding.viewFinder.post { - override fun onAttach(context: Context) { - super.onAttach(context) - cameraProviderFuture = ProcessCameraProvider.getInstance(context) - cameraProviderFuture.addListener( - Runnable { - bindPreview(cameraProviderFuture.get()) - }, - ContextCompat.getMainExecutor(context) - ) + // Keep track of the display in which this view is attached + displayId = fragmentCameraBinding.viewFinder.display.displayId + + // Set up the camera and its use cases + setUpCamera() + } + + // Initialize back button + _fragmentCameraBinding?.backButtonHitArea?.onClickNavBack() } override fun onDestroyView() { + _fragmentCameraBinding = null super.onDestroyView() - cameraExecutor?.shutdown() - cameraExecutor = null + + // Shut down our background executor + cameraExecutor.shutdown() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _fragmentCameraBinding = FragmentScanBinding.inflate(inflater, container, false) + return fragmentCameraBinding.root + } + + /** Initialize CameraX, and prepare to bind the camera use cases */ + private fun setUpCamera() { + val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) + cameraProviderFuture.addListener(Runnable { + + // CameraProvider + cameraProvider = cameraProviderFuture.get() + + // Select lensFacing depending on the available cameras + lensFacing = when { + hasBackCamera() -> CameraSelector.LENS_FACING_BACK + hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT + else -> throw IllegalStateException("Back and front camera are unavailable") + } + + // Build and bind the camera use cases + bindCameraUseCases() + }, ContextCompat.getMainExecutor(requireContext())) } - private fun bindPreview(cameraProvider: ProcessCameraProvider) { - // Most of the code here is adapted from: https://github.com/android/camera-samples/blob/master/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic/fragments/CameraFragment.kt - // it's worth keeping tabs on that implementation because they keep making breaking changes to these APIs! + /** Declare and bind preview, capture and analysis use cases */ + private fun bindCameraUseCases() { // Get screen metrics used to setup camera for full screen resolution - val metrics = DisplayMetrics().also { binding.preview.display.getRealMetrics(it) } - val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels) - val rotation = binding.preview.display.rotation + /* + val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(requireActivity()).bounds + Log.d("SilentDragon", "Screen metrics: ${metrics.width()} x ${metrics.height()}") + val screenAspectRatio = aspectRatio(metrics.width(), metrics.height()) + */ + + // Hardcode to square for now otherwise scanning doesn't work + val screenAspectRatio = aspectRatio(1, 1) + Log.d("SilentDragon", "Preview aspect ratio: $screenAspectRatio") + + val rotation = fragmentCameraBinding.viewFinder.display.rotation + + // CameraProvider + val cameraProvider = cameraProvider + ?: throw IllegalStateException("Camera initialization failed.") - val preview = - Preview.Builder().setTargetName("Preview").setTargetAspectRatio(screenAspectRatio) - .setTargetRotation(rotation).build() + // CameraSelector + val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build() - val cameraSelector = CameraSelector.Builder() - .requireLensFacing(CameraSelector.LENS_FACING_BACK) + // Preview + preview = Preview.Builder() + // We request aspect ratio but no resolution + .setTargetAspectRatio(screenAspectRatio) + // Set initial target rotation + .setTargetRotation(rotation) + .build() + + // ImageCapture + imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + // We request aspect ratio but no resolution to match preview config, but letting + // CameraX optimize for whatever specific resolution best fits our use cases + .setTargetAspectRatio(screenAspectRatio) + // Set initial target rotation, we will have to call this again if rotation changes + // during the lifecycle of this use case + .setTargetRotation(rotation) .build() - val imageAnalysis = ImageAnalysis.Builder().setTargetAspectRatio(screenAspectRatio) + // ImageAnalysis + imageAnalyzer = ImageAnalysis.Builder() + // We request aspect ratio but no resolution + .setTargetAspectRatio(screenAspectRatio) + // Set initial target rotation, we will have to call this again if rotation changes + // during the lifecycle of this use case .setTargetRotation(rotation) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() - imageAnalysis.setAnalyzer( - cameraExecutor!!, - QrAnalyzer { q, i -> - onQrScanned(q, i) + + // The analyzer can then be assigned to the instance + .also { + it.setAnalyzer(cameraExecutor, + QrAnalyzer { q, i -> + onQrScanned(q, i) + } + ) } - ) // Must unbind the use-cases before rebinding them cameraProvider.unbindAll() + if (camera != null) { + // Must remove observers from the previous camera instance + removeCameraStateObservers(camera!!.cameraInfo) + } + try { - cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis) - preview.setSurfaceProvider(binding.preview.surfaceProvider) - } catch (t: Throwable) { - // TODO: consider bubbling this up to the user - mainActivity?.feedback?.report(t) - twig("Error while opening the camera: $t") + // A variable number of use-cases can be passed here - + // camera provides access to CameraControl & CameraInfo + camera = cameraProvider.bindToLifecycle( + this, cameraSelector, preview, imageCapture, imageAnalyzer) + + // Attach the viewfinder's surface provider to preview use case + preview?.setSurfaceProvider(fragmentCameraBinding.viewFinder.surfaceProvider) + observeCameraState(camera?.cameraInfo!!) + } catch (exc: Exception) { + Log.e(TAG, "Use case binding failed", exc) + } + } + + private fun removeCameraStateObservers(cameraInfo: CameraInfo) { + cameraInfo.cameraState.removeObservers(viewLifecycleOwner) + } + + private fun observeCameraState(cameraInfo: CameraInfo) { + cameraInfo.cameraState.observe(viewLifecycleOwner) { cameraState -> + run { + when (cameraState.type) { + CameraState.Type.PENDING_OPEN -> { + // Ask the user to close other camera apps + Toast.makeText(context, + "CameraState: Pending Open", + Toast.LENGTH_SHORT).show() + } + CameraState.Type.OPENING -> { + // Show the Camera UI + Toast.makeText(context, + "CameraState: Opening", + Toast.LENGTH_SHORT).show() + } + CameraState.Type.OPEN -> { + // Setup Camera resources and begin processing + Toast.makeText(context, + "CameraState: Open", + Toast.LENGTH_SHORT).show() + } + CameraState.Type.CLOSING -> { + // Close camera UI + Toast.makeText(context, + "CameraState: Closing", + Toast.LENGTH_SHORT).show() + } + CameraState.Type.CLOSED -> { + // Free camera resources + Toast.makeText(context, + "CameraState: Closed", + Toast.LENGTH_SHORT).show() + } + } + } + + cameraState.error?.let { error -> + when (error.code) { + // Open errors + CameraState.ERROR_STREAM_CONFIG -> { + // Make sure to setup the use cases properly + Toast.makeText(context, + "Stream config error", + Toast.LENGTH_SHORT).show() + } + // Opening errors + CameraState.ERROR_CAMERA_IN_USE -> { + // Close the camera or ask user to close another camera app that's using the + // camera + Toast.makeText(context, + "Camera in use", + Toast.LENGTH_SHORT).show() + } + CameraState.ERROR_MAX_CAMERAS_IN_USE -> { + // Close another open camera in the app, or ask the user to close another + // camera app that's using the camera + Toast.makeText(context, + "Max cameras in use", + Toast.LENGTH_SHORT).show() + } + CameraState.ERROR_OTHER_RECOVERABLE_ERROR -> { + Toast.makeText(context, + "Other recoverable error", + Toast.LENGTH_SHORT).show() + } + // Closing errors + CameraState.ERROR_CAMERA_DISABLED -> { + // Ask the user to enable the device's cameras + Toast.makeText(context, + "Camera disabled", + Toast.LENGTH_SHORT).show() + } + CameraState.ERROR_CAMERA_FATAL_ERROR -> { + // Ask the user to reboot the device to restore camera function + Toast.makeText(context, + "Fatal error", + Toast.LENGTH_SHORT).show() + } + // Closed errors + CameraState.ERROR_DO_NOT_DISTURB_MODE_ENABLED -> { + // Ask the user to disable the "Do Not Disturb" mode, then reopen the camera + Toast.makeText(context, + "Do not disturb mode enabled", + Toast.LENGTH_SHORT).show() + } + } + } } } /** - * Adapted from: https://github.com/android/camera-samples/blob/master/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic/fragments/CameraFragment.kt#L350 + * [androidx.camera.core.ImageAnalysis.Builder] requires enum value of + * [androidx.camera.core.AspectRatio]. Currently it has values of 4:3 & 16:9. + * + * Detecting the most suitable ratio for dimensions provided in @params by counting absolute + * of preview ratio to one of the provided values. + * + * @param width - preview width + * @param height - preview height + * @return suitable aspect ratio */ private fun aspectRatio(width: Int, height: Int): Int { - val previewRatio = kotlin.math.max(width, height).toDouble() / kotlin.math.min( - width, - height - ) - if (kotlin.math.abs(previewRatio - (4.0 / 3.0)) - <= kotlin.math.abs(previewRatio - (16.0 / 9.0)) - ) { + val previewRatio = max(width, height).toDouble() / min(width, height) + if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) { return AspectRatio.RATIO_4_3 } return AspectRatio.RATIO_16_9 } + /** Returns true if the device has an available back camera. False otherwise */ + private fun hasBackCamera(): Boolean { + return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false + } + + /** Returns true if the device has an available front camera. False otherwise */ + private fun hasFrontCamera(): Boolean { + return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false + } + + companion object { + + private const val TAG = "SilentDragon" + private const val RATIO_4_3_VALUE = 4.0 / 3.0 + private const val RATIO_16_9_VALUE = 16.0 / 9.0 + } + private fun onQrScanned(qrContent: String, image: ImageProxy) { + //Log.d("SilentDragon", "QR scanned: $qrContent") resumedScope.launch { val parsed = viewModel.parse(qrContent) if (parsed == null) { val network = viewModel.networkName - binding.textScanError.text = getString(R.string.scan_invalid_address, network, qrContent) + _fragmentCameraBinding?.textScanError?.text = + getString(R.string.scan_invalid_address, network, qrContent) image.close() } else { /* continue scanning*/ - binding.textScanError.text = "" + _fragmentCameraBinding?.textScanError?.text = "" sendViewModel.toAddress = parsed mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send) } } } - -// private fun updateOverlay(detectedObjects: DetectedObjects) { -// if (detectedObjects.objects.isEmpty()) { -// return -// } -// -// overlay.setSize(detectedObjects.imageWidth, detectedObjects.imageHeight) -// val list = mutableListOf() -// for (obj in detectedObjects.objects) { -// val box = obj.boundingBox -// val name = "${categoryNames[obj.classificationCategory]}" -// val confidence = -// if (obj.classificationCategory != FirebaseVisionObject.CATEGORY_UNKNOWN) { -// val confidence: Int = obj.classificationConfidence!!.times(100).toInt() -// "$confidence%" -// } else { -// "" -// } -// list.add(BoxData("$name $confidence", box)) -// } -// overlay.set(list) -// } - - // - // Permissions - // - - private val requiredPermissions: Array - get() { - return try { - val info = mainActivity?.packageManager - ?.getPackageInfo(mainActivity?.packageName ?: "", PackageManager.GET_PERMISSIONS) - val ps = info?.requestedPermissions - if (ps != null && ps.isNotEmpty()) { - ps - } else { - arrayOfNulls(0) - } - } catch (e: Exception) { - arrayOfNulls(0) - } - } - - private fun allPermissionsGranted(): Boolean { - for (permission in requiredPermissions) { - if (!isPermissionGranted(mainActivity!!, permission!!)) { - return false - } - } - return true - } - - private fun getRuntimePermissions() { - val allNeededPermissions = arrayListOf() - for (permission in requiredPermissions) { - if (!isPermissionGranted(mainActivity!!, permission!!)) { - allNeededPermissions.add(permission) - } - } - - if (allNeededPermissions.isNotEmpty()) { - requestPermissions(allNeededPermissions.toTypedArray(), CAMERA_PERMISSION_REQUEST) - } - } - - companion object { - private const val CAMERA_PERMISSION_REQUEST = 1002 - - private fun isPermissionGranted(context: Context, permission: String): Boolean { - return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED - } - } } diff --git a/app/src/main/java/cash/z/ecc/android/ui/scan/ScanViewModel.kt b/app/src/main/java/cash/z/ecc/android/ui/scan/ScanViewModel.kt index fbe87bd..1ceef91 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/scan/ScanViewModel.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/scan/ScanViewModel.kt @@ -12,9 +12,9 @@ class ScanViewModel : ViewModel() { val networkName get() = synchronizer.network.networkName suspend fun parse(qrCode: String): String? { - // temporary parse code to allow both plain addresses and those that start with zcash: + // temporary parse code to allow both plain addresses and those that start with hush: // TODO: replace with more robust ZIP-321 handling of QR codes - val address = if (qrCode.startsWith("zcash:")) { + val address = if (qrCode.startsWith("hush:")) { qrCode.substring(6, qrCode.indexOf("?").takeUnless { it == -1 } ?: qrCode.length) } else { qrCode diff --git a/app/src/main/java/cash/z/ecc/android/ui/setup/BackupFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/setup/BackupFragment.kt index 89eee80..ba9f2b8 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/setup/BackupFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/setup/BackupFragment.kt @@ -63,6 +63,9 @@ class BackupFragment : BaseFragment() { binding.buttonPositive.setOnClickListener { onEnterWallet().also { if (hasBackUp) tapped(BACKUP_DONE) else tapped(BACKUP_VERIFY) } } + binding.buttonSeedphraseMoreInfo.setOnClickListener { + openSeedPhraseMoreInfoLink() + } if (hasBackUp) { binding.buttonPositive.text = getString(R.string.backup_button_done) } @@ -155,4 +158,10 @@ class BackupFragment : BaseFragment() { result } } + + private fun openSeedPhraseMoreInfoLink() { + getString(R.string.seedphrase_more_info_url).takeUnless { it.isBlank() }?.let { url -> + mainActivity?.onLaunchUrl(url) + } + } } diff --git a/app/src/main/res/drawable/ic_mastodon.xml b/app/src/main/res/drawable/ic_mastodon.xml new file mode 100644 index 0000000..aa3645c --- /dev/null +++ b/app/src/main/res/drawable/ic_mastodon.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_matrix.xml b/app/src/main/res/drawable/ic_matrix.xml new file mode 100644 index 0000000..a984f9f --- /dev/null +++ b/app/src/main/res/drawable/ic_matrix.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_peertube.xml b/app/src/main/res/drawable/ic_peertube.xml new file mode 100644 index 0000000..2311e2e --- /dev/null +++ b/app/src/main/res/drawable/ic_peertube.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_telegram.xml b/app/src/main/res/drawable/ic_telegram.xml new file mode 100644 index 0000000..00337eb --- /dev/null +++ b/app/src/main/res/drawable/ic_telegram.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_website.xml b/app/src/main/res/drawable/ic_website.xml new file mode 100644 index 0000000..711c722 --- /dev/null +++ b/app/src/main/res/drawable/ic_website.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_backup.xml b/app/src/main/res/layout/fragment_backup.xml index de2a4cd..d927b06 100644 --- a/app/src/main/res/layout/fragment_backup.xml +++ b/app/src/main/res/layout/fragment_backup.xml @@ -12,7 +12,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" - app:layout_constraintGuide_percent="0.667" /> + app:layout_constraintGuide_percent="0.75" /> @@ -363,14 +363,15 @@ text_address_part_3, text_address_part_6, text_address_part_9, text_address_part + + + app:srcCompat="@drawable/ic_profile_zebra_01" /> - - + app:layout_constraintTop_toBottomOf="@id/text_address" /> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_scan.xml b/app/src/main/res/layout/fragment_scan.xml index ce794f7..02bb264 100644 --- a/app/src/main/res/layout/fragment_scan.xml +++ b/app/src/main/res/layout/fragment_scan.xml @@ -46,7 +46,7 @@ tools:background="@color/zcashRed" /> + - Verify + I have written down my seed phrase and stored securely + More information Birthday Height: %1$,d Expecting Downloading…%1$d%% diff --git a/app/src/main/res/values/missing_translation.xml b/app/src/main/res/values/missing_translation.xml index 7eb3849..23a7131 100644 --- a/app/src/main/res/values/missing_translation.xml +++ b/app/src/main/res/values/missing_translation.xml @@ -6,9 +6,9 @@ Cancel - Store these backup words securely. + Write down these 24 words on paper. Make sure you write them in the correct order and don\'t misspell any words. Make a copy of that paper and store them securely in at least 2 physical locations empowering\neveryone\nwith\neconomic\nfreedom - Backup verification coming soon! + You may lose all your funds if you didn\'t actually backup! Be smart and responsible! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aacd614..acd4f13 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,6 +13,7 @@ @string/translated_button_done @string/custom_translation_verify + @string/custom_translation_seedphrase_more_info @string/custom_translation_birthday @string/missing_backup_instruction_store_words @string/missing_backup_slogan diff --git a/app/src/zcashmainnet/res/values/strings.xml b/app/src/zcashmainnet/res/values/strings.xml index 9630eb5..c8113d0 100644 --- a/app/src/zcashmainnet/res/values/strings.xml +++ b/app/src/zcashmainnet/res/values/strings.xml @@ -1,5 +1,11 @@ https://explorer.hush.is/tx/%1$s + https://git.hush.is/hush/SilentDragonAndroid/releases + https://hush.is/matrix + https://hush.is/mastodon + https://videos.hush.is/ + https://hush.is/seedphrase HUSH - https://hush.is + https://t.me/hush_main + https://hush.is diff --git a/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt b/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt index 13b9945..8d987b7 100644 --- a/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt +++ b/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt @@ -6,9 +6,9 @@ object Deps { const val kotlinVersion = "1.7.20" const val navigationVersion = "2.5.2" - const val compileSdkVersion = 31 + const val compileSdkVersion = 33 const val minSdkVersion = 21 - const val targetSdkVersion = 30 + const val targetSdkVersion = 33 const val versionName = "1.0.0" const val versionCode = 1_00_00 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release. const val packageName = "hush.android" @@ -25,15 +25,7 @@ object Deps { const val MULTIDEX = "androidx.multidex:multidex:2.0.1" const val PAGING = "androidx.paging:paging-runtime-ktx:2.1.2" const val RECYCLER = "androidx.recyclerview:recyclerview:1.2.1" - object CameraX : Version("1.1.0-alpha05") { - val CAMERA2 = "androidx.camera:camera-camera2:$version" - val CORE = "androidx.camera:camera-core:$version" - val LIFECYCLE = "androidx.camera:camera-lifecycle:$version" - object View : Version("1.0.0-alpha27") { - val EXT = "androidx.camera:camera-extensions:$version" - val VIEW = "androidx.camera:camera-view:$version" - } - } + object Lifecycle : Version("2.4.0-alpha02") { val LIFECYCLE_RUNTIME_KTX = "androidx.lifecycle:lifecycle-runtime-ktx:$version" }