Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/expo-go/android/expoview/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@
<data android:scheme="expauth" />
</intent-filter>
</activity>

<activity
android:name="host.exp.exponent.home.qrScanner.QRScannerActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.NoActionBar" />
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.play.core.review.ReviewManagerFactory
import com.google.gson.Gson
import com.google.mlkit.vision.barcode.common.Barcode
Expand Down Expand Up @@ -523,29 +525,34 @@ class HomeAppViewModel(
fun scanQR(
context: Context,
onSuccess: (String) -> Unit,
onError: (String) -> Unit = {}
onError: (String) -> Unit = {},
onNoPlayServices: () -> Unit
) {
val options = GmsBarcodeScannerOptions
.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()

val scanner = GmsBarcodeScanning.getClient(context, options)

scanner.startScan()
.addOnSuccessListener { barcode ->
val url = barcode.rawValue ?: run {
onError("No QR code data found")
return@addOnSuccessListener
if (isPlayServicesAvailable(context)) {
val options = GmsBarcodeScannerOptions
.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()

val scanner = GmsBarcodeScanning.getClient(context, options)

scanner.startScan()
.addOnSuccessListener { barcode ->
val url = barcode.rawValue ?: run {
onError("No QR code data found")
return@addOnSuccessListener
}
onSuccess(url)
}
onSuccess(url)
}
.addOnCanceledListener {
onError("QR code scan cancelled")
}
.addOnFailureListener { exception ->
onError("QR code scan failed: ${exception.message ?: "Unknown error"}")
}
.addOnCanceledListener {
onError("QR code scan cancelled")
}
.addOnFailureListener { exception ->
onError("QR code scan failed: ${exception.message ?: "Unknown error"}")
}
} else {
onNoPlayServices()
}
}

/**
Expand Down Expand Up @@ -626,6 +633,12 @@ class HomeAppViewModel(
updateUserReviewState(apps.dataFlow.value.size, snacks.dataFlow.value.size)
}
}

private fun isPlayServicesAvailable(context: Context): Boolean {
val googleApiAvailability = GoogleApiAvailability.getInstance()
val resultCode = googleApiAvailability.isGooglePlayServicesAvailable(context)
return resultCode == ConnectionResult.SUCCESS
}
}

class RefreshableFlow<T>(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package host.exp.exponent.home

import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
Expand Down Expand Up @@ -28,6 +29,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import host.exp.exponent.home.qrScanner.QRScannerActivity
import host.exp.expoview.R

@OptIn(ExperimentalMaterial3Api::class)
Expand All @@ -52,6 +54,11 @@ fun HomeScreen(

val context = LocalContext.current
val uriHandler = LocalUriHandler.current
val fallbackQrScannerLauncher = rememberLauncherForActivityResult(
contract = QRScannerActivity.Contract()
) { url ->
url?.let { uriHandler.openUri(it) }
}

val state = rememberPullToRefreshState()
val onRefresh: () -> Unit = {
Expand Down Expand Up @@ -135,6 +142,9 @@ fun HomeScreen(
},
onError = { error ->
Toast.makeText(context, error, Toast.LENGTH_LONG).show()
},
onNoPlayServices = {
fallbackQrScannerLauncher.launch(Unit)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package host.exp.exponent.home.qrScanner

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke

fun DrawScope.drawRoundedCorner(
offset: Offset,
startAngle: Float,
strokeWidth: Float,
cornerRadius: Float,
horizontalLineStart: Offset,
horizontalLineEnd: Offset,
verticalLineStart: Offset,
verticalLineEnd: Offset,
color: Color = Color.White
) {
drawArc(
color = color,
startAngle = startAngle,
sweepAngle = 90f,
useCenter = false,
topLeft = offset,
size = Size(cornerRadius * 2, cornerRadius * 2),
style = Stroke(width = strokeWidth)
)
drawLine(
color = color,
start = horizontalLineStart,
end = horizontalLineEnd,
strokeWidth = strokeWidth
)
drawLine(
color = color,
start = verticalLineStart,
end = verticalLineEnd,
strokeWidth = strokeWidth
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package host.exp.exponent.home.qrScanner

import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.mlkit.vision.MlKitAnalyzer
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean

class QRScannerActivity : ComponentActivity() {
class Contract : ActivityResultContract<Unit, String?>() {
override fun createIntent(context: Context, input: Unit): Intent {
return Intent(context, QRScannerActivity::class.java)
}

override fun parseResult(resultCode: Int, intent: Intent?): String? {
return if (resultCode == RESULT_OK) {
intent?.getStringExtra("data")
} else {
null
}
}
}

private lateinit var cameraExecutor: ExecutorService
private val isScanned = AtomicBoolean(false)

private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
startCamera()
} else {
Toast.makeText(this, "Camera permission required", Toast.LENGTH_SHORT).show()
finish()
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
cameraExecutor = Executors.newSingleThreadExecutor()

val cameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
if (cameraPermission == PackageManager.PERMISSION_GRANTED) {
startCamera()
} else {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}

private fun startCamera() {
setContent {
Box(modifier = Modifier.fillMaxSize()) {
CameraPreview(
onBarcodeDetected = { url ->
if (isScanned.compareAndSet(false, true)) {
setResult(Activity.RESULT_OK, Intent().apply { putExtra("data", url) })
finish()
}
}
)
QRScannerOverlay()
}
}
}

override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}

@Composable
private fun CameraPreview(onBarcodeDetected: (String) -> Unit) {
AndroidView(
factory = { context ->
val previewView = PreviewView(context)
previewView.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
previewView.scaleType = PreviewView.ScaleType.FILL_CENTER

val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build()
preview.surfaceProvider = previewView.surfaceProvider

val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
val barcodeScanner = BarcodeScanning.getClient(options)

val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()

val analyzer = MlKitAnalyzer(
listOf(barcodeScanner),
ImageAnalysis.COORDINATE_SYSTEM_ORIGINAL,
ContextCompat.getMainExecutor(context)
) { result: MlKitAnalyzer.Result? ->
val barcodeResults = result?.getValue(barcodeScanner)
if (!barcodeResults.isNullOrEmpty()) {
val first = barcodeResults.first()
first.rawValue?.let {
onBarcodeDetected(it)
}
}
}

imageAnalysis.setAnalyzer(cameraExecutor, analyzer)

try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageAnalysis
)
} catch (exc: Exception) {
exc.printStackTrace()
}
}, ContextCompat.getMainExecutor(context))

previewView
},
modifier = Modifier.fillMaxSize()
)
}
}
Loading
Loading