NTP-29800 | QR code as wallpaper (#15469)

This commit is contained in:
Ujjwal Kumar
2025-03-21 13:07:10 +05:30
committed by GitHub
parent 784e91e6ab
commit a0f08f28f0
12 changed files with 300 additions and 10 deletions

View File

@@ -47,6 +47,7 @@ androidx-test-junit = "1.1.5"
androidx-test-monitor = "1.6.1"
androidx-test-rules = "1.5.0"
androidx-test-runner = "1.5.0"
androidx-window = "1.3.0"
androidx-work-runtimeKtx = "2.10.0"
anrwatchdog = "1.4.0"
appsflyer = "6.16.0"
@@ -236,6 +237,7 @@ androidx-test-runner = { module = "androidx.test:runner", version.ref = "android
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
androidx-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding", version.ref = "uiViewbinding" }
androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" }
androidx-work-runtimeKtx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work-runtimeKtx" }
anrwatchdog = { module = "com.github.anrwatchdog:anrwatchdog", version.ref = "anrwatchdog" }

View File

@@ -139,6 +139,7 @@ object FirebaseRemoteConfigHelper {
const val NAVI_PAY_SEND_MONEY_PRE_VALIDATION_ENABLED =
"NAVI_PAY_SEND_MONEY_PRE_VALIDATION_ENABLED"
const val NAVI_PAY_AUTO_READ_OTP_DISABLED = "NAVI_PAY_AUTO_READ_OTP_DISABLED"
const val NAVI_PAY_SET_AS_WALLPAPER_ENABLED = "NAVI_PAY_SET_AS_WALLPAPER_ENABLED"
// COMMON
const val LITMUS_EXPERIMENTS_CACHE_DURATION_IN_MILLIS =

View File

@@ -97,6 +97,7 @@ dependencies {
implementation libs.androidx.paging.compose
implementation libs.androidx.room.paging
implementation libs.androidx.sqlite
implementation libs.androidx.window
implementation libs.dagger.hiltAndroid
implementation libs.kotlinx.serialization.json
implementation libs.mlkit.barcodeScanning

View File

@@ -17,6 +17,7 @@
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />

View File

@@ -5164,6 +5164,10 @@ class NaviPayAnalytics private constructor() {
),
)
}
fun onSetAsWallpaperClicked() {
NaviTrackEvent.trackEventOnClickStream(eventName = "NaviPay_SetAsWallpaper_Clicked")
}
}
inner class NaviPayViewModel {

View File

@@ -49,6 +49,8 @@ enum class CardType {
ADD_ACCOUNT,
}
data class QrAsWallpaperStateHolder(val qrDetails: QrDetails?, val showSnackBar: Boolean)
sealed class SettingAction {
data class Download(val qrDetails: QrDetails?) : SettingAction()

View File

@@ -12,15 +12,36 @@ import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
@@ -28,20 +49,31 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.navi.base.deeplink.DeepLinkManager
import com.navi.base.model.CtaData
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.NAVI_PAY_SET_AS_WALLPAPER_ENABLED
import com.navi.common.ui.compose.DrawerState
import com.navi.common.utils.toCtaData
import com.navi.design.font.FontWeightEnum
import com.navi.design.font.getFontWeight
import com.navi.design.font.naviFontFamily
import com.navi.naviwidgets.extensions.NaviText
import com.navi.pay.R
import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.common.settingscreen.model.QrDetails
import com.navi.pay.common.settingscreen.model.SettingAction
import com.navi.pay.common.settingscreen.utils.downloadQR
import com.navi.pay.common.settingscreen.utils.getQRBitmapForWallpaper
import com.navi.pay.common.settingscreen.utils.saveQrInGallery
import com.navi.pay.common.settingscreen.utils.shareQR
import com.navi.pay.common.settingscreen.viewmodel.SettingSDKViewmodel
import com.navi.pay.common.theme.color.NaviPayColor
import com.navi.pay.common.utils.NaviPayCommonUtils
import com.navi.pay.common.utils.launchOnboardingSDK
import com.navi.pay.utils.ACCOUNT_ID
import com.navi.pay.utils.ObserveAsEvents
import com.navi.pay.utils.SAVINGS_ONLY_ENABLED_ACCOUNTS
import com.navi.pay.utils.noRippleClickable
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -99,6 +131,10 @@ fun UPISettingSDK(
val naviPayAccessEligibility = remember {
NaviPayCommonUtils.getNaviPayAccessEligibility(activity)
}
val qrAsWallpaperState by
settingSDKViewmodel.qrAsWallpaperStateHolder.collectAsStateWithLifecycle()
val galleryLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result
->
@@ -150,6 +186,16 @@ fun UPISettingSDK(
galleryLauncher = galleryLauncher,
)
naviSettingSDKAnalytics.onDownloadQRClicked()
if (FirebaseRemoteConfigHelper.getBoolean(NAVI_PAY_SET_AS_WALLPAPER_ENABLED)) {
settingSDKViewmodel.updateQrAsWallpaperState(
showSnackBar = true,
qrDetails = action.qrDetails,
)
} else {
Toast.makeText(activity, "Downloaded successfully", Toast.LENGTH_SHORT)
.show()
}
}
is SettingAction.Copy -> {
clipboardManager.setText(annotatedString = AnnotatedString(action.copy))
@@ -200,11 +246,118 @@ fun UPISettingSDK(
}
}
}
UPISettingContent(
upiSettingDetails = upiSettingDetails,
onSelected = onSelected,
drawerState = drawerState,
qrPagerAnimationAvailable = qrPagerAnimationAvailable,
updateQrPagerAnimationCounter = settingSDKViewmodel::incrementQrPagerAnimationCounter,
)
Box {
UPISettingContent(
upiSettingDetails = upiSettingDetails,
onSelected = onSelected,
drawerState = drawerState,
qrPagerAnimationAvailable = qrPagerAnimationAvailable,
updateQrPagerAnimationCounter = settingSDKViewmodel::incrementQrPagerAnimationCounter,
)
if (qrAsWallpaperState.showSnackBar) {
SnackBarSection(
modifier =
Modifier.align(Alignment.TopCenter)
.padding(top = 640.dp)
.fillMaxWidth()
.padding(horizontal = 16.dp),
onDismissSnackBar = {
settingSDKViewmodel.updateQrAsWallpaperState(showSnackBar = false)
},
onActionClicked = {
val wallpaperBitmap =
getQRBitmapForWallpaper(
activity = activity,
qrDetails = qrAsWallpaperState.qrDetails,
)
wallpaperBitmap?.let {
settingSDKViewmodel.setWallpaper(
bitmap = it,
setAsHomeScreen = true,
setAsLockScreen = false,
)
}
naviSettingSDKAnalytics.onSetAsWallpaperClicked()
settingSDKViewmodel.updateQrAsWallpaperState(showSnackBar = false)
},
message = stringResource(R.string.np_qr_downloaded),
actionLabel = stringResource(R.string.np_set_as_wallpaper),
)
}
}
}
@Composable
private fun SnackBarSection(
modifier: Modifier = Modifier,
onDismissSnackBar: () -> Unit,
onActionClicked: () -> Unit,
message: String,
actionLabel: String,
) {
val snackState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
SnackbarHost(modifier = modifier, hostState = snackState) {
Snackbar(
content = {
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(id = R.drawable.navi_pay_ic_checked_circle_green),
contentDescription = null,
modifier = Modifier.size(14.dp),
)
Spacer(modifier = Modifier.width(8.dp))
NaviText(
text = message,
fontFamily = naviFontFamily,
fontSize = 12.sp,
lineHeight = 18.sp,
fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR),
color = NaviPayColor.textWhite,
)
Spacer(modifier = Modifier.weight(1f))
NaviText(
text = actionLabel,
fontFamily = naviFontFamily,
fontSize = 12.sp,
lineHeight = 18.sp,
fontWeight = getFontWeight(FontWeightEnum.NAVI_HEADLINE_REGULAR),
color = NaviPayColor.textWhite,
textDecoration = TextDecoration.Underline,
modifier = Modifier.noRippleClickable { onActionClicked.invoke() },
)
}
}
)
}
LaunchedEffect(Unit) {
scope.launch {
val snackBarResult =
snackState.showSnackbar(
message = message,
actionLabel = actionLabel,
duration = SnackbarDuration.Short,
)
when (snackBarResult) {
SnackbarResult.Dismissed -> onDismissSnackBar()
SnackbarResult.ActionPerformed -> {
onActionClicked()
}
}
}
}
}

View File

@@ -11,14 +11,18 @@ import android.app.Activity
import android.content.ContentValues
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.view.LayoutInflater
import android.widget.Toast
import android.view.View
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.result.ActivityResult
import androidx.core.graphics.createBitmap
import androidx.window.layout.WindowMetricsCalculator
import com.navi.base.model.ActionData
import com.navi.common.screenshot.ShareBitmapImage
import com.navi.common.upi.IS_ONBOARDING_REQUIRED
@@ -62,7 +66,6 @@ fun downloadQR(
outputStream.close()
}
}
Toast.makeText(activity, "Downloaded successfully", Toast.LENGTH_SHORT).show()
NaviPayNotificationHandler.showNotification(
context = activity.baseContext,
@@ -116,6 +119,47 @@ private fun getQRBitmap(activity: Activity, qrDetails: QrDetails?): Bitmap? {
}
}
fun getQRBitmapForWallpaper(activity: Activity, qrDetails: QrDetails?): Bitmap? {
try {
val windowMetrics =
WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)
val screenWidth = windowMetrics.bounds.width()
val screenHeight = windowMetrics.bounds.height()
val wallpaperBitmap = createBitmap(width = screenWidth, height = screenHeight)
val canvas = Canvas(wallpaperBitmap)
canvas.drawColor(Color.WHITE)
val binding = LayoutShareQrBinding.inflate(LayoutInflater.from(activity))
binding.nameTv.setText(qrDetails?.linkedAccountEntity?.name.orEmpty())
binding.vpaTv.setText(qrDetails?.linkedAccountEntity?.primaryVpa.orEmpty())
binding.vpaImage.setImageURI(qrDetails?.qrCodeUri)
val qrView = binding.root
qrView.measure(
View.MeasureSpec.makeMeasureSpec(screenWidth, View.MeasureSpec.AT_MOST),
View.MeasureSpec.makeMeasureSpec(screenHeight, View.MeasureSpec.AT_MOST),
)
qrView.layout(0, 0, qrView.measuredWidth, qrView.measuredHeight)
val qrBitmap = createBitmap(width = qrView.measuredWidth, height = qrView.measuredHeight)
qrView.draw(Canvas(qrBitmap))
val left = (screenWidth - qrView.measuredWidth) / 2
val verticalOffset = screenHeight * 0.05f
val top = ((screenHeight - qrView.measuredHeight) / 2) - verticalOffset
canvas.drawBitmap(qrBitmap, left.toFloat(), top, null)
return wallpaperBitmap
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
fun getFilteredLinkedAccounts(
accountType: String,
linkedAccounts: List<LinkedAccountEntity>,

View File

@@ -0,0 +1,56 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.pay.common.settingscreen.utils
import android.app.WallpaperManager as AndroidWallpaperManager
import android.content.Context
import android.graphics.Bitmap
import com.navi.common.utils.log
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
interface WallpaperManager {
fun setWallpaper(bitmap: Bitmap, setAsHomeScreen: Boolean, setAsLockScreen: Boolean): Boolean
}
class WallpaperManagerImpl @Inject constructor(@ApplicationContext private val context: Context) :
WallpaperManager {
private val androidWallpaperManagerInstance = AndroidWallpaperManager.getInstance(context)
override fun setWallpaper(
bitmap: Bitmap,
setAsHomeScreen: Boolean,
setAsLockScreen: Boolean,
): Boolean {
if (
!androidWallpaperManagerInstance.isWallpaperSupported ||
!androidWallpaperManagerInstance.isSetWallpaperAllowed
) {
return false
}
try {
val flag =
when {
setAsHomeScreen && setAsLockScreen ->
AndroidWallpaperManager.FLAG_LOCK or AndroidWallpaperManager.FLAG_SYSTEM
setAsHomeScreen -> AndroidWallpaperManager.FLAG_SYSTEM
setAsLockScreen -> AndroidWallpaperManager.FLAG_LOCK
else -> AndroidWallpaperManager.FLAG_SYSTEM
}
androidWallpaperManagerInstance.setBitmap(bitmap, null, true, flag)
return true
} catch (e: Exception) {
e.log()
return false
}
}
}

View File

@@ -7,6 +7,7 @@
package com.navi.pay.common.settingscreen.viewmodel
import android.graphics.Bitmap
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.viewModelScope
import com.navi.base.cache.model.NaviCacheEntity
@@ -18,7 +19,10 @@ import com.navi.pay.analytics.NaviPayAnalytics
import com.navi.pay.analytics.NaviPayAnalytics.Companion.NAVI_PAY_UPI_SETTINGS_SDK
import com.navi.pay.common.model.view.NaviPayFlowType
import com.navi.pay.common.repository.SharedPreferenceRepository
import com.navi.pay.common.settingscreen.model.QrAsWallpaperStateHolder
import com.navi.pay.common.settingscreen.model.QrDetails
import com.navi.pay.common.settingscreen.model.UpiSettingDetails
import com.navi.pay.common.settingscreen.utils.WallpaperManager
import com.navi.pay.common.settingscreen.utils.isOnboardingRequired
import com.navi.pay.common.setup.NaviPayManager
import com.navi.pay.common.usecase.LinkedAccountsUseCase
@@ -57,12 +61,13 @@ constructor(
private val linkedAccountsUseCase: LinkedAccountsUseCase,
private val naviPayPspManager: NaviPayPspManager,
val naviPayOnboardingNavigator: NaviPayOnboardingNavigator,
private val wallpaperManager: WallpaperManager,
) : NaviPayBaseVM() {
private val _upiSettingDetails = MutableStateFlow(UpiSettingDetails())
val upiSettingDetails = _upiSettingDetails.asStateFlow()
val naviSettingSDKAnalytics: NaviPayAnalytics.NaviSettingSDK =
private val naviSettingSDKAnalytics: NaviPayAnalytics.NaviSettingSDK =
NaviPayAnalytics.INSTANCE.NaviSettingSDK()
private val qrPagerAnimationCounter = getQrPagerAnimationCounter()
@@ -84,6 +89,10 @@ constructor(
private var pendingCollectRequestCount = 0
private var autoPayPendingRequestCount = 0
private val _qrAsWallpaperStateHolder =
MutableStateFlow(QrAsWallpaperStateHolder(qrDetails = null, showSnackBar = false))
val qrAsWallpaperStateHolder = _qrAsWallpaperStateHolder.asStateFlow()
init {
viewModelScope.launch(Dispatchers.IO) {
linkedAccountsUseCase.execute(screenName = screenName).collectLatest { linkedAccounts ->
@@ -251,6 +260,16 @@ constructor(
}
}
fun updateQrAsWallpaperState(qrDetails: QrDetails? = null, showSnackBar: Boolean) {
_qrAsWallpaperStateHolder.update {
QrAsWallpaperStateHolder(qrDetails = qrDetails, showSnackBar = showSnackBar)
}
}
fun setWallpaper(bitmap: Bitmap, setAsHomeScreen: Boolean, setAsLockScreen: Boolean): Boolean {
return wallpaperManager.setWallpaper(bitmap, setAsHomeScreen, setAsLockScreen)
}
override val screenName: String
get() = NAVI_PAY_UPI_SETTINGS_SDK
}

View File

@@ -22,6 +22,8 @@ import com.navi.pay.common.connectivity.NaviPayNetworkConnectivityImpl
import com.navi.pay.common.magiclocation.util.NetworkInfoDataProvider
import com.navi.pay.common.magiclocation.util.NetworkInfoDataProviderImpl
import com.navi.pay.common.repository.SharedPreferenceRepository
import com.navi.pay.common.settingscreen.utils.WallpaperManager
import com.navi.pay.common.settingscreen.utils.WallpaperManagerImpl
import com.navi.pay.common.setup.NaviPayManager
import com.navi.pay.common.utils.DeviceInfoImpl
import com.navi.pay.common.utils.DeviceInfoProvider
@@ -252,6 +254,9 @@ abstract class NaviPayViewModelScopedModule {
abstract fun bindNetworkInfoDataProvider(
networkInfoDataProviderImpl: NetworkInfoDataProviderImpl
): NetworkInfoDataProvider
@Binds
abstract fun bindWallpaperManager(wallpaperManagerImpl: WallpaperManagerImpl): WallpaperManager
}
@Module

View File

@@ -851,4 +851,6 @@
<string name="np_non_verified_sender_login_title">Retry OTP validation</string>
<string name="np_non_verified_sender_login_desc">Please logout and try logging in again. Ensure that the mobile number used for UPI registration is in your device and the OTP is auto-entered.</string>
<string name="np_logout">Logout</string>
<string name="np_qr_downloaded">QR downloaded</string>
<string name="np_set_as_wallpaper">Set as wallpaper</string>
</resources>