I am trying to set up the in app reviews in my app but just with a button click to show the app review dialogue. There is some limited info here: https://developer.android.com/guide/playcore/in-app-review/kotlin-java
However I am struggling to apply the logic in the docs to achieve my usecase.
Any help would be greatly appreciated.
For Kotliners.
In your Main Activity
private lateinit var reviewInfo: ReviewInfo
private lateinit var manager: ReviewManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupReviewManager()
button.setOnClickListener{
val flow = manager.launchReviewFlow(this, reviewInfo)
flow.addOnCompleteListener { _ ->
Toast.makeText(this, "Thanks for your Review", Toast.LENGTH_SHORT).show()
}
}
}
private fun setupReviewManager(){
manager = ReviewManagerFactory.create(this)
val requestReview = manager.requestReviewFlow()
requestReview.addOnCompleteListener { request ->
if (request.isSuccessful) {
reviewInfo = request.result
} else {
Log.d("ReviewException", request.exception.toString())
}
}
}
Remember to test on a device with google play store, also
Note: The ReviewInfo object is only valid for a limited amount of time. Your app should request a ReviewInfo object ahead of time (pre-cache) but only once you are certain that your app will launch the in-app review flow.
Related
I'm using the new SplashScreen API for android 12 but I'm a bit confused on the login flow now. I have one activity and multiple fragments as advised by google , the mainActivity is where the splashScreen launches and the user's supposed to be directed to either login fragment or the homefragment.
my question is how do I implement this work flow with the new SplashAPI? What fragment should be the startDestination? I don't want to use the popTo attribute since it doesn't look pretty to always show the loginfragment and then direct the user to Homefragment.
If someone could explain this to me I'd be grateful.
Homefragment should be the startDestination. Conditionally navigate to loginfragment and pop back to Homefragment after authentication.
Refer to the following video by Ian Lake.
https://www.youtube.com/watch?v=09qjn706ITA
I have a workaround for that you can set the content of the activity after you check if the user is authorized or not and save the user state in a ViewModel or something, then set your content upon this state.
I leverage the power of setKeepOnScreenCondition function from core-splashscreen library.
SplashInstaller.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SplashInstaller(
activity = this,
beforeHide = {
// Check if logged in or not asyncrounsly
delay(2000)
},
afterHide = {
setContent()
// start with you desired destination up on the check you did in beforeHide
})
}
/**
* #author Mohamed Elshaarawy on Oct 14, 2021.
*/
class SplashInstaller<A : ComponentActivity>(
private val activity: A,
visibilityPredicate: () -> Boolean = { BuildConfig.BUILD_TYPE != "debug" },
private val beforeHide: suspend A.() -> Unit = { delay(2000) },
private val afterHide: A.() -> Unit
) : CoroutineScope by activity.lifecycleScope {
private val isSplashVisibleChannel by lazy { Channel<Boolean>() }
private val isAfterCalled by lazy { Channel<Boolean>(capacity = 1) }
private val splashSuspensionJob = launch(start = CoroutineStart.LAZY) {
activity.beforeHide()
isSplashVisibleChannel.send(false)
}
init {
if (visibilityPredicate()) {
splashSuspensionJob.start()
installSplash()
} else afterSplash()
}
private fun installSplash() {
activity.installSplashScreen().setKeepOnScreenCondition {
val isVisible = isSplashVisibleChannel.tryReceive().getOrNull() ?: true
if (!isVisible) {
afterSplash()
}
isVisible
}
}
private fun afterSplash() {
if (isAfterCalled.tryReceive().getOrNull() != true) {
isAfterCalled.trySend(true)
activity.afterHide()
}
}
}
This solution uses
androidx.core:core-splashscreen:1.0.0-beta01
androidx.lifecycle:lifecycle-runtime-ktx:2.4.0
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2
I am trying to implement 1v1 video chat in my android app using token based authentication. I can generate the token and use it in my app. But it is not working. I wonder that when I generate the token will I use 1 uid for the 2 user or will I use 2 different uid for the 2 user. If I use 1 uid and then will I use the same token for this 2 user. if I use 2 uid and then 2 different token is being created for this 2 user. I can not solve this token based authentication. how does it work? can you help me please?
For example, 1 user is trying to make a video call. I can generate token using this user's uid. When the other user join this video call, will the second user use the same token or the second user will generate another token using its uid. I am confusied in this part. how the second user will join the call? thanks
My code for video call:
class VideoCallActivity : AppCompatActivity() {
private val db = Firebase.firestore
// Kotlin
// Fill the App ID of your project generated on Agora Console.
private val APP_ID = "app_id"
// Fill the channel name.
private val CHANNEL = "appointment"
private var TOKEN =
private var mRtcEngine: RtcEngine? = null
private lateinit var localContainer: FrameLayout
private lateinit var remoteContainer: FrameLayout
private var uniqueUserUidLocal: Int = 0
private var uniqueUserUidRemote: Int = 0
private val mRtcEventHandler = object : IRtcEngineEventHandler() {
// Listen for the remote user joining the channel to get the uid of the user.
override fun onUserJoined(uniqueUserUidRemote: Int, elapsed: Int) {
runOnUiThread {
// Call setupRemoteVideo to set the remote video view after getting uid from the onUserJoined callback.
setupRemoteVideo(uniqueUserUidRemote)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_video_call)
localContainer = findViewById<FrameLayout>(R.id.local_video_view_container)
remoteContainer = findViewById<FrameLayout>(R.id.remote_video_view_container)
val userId = intent.getStringExtra("chaplainUniqueUserId").toString()
uniqueUserUidRemote = userId.toInt(10)
getToken()
//initializeAndJoinChannel()
video_page_finnish_call_imageButton.setOnClickListener {
mRtcEngine?.stopPreview()
mRtcEngine?.leaveChannel()
RtcEngine.destroy()
finish()
}
}
private fun getToken(){
//getting token info from rest api
val retrofit = Retrofit.Builder()
.baseUrl("https://kadir.webprogrammer.fi/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val api = retrofit.create(TokenApiInterface::class.java)
//this part is not clear. which uid should be used remote uid or local uid
api.fetchAllData(uid = uniqueUserUidRemote.toString()).enqueue(object : Callback<TokenModelClass> {
override fun onResponse(
call: Call<TokenModelClass>,
response: Response<TokenModelClass>
) {
TOKEN = response.body()?.token ?: TOKEN
Log.e("TOKEN_1: ", TOKEN)
Log.e("TOKEN_2: ", uniqueUserUidRemote.toString())
initializeAndJoinChannel(TOKEN)
}
override fun onFailure(call: Call<TokenModelClass>, t: Throwable) {
Log.e("TOKEN: ", t.message.toString())
}
})
}
private fun initializeAndJoinChannel(TOKEN: String) {
try {
mRtcEngine = RtcEngine.create(baseContext, APP_ID, mRtcEventHandler)
} catch (e: Exception) {
}
// By default, video is disabled, and you need to call enableVideo to start a video stream.
mRtcEngine!!.enableVideo()
// Call CreateRendererView to create a SurfaceView object and add it as a child to the FrameLayout.
val localFrame = RtcEngine.CreateRendererView(baseContext)
localFrame.setZOrderMediaOverlay(true)
localContainer.addView(localFrame)
// Pass the SurfaceView object to Agora so that it renders the local video.
mRtcEngine!!.setupLocalVideo(VideoCanvas(localFrame, VideoCanvas.RENDER_MODE_FIT, 0))
//this uid is the local user uid, not the remote user uid
// Join the channel with a token.
mRtcEngine!!.joinChannel(TOKEN, CHANNEL, "", 0)
}
private fun setupRemoteVideo(uniqueUserUidRemote: Int) {
val remoteFrame = RtcEngine.CreateRendererView(baseContext)
//remoteFrame.setZOrderMediaOverlay(true)
remoteContainer.addView(remoteFrame)
mRtcEngine!!.setupRemoteVideo(
VideoCanvas(
remoteFrame,
VideoCanvas.RENDER_MODE_FIT,
uniqueUserUidRemote
)
)
}
override fun onDestroy() {
super.onDestroy()
mRtcEngine?.stopPreview()
mRtcEngine?.leaveChannel()
RtcEngine.destroy()
}
}
Every token that you generate is unique to a channel and UID. So when your app loads it should load the network request. Using the token returned from the token server you can call the joinChannel method, make sure you pass the same UID and channel name to the joinChannel parameter that you used while generating the token.
You can read more about it over here: https://www.agora.io/en/blog/connecting-to-agora-with-tokens-android/
Background
In the past, I've found a special app called "Purchased apps" that somehow gets a list of the apps you've purchased. Not seeing any API for this, I asked how does it do it (and sadly still couldn't find a clear answer and a POC to demonstrate it).
The problem
Time passed, and I've noticed there is actually an open sourced app called "Aurora Store" (repository here) that can get about as much information as the Play Store. Screenshot from it:
Thing is, I got issues trying to figure out how to use its code properly, and the weird thing is that those apps get the information from different sources.
What I've tried
So, seeing it allows you to login to Google, and then get the "library" information (history of installed apps), I decided to give it a go (full sample on Github, here) :
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
private val cookieManager = CookieManager.getInstance()
#SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val cachedEmail = defaultSharedPreferences.getString("email", null)
val cachedAasToken = defaultSharedPreferences.getString("aasToken", null)
if (cachedEmail != null && cachedAasToken != null) {
onGotAasToken(applicationContext, cachedEmail, cachedAasToken)
} else {
webView = findViewById(R.id.webView)
cookieManager.removeAllCookies(null)
cookieManager.acceptThirdPartyCookies(webView)
cookieManager.setAcceptThirdPartyCookies(webView, true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
webView.settings.safeBrowsingEnabled = false
}
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
val cookies = CookieManager.getInstance().getCookie(url)
val cookieMap: MutableMap<String, String> = AC2DMUtil.parseCookieString(cookies)
val oauthToken: String? = cookieMap[AUTH_TOKEN]
oauthToken?.let {
webView.evaluateJavascript("(function() { return document.getElementById('profileIdentifier').innerHTML; })();") {
val email = it.replace("\"".toRegex(), "")
Log.d("AppLog", "got email?${email.isNotBlank()} got oauthToken?${oauthToken.isNotBlank()}")
buildAuthData(applicationContext, email, oauthToken)
}
} ?: Log.d("AppLog", "could not get oauthToken")
}
}
webView.settings.apply {
allowContentAccess = true
databaseEnabled = true
domStorageEnabled = true
javaScriptEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
}
webView.loadUrl(EMBEDDED_SETUP_URL)
}
}
companion object {
const val EMBEDDED_SETUP_URL =
"https://accounts.google.com/EmbeddedSetup/identifier?flowName=EmbeddedSetupAndroid"
const val AUTH_TOKEN = "oauth_token"
private fun buildAuthData(context: Context, email: String, oauthToken: String?) {
thread {
try {
val aC2DMResponse: Map<String, String> =
AC2DMTask().getAC2DMResponse(email, oauthToken)
val aasToken = aC2DMResponse["Token"]!!
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putString("email", email).putString("aasToken", aasToken).apply()
onGotAasToken(context, email, aasToken)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun onGotAasToken(context: Context, email: String, aasToken: String) {
thread {
val properties = NativeDeviceInfoProvider(context).getNativeDeviceProperties()
val authData = AuthHelper.build(email, aasToken, properties)
val purchaseHelper = PurchaseHelper(authData).using(HttpClient.getPreferredClient())
var offset = 0
Log.d("AppLog", "list of purchase history:")
while (true) {
val purchaseHistory = purchaseHelper.getPurchaseHistory(offset)
if (purchaseHistory.isNullOrEmpty())
break
val size = purchaseHistory.size
offset += size
purchaseHistory.forEach {
Log.d("AppLog", "${it.packageName} ${it.displayName}")
}
}
Log.d("AppLog", "done")
}
}
}
}
It seems it got the token it needs (and the email), but sadly it seems to get 2 apps and that's it, and then when I try to get the next ones, I get the same 2 apps, twice more, meaning as such:
list of purchase history:
dev.southpaw.dungeon Dungeon Live Wallpaper
com.crydata.mylivewallpaper Hex AMOLED Neon Live Wallpaper 2021
dev.southpaw.dungeon Dungeon Live Wallpaper
com.crydata.mylivewallpaper Hex AMOLED Neon Live Wallpaper 2021
dev.southpaw.dungeon Dungeon Live Wallpaper
com.crydata.mylivewallpaper Hex AMOLED Neon Live Wallpaper 2021
and on the last time it tries to get the next chunk of apps, it crashes with this exception:
FATAL EXCEPTION: Thread-4
Process: com.lb.getplaystoreinstalledappshistory, PID: 6149
Server(code=400, reason=Bad Request)
at com.aurora.gplayapi.helpers.AppDetailsHelper.getAppByPackageName(AppDetailsHelper.kt:115)
at com.aurora.gplayapi.helpers.PurchaseHelper.getPurchaseHistory(PurchaseHelper.kt:63)
at com.lb.getplaystoreinstalledappshistory.MainActivity$Companion$onGotAasToken$1.invoke(MainActivity.kt:96)
at com.lb.getplaystoreinstalledappshistory.MainActivity$Companion$onGotAasToken$1.invoke(MainActivity.kt:68)
at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)
The questions
What's wrong with how I tried to get the list of apps? How can I get it right, ordered by time installed?
Is there any way to get the time they were installed (or any clue about it) ? Somehow the "Purchased apps" app got the time. Granted it was only for purchased apps, but still...
The "Purchased apps" app even got login better, as it doesn't require user-name and password. Instead it offers a dialog to choose the account. Assuming I get it right, is it possible to get the same information using the same login dialog ?
Not sure if this information is remotely useful but might as well mention it...
I accidentally decompiled their archived APK from 2015 when they didn't minimize their code and at least back then, they were using a JSoup HTML parser spider. Possibly they still are, which is probably not allowed by Google and incredibly prone to maintenance.
I'm trying to figure out how to modify (if it's possible) the normal behavior of biometricPrompt, in particular i want to display Gandalf, when the authentication fails.
I'm currently displaying it with a custom alertDialog, but it remains in background, with the biometricPrompt fragment on foreground exactly like this, and it loses all of its dumbness...
The best solution would probably be to display both, alertDialog and biometricPrompt, on foreground, displaying the image only in the upper half of the screen, but at the moment I've no idea of how to do it, or better, I have no idea how to link layouts together to manage size / margins and everything else.
The other thing I was thinking is to remove the biometricPrompt, so the alert dialog will be put on foreground, but any solution I've tried has failed miserably.
Any type of help/ideas will be welcome.
Anyway, here's the code:
class BiometricPromptManager(private val activity: FragmentActivity) {
private val cryptoManager = CryptoManager(activity)
fun authenticateAndDecrypt(failedAction: () -> Unit, successAction: (String) -> Unit) {
// display biometric prompt, if the user is authenticated, the decryption will start
// if biometric related decryption gives positives results, the successAction will start services data decryption
val executor = Executors.newSingleThreadExecutor()
val biometricPrompt = BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
cryptoManager.startDecrypt(failedAction,successAction)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
activity.runOnUiThread { failedAction() }
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
activity.runOnUiThread { failedAction() }
}
})
val promptInfo = biometricPromptInfo()
biometricPrompt.authenticate(promptInfo)
}
private fun biometricPromptInfo(): BiometricPrompt.PromptInfo {
return BiometricPrompt.PromptInfo.Builder()
.setTitle("Fingerprint Authenticator")
.setNegativeButtonText(activity.getString(android.R.string.cancel))
.build()
}
}
Open biometric auth from activity :
private fun openBiometricAuth(){
if(sharedPreferences.getBoolean("fingerPrintEnabled",false)) {
if (BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { // check for hardware/permission
biometric.visibility = View.VISIBLE
BiometricPromptManager(this).authenticateAndDecrypt(::failure, ::callDecryption)
}
}
}
What to do when the user is not recognized :
private fun failure(){
val view = layoutInflater.inflate(R.layout.gandalf, null)
val builder = AlertDialog.Builder(this)
builder.setView(view)
builder.setPositiveButton("Dismiss") { dialog: DialogInterface, id: Int -> dialog.cancel() }
val alertDialog = builder.create()
alertDialog.show()
}
The Biometric API itself handles failed attempts at authentication the following way:
For each failed attempt the onAuthenticationFailed() callback is invoked.
The user gets 5 tries, after the 5th attempt fails the onAuthenticationError() callback receives the error code ERROR_LOCKOUT and the user must wait for 30 seconds to try again.
Showing your dialog inside onAuthenticationFailed() may be too eager and may result in poor user experience. And so a good place might be after you get the ERROR_LOCKOUT. The way the AndroidX Biometric Library works is that it dismisses the BiometricPrompt when the error is sent. Therefore you should have little problem showing your own dialog at that point.
In any case -- i.e. beyond these opinions -- the more general approach is to call cancelAuthentication() to dismiss the prompt and then proceed to showing your own dialog that way.
Also please follow blogpost1 and blogpost2 for recommended design pattern for BiometricPrompt.
Hey Guys Im trying to push the user data in kotlin to firebase but when i click the create account button nothing happends here is the code for Create account class
class CreateAccount : AppCompatActivity() {
var mAuth:FirebaseAuth?=null
var mdata:DatabaseReference?=null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_create_account)
mAuth= FirebaseAuth.getInstance()
Createacc.setOnClickListener{
var email=acemail.text.toString().trim()
var pass=acpass.text.toString().trim()
var name=acname.text.toString().trim()
if(!TextUtils.isEmpty(email)||!TextUtils.isEmpty(name)||!TextUtils.isEmpty(pass)){
createAccount(email,pass,name)
}
else{
Toast.makeText(this,"Please fill all the details",Toast.LENGTH_LONG).show()
}
}
}
fun createAccount(email: String,password:String,dispname:String){
mAuth!!.createUserWithEmailAndPassword(email,password).addOnCompleteListener(this,{
task: Task<AuthResult> ->
if(task.isSuccessful){
var curruser=mAuth!!.currentUser
var userid=curruser!!.uid
Toast.makeText(this,"Building user wait",Toast.LENGTH_LONG).show()
var uobject=HashMap<String,String>()
uobject.put("Display_name",dispname)
uobject.put("Status","Hi I'm New")
uobject.put("image","default")
uobject.put("thumb image","default")
mdata=FirebaseDatabase.getInstance().reference.child("Users").child(userid)
mdata!!.setValue(uobject).addOnCompleteListener{
task:Task<Void> ->
if(task.isSuccessful){
Toast.makeText(this,"User Created",Toast.LENGTH_LONG).show()
}
else{
Toast.makeText(this,"OOPS!! User not Created",Toast.LENGTH_LONG).show()
}
}
}
})
}
}
Main problem is in create account function ,it is being called correctly but right after the createUserWithEmailAndPassword function the task is not successfull hence the if loop does not run. There nothing is printed in the toast Neither "User created " nor "OOPS not created" i dont know whats going on.
I have installed the firebase dependency and my app is connected to a firebase database.