diff --git a/android/src/main/java/com/frontegg/android/FronteggApp.kt b/android/src/main/java/com/frontegg/android/FronteggApp.kt index ec53a44..23a568c 100644 --- a/android/src/main/java/com/frontegg/android/FronteggApp.kt +++ b/android/src/main/java/com/frontegg/android/FronteggApp.kt @@ -12,6 +12,7 @@ import com.frontegg.android.exceptions.FronteggException import com.frontegg.android.exceptions.FronteggException.Companion.FRONTEGG_APP_MUST_BE_INITIALIZED import com.frontegg.android.regions.RegionConfig import com.frontegg.android.services.* +import java.time.Instant class FronteggApp private constructor( val context: Context, @@ -34,7 +35,9 @@ class FronteggApp private constructor( val auth: FronteggAuth = FronteggAuth(baseUrl, clientId, applicationId, credentialManager, regions, selectedRegion) val packageName: String = context.packageName + var appInForeground = true + var lastJobStart:Long = Instant.now().toEpochMilli(); companion object { @SuppressLint("StaticFieldLeak") @@ -129,9 +132,12 @@ class FronteggApp private constructor( when (event) { Lifecycle.Event.ON_STOP -> { Log.d(TAG, "ON_STOP") + getInstance().appInForeground = false + getInstance().auth.refreshTokenWhenNeeded() } Lifecycle.Event.ON_START -> { Log.d(TAG, "ON_START") + getInstance().appInForeground = true getInstance().auth.refreshTokenWhenNeeded() } else -> {} diff --git a/android/src/main/java/com/frontegg/android/FronteggAuth.kt b/android/src/main/java/com/frontegg/android/FronteggAuth.kt index fdafac9..cafb0c5 100644 --- a/android/src/main/java/com/frontegg/android/FronteggAuth.kt +++ b/android/src/main/java/com/frontegg/android/FronteggAuth.kt @@ -27,6 +27,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.time.Instant +import java.util.Timer +import java.util.TimerTask +import kotlin.concurrent.schedule @SuppressLint("CheckResult") @@ -59,9 +62,11 @@ class FronteggAuth( val isLoading: ObservableValue = ObservableValue(true) val initializing: ObservableValue = ObservableValue(true) val showLoader: ObservableValue = ObservableValue(true) + val refreshingToken: ObservableValue = ObservableValue(false) var pendingAppLink: String? = null val isMultiRegion: Boolean = regions.isNotEmpty() var refreshTokenJob: JobInfo? = null; + var timerTask: TimerTask? = null; private var _api: Api? = null init { @@ -113,42 +118,47 @@ class FronteggAuth( if (!refreshTokenIfNeeded()) { accessToken.value = null refreshToken.value = null - isLoading.value = false initializing.value = false + isLoading.value = false } } else { - isLoading.value = false initializing.value = false + isLoading.value = false } } } fun sendRefreshToken(): Boolean { val refreshToken = this.refreshToken.value ?: return false - val data = api.refreshToken(refreshToken) - return if (data != null) { - setCredentials(data.access_token, data.refresh_token) - true - } else { - Log.e(TAG, "Failed to refresh token, data = null") - false + this.refreshingToken.value = true + try { + + val data = api.refreshToken(refreshToken) + return if (data != null) { + setCredentials(data.access_token, data.refresh_token) + true + } else { + Log.e(TAG, "Failed to refresh token, data = null") + false + } + } finally { + this.refreshingToken.value = false } } fun refreshTokenWhenNeeded() { val accessToken = this.accessToken.value - if(this.refreshToken.value == null){ + if (this.refreshToken.value == null) { return } - if (refreshTokenJob != null) { - cancelLastTimer() - } - if(accessToken == null){ - // when we have valid refreshToken without accessToken => failed to refresh in background + cancelLastTimer() + + if (accessToken == null) { + // when we have valid refreshToken without accessToken => failed to refresh in background GlobalScope.launch(Dispatchers.IO) { sendRefreshToken() } @@ -220,24 +230,46 @@ class FronteggAuth( private fun cancelLastTimer() { Log.d(TAG, "Cancel Last Timer") - val context = FronteggApp.getInstance().context - val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler - jobScheduler.cancel(JOB_ID) - this.refreshTokenJob = null + if(timerTask!= null){ + timerTask?.cancel() + timerTask = null + } + if(refreshTokenJob!= null) { + val context = FronteggApp.getInstance().context + val jobScheduler = + context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler + jobScheduler.cancel(JOB_ID) + this.refreshTokenJob = null + } } - private fun scheduleTimer(offset: Long) { - Log.d(TAG, "Schedule Timer") - val context = FronteggApp.getInstance().context - val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler - // Schedule the job - val jobInfo = JobInfo.Builder( - JOB_ID, ComponentName(context, RefreshTokenService::class.java) - ).setMinimumLatency(offset) // Schedule the job to run after the offset - .setOverrideDeadline(offset + 10000) // Add a buffer to the deadline - .build() - this.refreshTokenJob = jobInfo - jobScheduler.schedule(jobInfo) + fun scheduleTimer(offset: Long) { + FronteggApp.getInstance().lastJobStart = Instant.now().toEpochMilli() + if(FronteggApp.getInstance().appInForeground){ + Log.d(TAG, "[foreground] Start Timer task (${offset} ms)") + + this.timerTask = Timer().schedule(offset) { + Log.d(TAG, "[foreground] Job started, (${Instant.now().toEpochMilli()-FronteggApp.getInstance().lastJobStart} ms)") + refreshTokenIfNeeded() + } + + }else { + Log.d(TAG, "[background] Start Job Scheduler task (${offset} ms)") + val context = FronteggApp.getInstance().context + val jobScheduler = + context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler + // Schedule the job + val jobInfo = JobInfo.Builder( + JOB_ID, ComponentName(context, RefreshTokenService::class.java) + ) + .setMinimumLatency(offset / 2) // Schedule the job to run after the offset + .setOverrideDeadline(offset) // Add a buffer to the deadline + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // Require network + .setBackoffCriteria(10000, JobInfo.BACKOFF_POLICY_LINEAR) + .build() + this.refreshTokenJob = jobInfo + jobScheduler.schedule(jobInfo) + } } fun handleHostedLoginCallback( @@ -357,8 +389,17 @@ class FronteggAuth( } } - private fun calculateTimerOffset(exp: Long): Long { + private fun calculateTimerOffset(expirationTime: Long): Long { val now: Long = Instant.now().toEpochMilli() - return (((exp * 1000) - now) * 0.80).toLong() + val remainingTime = (expirationTime * 1000) - now + + val minRefreshWindow = 20000 // minimum 20 seconds before exp + val adaptiveRefreshTime = remainingTime * 0.8 // 80% of remaining time + + return if (remainingTime > minRefreshWindow) { + adaptiveRefreshTime.toLong() + } else { + (remainingTime - minRefreshWindow).coerceAtLeast(0) + } } } diff --git a/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt b/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt index 14ceb8d..5463c7a 100644 --- a/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt +++ b/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt @@ -38,11 +38,13 @@ class FronteggWebClient(val context: Context) : WebViewClient() { private val TAG = FronteggWebClient::class.java.simpleName } + var webViewStatusCode: Int = 200 + var lastErrorResponse: WebResourceResponse? = null + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) Log.d(TAG, "onPageStarted $url") FronteggAuth.instance.isLoading.value = true - } override fun onPageFinished(view: WebView?, url: String?) { @@ -65,16 +67,26 @@ class FronteggWebClient(val context: Context) : WebViewClient() { return } - val fronteggApp = FronteggApp.getInstance() - val nativeModuleFunctions = JSONObject() - nativeModuleFunctions.put("loginWithSocialLogin", fronteggApp.handleLoginWithSocialLogin) - nativeModuleFunctions.put("loginWithSSO", fronteggApp.handleLoginWithSSO) - nativeModuleFunctions.put( - "shouldPromptSocialLoginConsent", - fronteggApp.shouldPromptSocialLoginConsent - ) - val jsObject = nativeModuleFunctions.toString() - view?.evaluateJavascript("window.FronteggNativeBridgeFunctions = ${jsObject};", null) + + if (webViewStatusCode >= 400) { + checkIfFronteggError(view, url, lastErrorResponse) + webViewStatusCode = 200 + lastErrorResponse = null + } else { + val fronteggApp = FronteggApp.getInstance() + val nativeModuleFunctions = JSONObject() + nativeModuleFunctions.put( + "loginWithSocialLogin", + fronteggApp.handleLoginWithSocialLogin + ) + nativeModuleFunctions.put("loginWithSSO", fronteggApp.handleLoginWithSSO) + nativeModuleFunctions.put( + "shouldPromptSocialLoginConsent", + fronteggApp.shouldPromptSocialLoginConsent + ) + val jsObject = nativeModuleFunctions.toString() + view?.evaluateJavascript("window.FronteggNativeBridgeFunctions = ${jsObject};", null) + } } @@ -114,25 +126,49 @@ class FronteggWebClient(val context: Context) : WebViewClient() { super.onReceivedError(view, request, error) } - private fun checkIfFronteggError(view: WebView?, url: String?, status: Int? = null) { + private fun checkIfFronteggError( + view: WebView?, + url: String?, + errorResponse: WebResourceResponse? + ) { if (view == null || url == null) { return } + val status = errorResponse?.statusCode + + if (url.startsWith("${FronteggAuth.instance.baseUrl}/oauth/authorize")) { + val reloadScript = + "setTimeout(()=>window.location.href=\"${url.replace("\"", "\\\"")}\", 4000)" + val jsCode = "(function(){\n" + + " var script = document.createElement('script');\n" + + " script.innerHTML=`$reloadScript`;" + + " document.body.appendChild(script)\n" + + " })()" + view.evaluateJavascript(jsCode, null) + FronteggAuth.instance.isLoading.value = false + return + } + + view.evaluateJavascript("document.body.innerText") { result -> try { var text = result if (text == null) { return@evaluateJavascript } + + var error = "" + var json = JsonParser.parseString(text) while (!json.isJsonObject) { text = json.asString json = JsonParser.parseString(text) } - val error = json.asJsonObject.get("errors").asJsonArray.map { + error = json.asJsonObject.get("errors").asJsonArray.map { it.asString }.joinToString("\n") + Log.e(TAG, "Frontegg ERROR: $error") val htmlError = Html.escapeHtml(error) @@ -153,12 +189,22 @@ class FronteggWebClient(val context: Context) : WebViewClient() { request: WebResourceRequest?, errorResponse: WebResourceResponse? ) { - if (view!!.url == request?.url.toString()) { - Log.d(TAG, "onReceivedHttpError: Direct load url, ${request?.url?.path}") + + if (view == null || request == null) { + return + } + val requestUrl = request.url + val authorizeUrlPrefix = "${FronteggAuth.instance.baseUrl}/oauth/authorize" + if (view.url == requestUrl.toString() + || requestUrl.toString().startsWith(authorizeUrlPrefix) + ) { + Log.d(TAG, "onReceivedHttpError: Direct load url, ${requestUrl.path}") + webViewStatusCode = errorResponse?.statusCode ?: 200 + lastErrorResponse = errorResponse } else { Log.d(TAG, "onReceivedHttpError: HTTP api call, ${request?.url?.path}") } - checkIfFronteggError(view, request?.url.toString(), errorResponse?.statusCode) + super.onReceivedHttpError(view, request, errorResponse) } diff --git a/android/src/main/java/com/frontegg/android/services/RefreshTokenService.kt b/android/src/main/java/com/frontegg/android/services/RefreshTokenService.kt index 82857d5..3732b5a 100644 --- a/android/src/main/java/com/frontegg/android/services/RefreshTokenService.kt +++ b/android/src/main/java/com/frontegg/android/services/RefreshTokenService.kt @@ -4,8 +4,10 @@ package com.frontegg.android.services import android.app.job.JobParameters import android.app.job.JobService import android.util.Log +import com.frontegg.android.FronteggApp import com.frontegg.android.FronteggAuth import java.net.SocketTimeoutException +import java.time.Instant class RefreshTokenService : JobService() { @@ -14,20 +16,13 @@ class RefreshTokenService : JobService() { } override fun onStartJob(params: JobParameters?): Boolean { - Log.d(TAG, "Job started") - - // Perform your background task here + Log.d(TAG, "Job started, (${Instant.now().toEpochMilli() - FronteggApp.getInstance().lastJobStart} ms)") performBackgroundTask(params) - - // Return true if the job is still running, false if it is completed - // This example returns false, indicating the job is finished return true } override fun onStopJob(params: JobParameters?): Boolean { Log.d(TAG, "Job stopped before completion") - - // Cleanup if necessary. Return true to reschedule the job if needed return false } @@ -45,7 +40,11 @@ class RefreshTokenService : JobService() { FronteggAuth.instance.accessToken.value = null FronteggAuth.instance.isLoading.value = true isError = true + if(e is SocketTimeoutException) { + FronteggAuth.instance.scheduleTimer(20000) + } } finally { + FronteggApp.getInstance().lastJobStart = Instant.now().toEpochMilli() // Notify the job manager that the job has been completed jobFinished(params, isError) }