Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
158 views
in Technique[技术] by (71.8m points)

android - Logout service at midnight with both system and local broadcast receiver

I'm trying to logout user at midnight. This consists of erasing local data and calling remote endpoint. If app is running, it must inform user with dialog and status bar notification. If app is not running, it only leaves notification in status bar that user has been logged out. Currently I managed to run service manually by clicking a menu item - it starts and runs as expected, receiving local broadcast (so, it only works when app is running):

R.id.menu_option_test -> {
                println("menu_option_test")
                Intent(this, LogoutService::class.java).also { intent ->
                    startService(intent)
                }
                return true
            }
...
private fun watchForServiceBroadcasts() {
    mBroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            when (intent.action) {
                Constants.ACTION_RESTART_APP -> {
                    showDialog(getString(R.string.you_being_auto_logout), "OK", "")
                }
            }
        }
    }
    mLocalBroadcastManager = LocalBroadcastManager.getInstance(this)
    val restartIntentFilter = IntentFilter(Constants.ACTION_RESTART_APP)
    mLocalBroadcastManager!!.registerReceiver(mBroadcastReceiver!!, restartIntentFilter)
}

Unfortunately, when I schedule this service to start at midnight, it doesn't run. When I restart device to receive BOOT_COMPLETED intent, I see Toast, that this system intent was received and service was scheduled, but it doesn't run at scheduled time. I'm not sure how to debug these system pending intents. I think there are errors happening inside a service which are hidden from my eye.

Here's my AndroidManifest part where these service/receivers are written. Seems OK (receiver is enabled manually to run even during restarts at first activity onCreate()):

...<service android:name=".services.LogoutService" />
   <receiver
       android:name=".services.BootReceiver"
       android:enabled="false">
           <intent-filter>
               <action android:name="android.intent.action.BOOT_COMPLETED" />
           </intent-filter>
    </receiver>
</application>

And finally, the LogoutService with BootReceiver:

class LogoutService : Service(), CoroutineScope {

    private var coroutineJob: Job = Job()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.IO + coroutineJob
    private var mRedelivery: Boolean = false

    private var mXApi: XApi? = null
    private var localDatabase: LocalDatabase? = null
    private var localDataSource: LocalDataSource? = null
    private var remoteDataSource: RemoteDataSource? = null
    private var sharedDataSource: SharedDataSource? = null
    private var mainRepository: MainRepository? = null

    fun setIntentRedelivery(enabled: Boolean) {
        mRedelivery = enabled
    }

    override fun onBind(intent: Intent?): IBinder? = null

    private suspend fun logoutUser() {
        Logger.d("Running LogoutService logoutUser()")
        withContext(coroutineContext) {
            try {
                val UserPin = mainRepository?.getUser()?.value?.pinCode
                Logger.d("Logging out User with pin $UserPin")
                if (UserPin != null) {
                    mainRepository?.logoutAndDeleteLocalData(UserPin)
                    showLogoutStatusBarNotification()
                    receiveRestartBroadcastIfAppRunning()
                } else {
                    println("Cancelling coroutineJob")
                    coroutineJob.cancel()
                }
            } catch (e: Throwable) {
                Logger.e(e.message!!)
                coroutineJob.cancel()
            }
        }
        if (coroutineJob.isCompleted) {
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
                StringUtils.scheduleNextLogoutService(this)
            }
            onDestroy()
        }
    }

    override fun onCreate() {
        ToastUtils.updateWarning(this, getString(R.string.you_being_auto_logout))
        mXApi = XApi
        localDatabase = LocalDatabase.getInstance(this)
        localDataSource = LocalDataSource(localDatabase!!, Dispatchers.IO)
        remoteDataSource = RemoteDataSource(mXApi!!, Dispatchers.IO)
        sharedDataSource = SharedDataSource(localDataSource!!, remoteDataSource!!, Dispatchers.IO, this)
        this.launch {
            logoutUser()
        }
    }

    // only when started with startService() ex. from activity
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        return if (mRedelivery) START_REDELIVER_INTENT else START_NOT_STICKY
    }

    private fun showLogoutStatusBarNotification() {
        val logoutNotification = NotificationCompat.Builder(this, Constants.NOTIFICATION_AUTH_CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_logo)
                .setContentTitle(getString(R.string.auto_logout))
                .setContentText(getString(R.string.you_being_auto_logout))
                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                .setAutoCancel(true)
        with(NotificationManagerCompat.from(this)) {
            notify(555, logoutNotification.build())
        }
    }

    // broadcast only reaches application if it's running and has registered receiver
    private fun receiveRestartBroadcastIfAppRunning() {
        // this broadcast invokes dialog which restarts application on any answer
        LocalBroadcastManager.getInstance(this)
                .sendBroadcast(Intent(Constants.ACTION_RESTART_APP))
    }
}

BootReceiver

class BootReceiver : BroadcastReceiver() {

    private var alarmMgr: AlarmManager? = null
    private lateinit var logoutIntent: PendingIntent

    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action.equals(Intent.ACTION_BOOT_COMPLETED)) {
            Toast.makeText(context, "Setting up logout service to run at midnight", Toast.LENGTH_LONG).show()
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
                StringUtils.scheduleNextLogoutService(context)
            } else {
                alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
                logoutIntent = Intent(context, LogoutService::class.java).let {
                    PendingIntent.getService(context, 0, it, 0)
                }
                // Set the alarm to start at approximately 0:00 p.m.
                val calendar: Calendar = Calendar.getInstance().apply {
                    timeInMillis = System.currentTimeMillis()
                    set(Calendar.HOUR_OF_DAY, 0)
                }
                // With setInexactRepeating(), you have to use one of the AlarmManager interval
                // constants--in this case, AlarmManager.INTERVAL_DAY.
                alarmMgr?.setInexactRepeating(
                        AlarmManager.RTC_WAKEUP,
                        calendar.timeInMillis,
                        AlarmManager.INTERVAL_DAY,
                        logoutIntent)
            }
        } else {
            Logger.e("LogoutReceiver: not allowed intent filter called")
        }
    }
}

I've also tried to call BOOT_COMPLETED manually through adb shell - it shows Toast "Setting up logout service to run at midnight", so alarm is scheduled. Also, dumpsys alarm output shows that alarm is scheduled. When it comes time to run it at midnight - nothing happens. What's the solution here?

question from:https://stackoverflow.com/questions/65940977/logout-service-at-midnight-with-both-system-and-local-broadcast-receiver

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Seems to be resolved. On some phones, like Xiaomi with MIUI, BOOT_COMPLETE is not received, so I schedule alarm on app start in launcher activity onCreate():

// schedule logout service at start
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    NetworkUtils.scheduleNextLogoutService(applicationContext)
}

So, now alarm service is rescheduled (replaced) unlimited times with the same requestCode (0):

  1. on BOOT_COMPLETE (on most devices)
  2. on app launch;
  3. on every service run;

LogoutService has been rewritten, using minimal dependencies from app (only api service and database instances). Service now uses CompletableJob instead of Job, so when I receive success from endpoint and erase local data, I can inform system that service has finished it's work:

private suspend fun logoutUser() {
        Logger.d("Running LogoutService logoutUser()")
        withContext(coroutineContext) {
            try {
                val UserPin = localDatabase?.UserDao?.getUser()?.pinCode
                Logger.d("Logging out User with pin $UserPin")
                if (UserPin != null) {
                    val call = mXApi?.apiService?.logoutUser(UserPin)
                    when (val result = XApi.getResult(call!!)) {
                        is Result.Success -> {
                            Logger.d("Success: logged out User ${result.data}")
                            var rowsDeleted = 0
                            rowsDeleted += localDatabase?.settingsDao?.delete()!!
                            rowsDeleted += localDatabase?.UserDao?.deleteUser()!!
                            rowsDeleted += localDatabase?.UserDao?.deleteUserCompany()!!
                            showLogoutStatusBarNotification()
                            sendRestartBroadcastToRunningApp()
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                                NetworkUtils.scheduleNextLogoutService(applicationContext)
                            }
                            coroutineJob.complete()
                            stopSelf()
                        }
                        is Result.NetworkFailure -> coroutineJob.cancel("network failure")
                        is Result.ApiFailure -> coroutineJob.cancel("api failure")
                        Result.Loading -> Logger.i("LogoutService job is loading")
                    }
                } else {
                    println("Cancelling coroutineJob")
                    coroutineJob.cancel()
                }
            } catch (e: Throwable) {
                Logger.e("LogoutService error ${e.message!!}")
                coroutineJob.cancel()
            }
        }
    }

    override fun onCreate() {
        ToastUtils.updateWarning(this, getString(R.string.you_being_auto_logout))
        mXApi = XApi
        localDatabase = LocalDatabase.getInstance(this)
        this.launch {
            logoutUser()
        }
    }

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...