How to check permission is granted in ViewModel? - android

I need to ask permission for contacts and when application starts I'm asking,in ViewModel part I need to call method which requires permission. I need to check permission is granted by user or not and then call, but for checking permission I need to have access Activity. while in my ViewModel I don't have a reference to Activity and don't want to have, How I can overcome, the problem?

I just ran into this problem, and I decided to use make use of LiveData instead.
Core concept:
ViewModel has a LiveData on what permission request needs to be made
ViewModel has a method (essentially callback) that returns if permission is granted or not
SomeViewModel.kt:
class SomeViewModel : ViewModel() {
val permissionRequest = MutableLiveData<String>()
fun onPermissionResult(permission: String, granted: Boolean) {
TODO("whatever you need to do")
}
}
FragmentOrActivity.kt
class FragmentOrActivity : FragmentOrActivity() {
private viewModel: SomeViewModel by lazy {
ViewModelProviders.of(this).get(SomeViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
......
viewModel.permissionRequest.observe(this, Observer { permission ->
TODO("ask for permission, and then call viewModel.onPermissionResult aftwewards")
})
......
}
}

I have reworked the solution. The PermissionRequester object is everything you need to request permissions from any point where you have at least an application context. It uses its helper PermissionRequestActivity to accomplish this job.
#Parcelize
class PermissionResult(val permission: String, val state: State) : Parcelable
enum class State { GRANTED, DENIED_TEMPORARILY, DENIED_PERMANENTLY }
typealias Cancellable = () -> Unit
private const val PERMISSIONS_ARGUMENT_KEY = "PERMISSIONS_ARGUMENT_KEY"
private const val REQUEST_CODE_ARGUMENT_KEY = "REQUEST_CODE_ARGUMENT_KEY"
object PermissionRequester {
private val callbackMap = ConcurrentHashMap<Int, (List<PermissionResult>) -> Unit>(1)
private var requestCode = 256
get() {
requestCode = field--
return if (field < 0) 255 else field
}
fun requestPermissions(context: Context, vararg permissions: String, callback: (List<PermissionResult>) -> Unit): Cancellable {
val intent = Intent(context, PermissionRequestActivity::class.java)
.putExtra(PERMISSIONS_ARGUMENT_KEY, permissions)
.putExtra(REQUEST_CODE_ARGUMENT_KEY, requestCode)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
callbackMap[requestCode] = callback
return { callbackMap.remove(requestCode) }
}
internal fun onPermissionResult(responses: List<PermissionResult>, requestCode: Int) {
callbackMap[requestCode]?.invoke(responses)
callbackMap.remove(requestCode)
}
}
class PermissionRequestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
requestPermissions()
}
}
private fun requestPermissions() {
val permissions = intent?.getStringArrayExtra(PERMISSIONS_ARGUMENT_KEY) ?: arrayOf()
val requestCode = intent?.getIntExtra(REQUEST_CODE_ARGUMENT_KEY, -1) ?: -1
when {
permissions.isNotEmpty() && requestCode != -1 -> ActivityCompat.requestPermissions(this, permissions, requestCode)
else -> finishWithResult()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
val permissionResults = grantResults.zip(permissions).map { (grantResult, permission) ->
val state = when {
grantResult == PackageManager.PERMISSION_GRANTED -> State.GRANTED
ActivityCompat.shouldShowRequestPermissionRationale(this, permission) -> State.DENIED_TEMPORARILY
else -> State.DENIED_PERMANENTLY
}
PermissionResult(permission, state)
}
finishWithResult(permissionResults)
}
private fun finishWithResult(permissionResult: List<PermissionResult> = listOf()) {
val requestCode = intent?.getIntExtra(REQUEST_CODE_ARGUMENT_KEY, -1) ?: -1
PermissionRequester.onPermissionResult(permissionResult, requestCode)
finish()
}
}
Usage:
class MyViewModel(application: Application) : AndroidViewModel(application) {
private val cancelRequest: Cancellable = requestPermission()
private fun requestPermission(): Cancellable {
return PermissionRequester.requestPermissions(getApplication(), "android.permission.SEND_SMS") {
if (it.firstOrNull()?.state == State.GRANTED) {
Toast.makeText(getApplication(), "GRANTED", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(getApplication(), "DENIED", Toast.LENGTH_LONG).show()
}
}
}
override fun onCleared() {
super.onCleared()
cancelRequest()
}
}

I did something like this:
create an abstract class that extends AndroidViewModel which gives you access to the application context:
abstract class BaseViewModel(application: Application) : AndroidViewModel(application), CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
Now, create your view model by extending the BaseViewModel class and you will have access to the application context
class AdminViewModel(application: Application) : BaseViewModel(application) {
.....
}
Now you always have access to a Context that you can use to get access to resources.

Related

Observers don't get notified when LiveData is set through FileObserver

I have a sharedViewModel with LiveData properties.
MainActivity:
private val logViewModel by viewModels<LogViewModel>()
fun startLocationUpdates() {
val locationItem = LocationItem(...)
logViewModel.addLogEntry(locationItem)
}
override fun onCreate(savedInstanceState: Bundle?) {
val observer = object : FileObserver(applicationContext.filesDir.path + "/" + jsonFileName) {
override fun onEvent(event: Int, file: String?) {
if (event == FileObserver.MODIFY) {
loadLogViewModel()
}
}
}
observer.startWatching()
}
fun loadLogViewModel() {
val json = getData(this)
val myType = object : TypeToken<ArrayList<LocationItem>>() {}.type
if (json != null) {
val listItems: ArrayList<LocationItem> = gson.fromJson(json, myType)
for (item in listItems) {
logViewModel.addLogEntry(item)
}
}
}
LogFragment:
private val logViewModel: LogViewModel by activityViewModels()
logViewModel.locationListItems.observe(requireActivity()) {
locationItemAdapter.updateLocations()
}
LogViewModel:
class LogViewModel : ViewModel() {
var locationListItems: MutableLiveData<LocationItem> = MutableLiveData<LocationItem>()
fun addLogEntry(locationItem: LocationItem) {
locationListItems.postValue(locationItem)
}
}
When I execute startLocationUpdates() it all works as expected. The observer gets notifications.
But when the logfile (json) is changed through the WorkManager (potentially within a different process/thread) and the FileObserver kicks in to load the JSON file and adds them to the LogViewModel, the Live data is not firing anything.
Why does it make a difference if the fileObserver is doing it, or if startLocationUpdates() would be doing it? I can only assume this is a threading issue. But I thought LivaData deals with that?

how to get permission result in kotlin

I am trying to use a camera on my android. the user will be prompt for the permission of the camera. previously I used startActivityForResult and onRequestPermissionRequest for them. recently I found out that they are deprecated, so I'm trying out with registerForActivity. I managed to change to startActivity but I'm stuck at the permission request. I am wondering do I have to create another permissionlauncher or can I do the permission inside my resultlauncher.
companion object{
private const val CAMERA_PERMISSION_CODE = 1
private const val CAMERA_REQUEST_CODE = 2
}
val checkpermission = Manifest.permission.CAMERA
var resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
val DP: Bitmap = data!!.extras!!.get("data") as Bitmap
val image = findViewById<ImageView>(R.id.imageButtonVerifyPhoto)
image.setImageBitmap(DP)
}
}
val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()){
isGranted ->
if(isGranted){
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
resultLauncher.launch((intent))
Toast.makeText(this,"Permission is tested", Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(this,"Permission is denied",Toast.LENGTH_SHORT).show()
}
}
var cameraButton = findViewById<Button>(R.id.buttonRetakePhoto) // can change later
cameraButton.setOnClickListener {
if(ContextCompat.checkSelfPermission(
this,
checkpermission
) == PackageManager.PERMISSION_GRANTED
){
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
resultLauncher.launch(intent)
}else{
permissionLauncher.launch(checkpermission)
}
}
}
below is my previous code for the onRequestPermission
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if(requestCode == CAMERA_REQUEST_CODE){
if(grantResults.isNotEmpty()&& grantResults[0] == PackageManager.PERMISSION_GRANTED){
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
startActivityForResult(intent, CAMERA_REQUEST_CODE)
}else{
Toast.makeText(this,"Permission is denied",Toast.LENGTH_SHORT).show()
}
}
}
Requesting runtime permissions is just a little more simplified
private val requestPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
// do something
}
Now we can call this to get any type of permission you want
cameraButton.setOnClickListener {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
// Pass any permission you want while launching
requestPermission.launch(Manifest.permission.CAMERA)
}
}
Make sure to add in build.gradle
implementation 'androidx.fragment:fragment-ktx:1.2.0' // or later
implementation 'androidx.activity:activity-ktx:1.3.0' // or later
If you want to understand how all this works check here
Latest soluton in 2022: (no more request code)
Create Helper Extension Functions (for use in Fragment):
fun Fragment.requestPermissions(request: ActivityResultLauncher<Array<String>>, permissions: Array<String>) = request.launch(permissions)
fun Fragment.isAllPermissionsGranted(permissions: Array<String>) = permissions.all {
ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED
}
Request Permissions in Fragment:
class FirstFragment : Fragment() {
companion object {
private val PERMISSIONS = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
}
private lateinit var binding: FragmentFirstBinding
private lateinit var permissionsRequest: ActivityResultLauncher<Array<String>>
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_first, container, false)
permissionsRequest = getPermissionsRequest()
binding.grantButton.setOnClickListener {
requestPermissions(permissionsRequest, PERMISSIONS) //extension function
}
return binding.root
}
private fun getPermissionsRequest() = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
if (isAllPermissionsGranted(PERMISSIONS)) { //extension function
//do your stuff
} else {
//do your stuff
}
}
}
You could try Aaper, it allows to request for permissions using annotations like so:
Fist, add the permission to your manifest:
<uses-permission android:name="android.permission.CAMERA" />
Then, annotate a method that needs to use the camera:
#EnsurePermissions(permissions = [Manifest.permission.CAMERA])
fun takePhoto() {
Toast.makeText(this, "Camera permission granted", Toast.LENGTH_SHORT).show()
}
That's it, when you call the takePhoto method, it'll check for the camera permission, and if it's not available it will launch a permission request dialog. If the user approves it, then it will proceed to run takePhotos body.
Disclaimer, I'm the creator of Aaper

How to get result using registerForActivityResult from within ktor's Routing call running in a non-activity class?

How to get result from another activity (registerForActivity) from with in ktor's Routing API call (eg. /POST) running in a non-activity class?
Background: For an Android app, I run ktor server engine 'netty' in a non-activity class HttpServer.kt. I need to call another app's activity from with in ktor's Routing' POST handler, so I pass 'appCompatActivity' from MainActivity.kt. That's done, just because, I assume, registerForActivityResult() has dependency on UI/life cycle class.
Problem arises when running this as below, as registerForActivityResult() requires to be run earlier (like onCreate() ?), and I don't have such a class in this non-activity class. Moreover, the callback to run when ActivityResult is returned needs to call ktor ApplicationCall's respond which is also a suspend function.
class HttpServer(
private val applicationContext: AppCompatActivity
) {
private val logger = LoggerFactory.getLogger(HttpServer::class.java.simpleName)
private val server = createServer()
private fun ApplicationCall.startSaleActivityForResult() { // <====== *
val activityLauncherCustom =
applicationContext.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK || result.resultCode == Activity.RESULT_CANCELED) {
val transactionResultReturned = result.data
// Handle the returned result properly using transactionResultReturned
GlobalScope.launch {
respond(status = HttpStatusCode.OK, TransactionResponse())
}
}
}
val intent = Intent()
// Ignoring statements to create proper action/data intent
activityLauncherCustom.launch(intent) // <====== *
}
fun start() = server.start()
fun stop() = server.stop(0, 0)
private fun createServer(): NettyApplicationEngine {
return GlobalScope.embeddedServer(Netty) {
install(CallLogging)
install(ContentNegotiation) {
gson {
setPrettyPrinting()
}
}
routing {
route("/") {
post {
call.startSaleActivityForResult() // <====== *
}
}
}
}
}
private fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration>
CoroutineScope.embeddedServer(
factory: ApplicationEngineFactory<TEngine, TConfiguration>,
module: Application.() -> Unit
): TEngine {
val environment = applicationEngineEnvironment {
this.parentCoroutineContext = coroutineContext + parentCoroutineContext
this.log = logger
this.module(module)
connector {
this.port = 8081
}
}
return embeddedServer(factory, environment)
}
}
Above is what I tried, but gives below error. And I don't have onCreate on this non-activity class.
java.lang.IllegalStateException: LifecycleOwner com.youtap.upti.MainActivity#38dcf06 is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED.
Any suggestions to resolve this problem would be grateful.
Below same above snippet as a screenshot to display helper text on declaration/param types from Android Studio:
And I invoke this server class from onCreate() of MainActivity:
To solve your problem and to hide the complexity you can create an intermediate class for launching activity and waiting for a result to come:
import kotlinx.coroutines.channels.Channel
class Repository(private val activity: MainActivity) {
private val channel = Channel<Int>(1)
suspend fun get(input: String): Int {
activity.activityLauncher.launch(input)
return channel.receive()
}
suspend fun callback(result: Int) {
channel.send(result)
}
}
You can store a reference to a repository and an activity launcher in the MainActivity class:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.IO).launch {
HttpServer(this#MainActivity).also { it.start() }
}
}
val activityLauncher = registerForActivityResult(MySecondActivityContract()) { result ->
GlobalScope.launch {
repository.callback(result!!)
}
}
val repository = Repository(this)
}
My second activity and a contract looks like the following:
class ChildActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_child)
val result = Intent()
result.putExtra("name", 6666)
result.data = Uri.parse("http://mydata")
setResult(Activity.RESULT_OK, result)
finish()
}
}
class MySecondActivityContract : ActivityResultContract<String, Int?>() {
override fun createIntent(context: Context, input: String?): Intent {
return Intent(context, ChildActivity::class.java)
.putExtra("my_input_key", input)
}
override fun parseResult(resultCode: Int, intent: Intent?): Int? = when {
resultCode != Activity.RESULT_OK -> null
else -> intent?.getIntExtra("name", 42)
}
override fun getSynchronousResult(context: Context, input: String?): SynchronousResult<Int?>? {
return if (input.isNullOrEmpty()) SynchronousResult(42) else null
}
}
The most simplest part is routing handler:
routing {
route("/") {
post {
val result = (applicationContext as MainActivity).repository.get("input")
call.respondText { result.toString() }
}
}
}
This solution works but only one request is processed at the same time and it's not robust because Activity may be destroyed before HTTP server or repository objects.

ViewModel does not trigger observer of mutablelivedata

I have the following ViewModel class -
class VerifyOtpViewModel : ViewModel() {
private var existingUserProfileData: MutableLiveData<TwoVerteUsers.TwoVerteUser>? = null
fun checkInfoForAuthenticatedUser(authorization: String, user: String) {
ProfileNetworking.getUsersProfiles(authorization, GetUserProfilesBodyModel(listOf(user)), object : ProfileNetworking.OnGetUserProfilesListener {
override fun onSuccess(model: TwoVerteUsers) {
existingUserProfileData?.value = model[0]
}
override fun onError(reason: String) {
Log.d("existingProfile", reason)
}
})
}
fun getExistingUserProfileData(): LiveData<TwoVerteUsers.TwoVerteUser>? {
if (existingUserProfileData == null) return null
return existingUserProfileData as LiveData<TwoVerteUsers.TwoVerteUser>
}
}
and the following observer -
private fun initViewModel() {
verifyOtpViewModel = ViewModelProvider(this).get(VerifyOtpViewModel::class.java)
verifyOtpViewModel.getExistingUserProfileData()?.observe(this, Observer {
if (it != null)
Log.d("existingProfile", it.username)
})
}
For some reason the observe is never triggered even after the MutableLiveData object is being given a value
Tried to search for a solution here at stackoverflow but nothing helped
what am I missing?
refactor your code to this, and you should be good to go:
class VerifyOtpViewModel : ViewModel() {
private val _existingUserProfileData = MutableLiveData<TwoVerteUsers.TwoVerteUser>()
val existingUserProfileData: LiveData<TwoVerteUsers.TwoVerteUser>
get() = _existingUserProfileData
fun checkInfoForAuthenticatedUser(authorization: String, user: String) {
ProfileNetworking.getUsersProfiles(
authorization,
GetUserProfilesBodyModel(listOf(user)),
object : ProfileNetworking.OnGetUserProfilesListener {
override fun onSuccess(model: TwoVerteUsers) {
existingUserProfileData.value = model[0]
}
override fun onError(reason: String) {
Log.d("existingProfile", reason)
}
})
}
}
And observing:
verifyOtpViewModel.existingUserProfileData.observe(this, Observer {
.....
})

How to prevent memory leak with In-App Update Library

I want to implement the new In-App Update library in my app, but I've noticed that it trigger a memory leak in my activity when it's recreated/rotated.
Here's the only detail I have from LeakCanary:
Obviously, I've nothing if I remove the code from the In-App Update lib especially the addOnSuccessListener :
appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)){
updateInfo.value = appUpdateInfo
updateAvailable.value = true
}else{
updateInfo.value = null
updateAvailable.value = false
}
}
According to this post, I have first used some LiveData, but the problem was the same, so I used a full class to handle the callback, with LiveData :
My Service class :
class AppUpdateService {
val updateAvailable: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
val updateDownloaded: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
val updateInfo: MutableLiveData<AppUpdateInfo> by lazy { MutableLiveData<AppUpdateInfo>() }
fun checkForUpdate(appUpdateManager: AppUpdateManager){
appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)){
updateInfo.value = appUpdateInfo
updateAvailable.value = true
}else{
updateInfo.value = null
updateAvailable.value = false
}
}
}
fun checkUpdateOnResume(appUpdateManager: AppUpdateManager){
appUpdateManager.appUpdateInfo.addOnSuccessListener {
updateDownloaded.value = (it.installStatus() == InstallStatus.DOWNLOADED)
}
}
}
My Activity simplified :
class MainActivity : BaseActivity(), InstallStateUpdatedListener {
override fun contentViewID(): Int { return R.layout.activity_main }
private val UPDATE_REQUEST_CODE = 8000
private lateinit var appUpdateManager : AppUpdateManager
private val appUpdateService = AppUpdateService()
override fun onStateUpdate(state: InstallState?) {
if(state?.installStatus() == InstallStatus.DOWNLOADED){ notifyUser() }
}
// Called in the onCreate()
override fun setupView(){
appUpdateManager = AppUpdateManagerFactory.create(this)
appUpdateManager.registerListener(this)
setupAppUpdateServiceObservers()
// Check for Update
appUpdateService.checkForUpdate(appUpdateManager)
}
private fun setupAppUpdateServiceObservers(){
appUpdateService.updateAvailable.observe(this, Observer {
if (it)
requestUpdate(appUpdateService.updateInfo.value)
})
appUpdateService.updateDownloaded.observe(this, Observer {
if (it)
notifyUser()
})
}
private fun requestUpdate(appUpdateInfo: AppUpdateInfo?){
appUpdateManager.startUpdateFlowForResult(appUpdateInfo, AppUpdateType.FLEXIBLE, this, UPDATE_REQUEST_CODE)
}
private fun notifyUser(){
showSnackbar(getString(R.string.updated_downloaded), getString(R.string.restart)) {
appUpdateManager.completeUpdate()
appUpdateManager.unregisterListener(this)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == UPDATE_REQUEST_CODE) {
if (resultCode != RESULT_OK) {
Timber.d("Update flow failed! Result code: $resultCode")
}
}
}
override fun onDestroy() {
appUpdateManager.unregisterListener(this)
super.onDestroy()
}
override fun onResume() {
super.onResume()
appUpdateService.checkUpdateOnResume(appUpdateManager)
}
}
I don't really understand how to avoid the memory leak as the appUpdateManager has to be created with the context of the activity, and it looks to be the thing that causes the memory leak with the callback.
Does someone already implement it without having this issue?
Using weak reference to the context will probably solve your memory leak problem. Write this in your activity:
WeakReference<Context> contextWeakReference = new WeakReference<Context>(this);
Context context = contextWeakReference.get();
if (context != null) {
// Register using context here
}
There are lots of good articles on WeakReference, Garbage Collection and Memory Leaks to read more on the subject.
Also, onDestroy() is not guaranteed to be called. When you start another Activity, onPause() and onStop() method called instead of onDestroy().
The onDestroy() calls when you hit back button or call finish() method. So, unregister Listener in onPause() or onStop(). If you unregister in onDestroy() method, it might cause a memory leak.
Another idea is that since AppUpdateService class in not a subclass of ViewModel, it is not lifecycle aware. I'm not sure, but, you might need to remove observers in onstop/onDestroy of the activity and add them in onResume. (observers has a strong reference to the LifecycleOwner, here the activiy) To do that you need to define observers to be able to remove them later. Something like:
MutableLiveData<Boolean> someData = new MutableLiveData<>;
and then in onResume:
someData = appUpdateService.updateAvailable;
someData.observe()
and in onStop:
someData.removeObservers()
It's just a guess, but, I hope it would help somehow.
Thanks to #Sina Farahzadi I searched and try a lot of things and figured that the problem was the appUpdateManager.appUdateInfo call with the Task object.
The way I found to solve the memory leak is to use the applicationContext instead of the context of the activity. I'm not sure it's the best solution, but it's the one I've found for now. I've exported all in my service class so here's my code :
AppUpdateService.kt :
class AppUpdateService : InstallStateUpdatedListener {
val updateAvailable: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
val updateDownloaded: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
val notifyUser: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
val updateInfo: MutableLiveData<AppUpdateInfo> by lazy { MutableLiveData<AppUpdateInfo>() }
private var appUpdateManager : AppUpdateManager? = null
private var appUpdateInfoTask: Task<AppUpdateInfo>? = null
override fun onStateUpdate(state: InstallState?) {
notifyUser.value = (state?.installStatus() == InstallStatus.DOWNLOADED)
}
fun setupAppUpdateManager(context: Context){
appUpdateManager = AppUpdateManagerFactory.create(context)
appUpdateManager?.registerListener(this)
checkForUpdate()
}
fun onStopCalled(){
appUpdateManager?.unregisterListener(this)
appUpdateInfoTask = null
appUpdateManager = null
}
fun checkForUpdate(){
appUpdateInfoTask = appUpdateManager?.appUpdateInfo
appUpdateInfoTask?.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)){
updateInfo.value = appUpdateInfo
updateAvailable.value = true
}else{
updateInfo.value = null
updateAvailable.value = false
}
}
}
fun startUpdate(activity: Activity, code: Int){
appUpdateManager?.startUpdateFlowForResult(updateInfo.value, AppUpdateType.FLEXIBLE, activity, code)
}
fun updateComplete(){
appUpdateManager?.completeUpdate()
appUpdateManager?.unregisterListener(this)
}
fun checkUpdateOnResume(){
appUpdateManager?.appUpdateInfo?.addOnSuccessListener {
updateDownloaded.value = (it.installStatus() == InstallStatus.DOWNLOADED)
}
}
}
MainActivity simplified :
class MainActivity : BaseActivity(){
override fun contentViewID(): Int { return R.layout.activity_main }
private val UPDATE_REQUEST_CODE = 8000
private var appUpdateService: AppUpdateService? = AppUpdateService()
/**
* Setup the view of the activity (navigation and menus)
*/
override fun setupView(){
val contextWeakReference = WeakReference<Context>(applicationContext)
contextWeakReference.get()?.let {weakContext ->
appUpdateService?.setupAppUpdateManager(weakContext)
}
}
private fun setupAppUpdateServiceObservers(){
appUpdateService?.updateAvailable?.observe(this, Observer {
if (it)
requestUpdate()
})
appUpdateService?.updateDownloaded?.observe(this, Observer {
if (it)
notifyUser()
})
appUpdateService?.notifyUser?.observe(this, Observer {
if (it)
notifyUser()
})
}
private fun removeAppUpdateServiceObservers(){
appUpdateService?.updateAvailable?.removeObservers(this)
appUpdateService?.updateDownloaded?.removeObservers(this)
appUpdateService?.notifyUser?.removeObservers(this)
}
private fun requestUpdate(){
appUpdateService?.startUpdate(this, UPDATE_REQUEST_CODE)
}
private fun notifyUser(){
showSnackbar(getString(R.string.updated_downloaded), getString(R.string.restart)) {
appUpdateService?.updateComplete()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == UPDATE_REQUEST_CODE) {
if (resultCode != RESULT_OK) {
Timber.d("Update flow failed! Result code: $resultCode")
}
}
}
override fun onStop() {
appUpdateService?.onStopCalled()
removeAppUpdateServiceObservers()
appUpdateService = null
super.onStop()
}
override fun onResume() {
super.onResume()
setupAppUpdateServiceObservers()
appUpdateService?.checkUpdateOnResume()
}
}
For now, I will keep it that way and continue to search for another way to do it.
Let me know if someone has a better way to do it.
Use this helper class:
class GoogleUpdater(activity: FragmentActivity) : LifecycleObserver {
private val appUpdateManager = AppUpdateManagerFactory.create(activity)
private var installStateUpdatedListener: InstallStateUpdatedListener? = null
private var wra = WeakReference(activity)
private val activity get() = wra.get()
init {
activity.lifecycle.addObserver(this)
}
fun checkUpdate() {
fun showCompleteUpdateDialog() {
activity?.let { activity ->
if (!activity.isFinishing)
AlertDialog.Builder(activity)
.setTitle(R.string.notification)
.setMessage(R.string.restart_to_complete_update)
.setIcon(ContextCompat.getDrawable(activity, R.drawable.ic_notification)
?.apply {
mutate()
alpha = 127
})
.setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> appUpdateManager.completeUpdate() }
.setNegativeButton(R.string.no, null)
.create()
.apply { setCanceledOnTouchOutside(false) }
.show()
}
}
installStateUpdatedListener = object : InstallStateUpdatedListener {
override fun onStateUpdate(state: InstallState) {
if (state.installStatus() == InstallStatus.DOWNLOADED)
showCompleteUpdateDialog()
else if (state.installStatus() == InstallStatus.INSTALLED)
appUpdateManager.unregisterListener(this)
}
}.also { appUpdateManager.registerListener(it) }
appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
val clientVersionStalenessDays = appUpdateInfo.clientVersionStalenessDays()
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
&& clientVersionStalenessDays != null
&& clientVersionStalenessDays >= DAYS_FOR_FLEXIBLE_UPDATE) {
try {
activity?.let { activity ->
if (!activity.isFinishing)
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.FLEXIBLE,
activity,
REQUEST_CODE_APP_UPDATE)
}
} catch (e: SendIntentException) {
FirebaseCrashlytics.getInstance().recordException(e)
}
} else if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED)
showCompleteUpdateDialog()
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
private fun onStop() {
installStateUpdatedListener?.let { appUpdateManager.unregisterListener(it) }
}
companion object {
const val REQUEST_CODE_APP_UPDATE = 11
const val DAYS_FOR_FLEXIBLE_UPDATE = 1
}
}
In Activity:
GoogleUpdater(this).apply { checkUpdate() }

Categories

Resources