Skip to content

Commit

Permalink
QuickEditor: Make sure the QE works well when the app get's killed du…
Browse files Browse the repository at this point in the history
…ring the OAuth flow (#541)

* Create GravatarOAuthActivity to handle OAuth flow

* Use the GravatarOAuthActivity in the QuickEditor's flow

* Make sure to not repeat the StartOAuth action in ViewModel when restored

* Adjust the demo app

* Generate quickeditor.api file

* Listen to onNewIntent in the OAuthPage for backwards-compatibility

* Update gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/oauth/OAuthPage.kt

Co-authored-by: Maxime Lumeau <[email protected]>

* Fix NPE when the browser creates a new instance of GravatarOAuthActivity

* Fixed lint issue introduced with new warning message.

* Remove enter transition for GravatarOAuthActivity

---------

Co-authored-by: Maxime Lumeau <[email protected]>
  • Loading branch information
AdamGrzybkowski and mlumeau authored Jan 22, 2025
1 parent 03d62bc commit e4974bc
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 64 deletions.
83 changes: 44 additions & 39 deletions demo-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<!-- <uses-permission android:name="android.permission.CAMERA" />-->
<!-- <uses-permission android:name="android.permission.CAMERA" />-->

<application
android:allowBackup="true"
Expand All @@ -19,59 +19,64 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Gravatar">
android:theme="@style/Theme.Gravatar"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="${DEMO_OAUTH_REDIRECT_URI_HOST}"
android:scheme="${DEMO_OAUTH_REDIRECT_URI_SCHEME}" />
</intent-filter>
</activity>
<activity
android:name=".ui.activity.QuickEditorTestActivity"
android:launchMode="singleTask"
android:exported="true"
android:label="Old good Activity"
android:theme="@style/Theme.Gravatar"
android:exported="true">
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
android:launchMode="singleTask"
android:theme="@style/Theme.Gravatar">
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->

<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <category android:name="android.intent.category.BROWSABLE" />-->

<!-- <data-->
<!-- android:host="${DEMO_OAUTH_REDIRECT_URI_HOST}"-->
<!-- android:scheme="${DEMO_OAUTH_REDIRECT_URI_SCHEME}" />-->
<!-- </intent-filter>-->
<!-- <data-->
<!-- android:host="${DEMO_OAUTH_REDIRECT_URI_HOST}"-->
<!-- android:scheme="${DEMO_OAUTH_REDIRECT_URI_SCHEME}" />-->
<!-- </intent-filter>-->
</activity>

<!-- Gravatar QE Activity -->
<!-- <activity-->
<!-- android:name="com.gravatar.quickeditor.ui.GravatarQuickEditorActivity"-->
<!-- tools:node="merge">-->
<!-- <activity-->
<!-- android:name="com.gravatar.quickeditor.ui.GravatarQuickEditorActivity"-->
<!-- tools:node="merge">-->

<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->

<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <category android:name="android.intent.category.BROWSABLE" />-->

<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <data-->
<!-- android:host="${DEMO_OAUTH_REDIRECT_URI_HOST}"-->
<!-- android:scheme="${DEMO_OAUTH_REDIRECT_URI_SCHEME}" />-->
<!-- </intent-filter>-->
<!-- </activity>-->

<activity
android:name="com.gravatar.quickeditor.ui.oauth.GravatarOAuthActivity"
tools:node="merge">

<intent-filter>
<action android:name="android.intent.action.VIEW" />

<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<!-- <data-->
<!-- android:host="${DEMO_OAUTH_REDIRECT_URI_HOST}"-->
<!-- android:scheme="${DEMO_OAUTH_REDIRECT_URI_SCHEME}" />-->
<!-- </intent-filter>-->
<!-- </activity>-->
<data
android:host="${DEMO_OAUTH_REDIRECT_URI_HOST}"
android:scheme="${DEMO_OAUTH_REDIRECT_URI_SCHEME}" />
</intent-filter>
</activity>


<!-- Lib activities -->
Expand All @@ -82,8 +87,8 @@
<provider
android:name=".DemoFileProvider"
android:authorities="${applicationId}.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
Expand Down
5 changes: 5 additions & 0 deletions gravatar-quickeditor/api/gravatar-quickeditor.api
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,11 @@ public final class com/gravatar/quickeditor/ui/oauth/ComposableSingletons$OAuthP
public final fun getLambda-2$gravatar_quickeditor_release ()Lkotlin/jvm/functions/Function2;
}

public final class com/gravatar/quickeditor/ui/oauth/GravatarOAuthActivity : androidx/appcompat/app/AppCompatActivity {
public static final field $stable I
public fun <init> ()V
}

public final class com/gravatar/quickeditor/ui/oauth/OAuthParams : android/os/Parcelable {
public static final field $stable I
public static final field CREATOR Landroid/os/Parcelable$Creator;
Expand Down
6 changes: 6 additions & 0 deletions gravatar-quickeditor/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
android:launchMode="singleTask"
android:theme="@style/GravatarQETheme.Transparent" />

<activity
android:name=".ui.oauth.GravatarOAuthActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/GravatarQETheme.Transparent" />

<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package com.gravatar.quickeditor.ui.oauth

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContract
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
import com.gravatar.quickeditor.ui.GravatarQuickEditorActivity
import com.gravatar.types.Email
import java.net.URLDecoder

/**
* Activity to handle OAuth authentication with Gravatar.
*
* This activity launches a custom tab for the user to authenticate with Gravatar.
* Once the authentication is complete, it retrieves the access token from the redirect URI
* and returns it as a result.
*
* It's used internally, but it's exposed as a public as it needs to be declared in your AndroidManifest.xml file
* with the proper deep linking setup.
*/
public class GravatarOAuthActivity : AppCompatActivity() {
private var oAuthStarted = false
private var clientId: String? = null
private var redirectUri: String? = null
private var email: String? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, 0, 0)
} else {
@Suppress("DEPRECATION")
overridePendingTransition(0, 0)
}

oAuthStarted = savedInstanceState?.getBoolean(OAUTH_STARTED_KEY) ?: false

clientId = intent.getStringExtra(CLIENT_ID_KEY)
redirectUri = intent.getStringExtra(REDIRECT_URI_KEY)
email = intent.getStringExtra(EMAIL_KEY)

if (clientId == null || redirectUri == null || email == null) {
setResult(
RESULT_OK,
Intent().apply {
putExtra(ACTIVITY_RESULT, RESULT_CANCELED)
},
)
finish()
return
}

addOnNewIntentListener { newIntent ->
val token = newIntent.data
?.encodedFragment
?.split("&")
?.associate {
val split = it.split("=")
split.first() to split.last()
}
?.get("access_token")
?.let { URLDecoder.decode(it, "UTF-8") }

if (token != null) {
val resultIntent = Intent().apply {
putExtra(ACTIVITY_RESULT, RESULT_TOKEN_RETRIEVED)
putExtra(TOKEN_KEY, token)
}

setResult(RESULT_OK, resultIntent)
} else {
setResult(
RESULT_OK,
Intent().apply {
putExtra(ACTIVITY_RESULT, RESULT_TOKEN_ERROR)
},
)
}
finish()
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)

outState.putBoolean(OAUTH_STARTED_KEY, oAuthStarted)
}

override fun onResume() {
super.onResume()

if (!oAuthStarted) {
launchCustomTab(
this,
clientId = clientId!!,
redirectUri = redirectUri!!,
email = Email(email!!),
)
oAuthStarted = true
} else {
setResult(
RESULT_OK,
Intent().apply {
putExtra(ACTIVITY_RESULT, RESULT_CANCELED)
},
)
finish()
}
}

private fun launchCustomTab(context: Context, clientId: String, redirectUri: String, email: Email) {
val customTabIntent: CustomTabsIntent = CustomTabsIntent.Builder()
.build()
customTabIntent.launchUrl(
context,
Uri.parse(WordPressOauth.buildUrl(clientId, redirectUri, email)),
)
}

internal companion object {
private const val CLIENT_ID_KEY = "client_id"
private const val REDIRECT_URI_KEY = "redirect_uri"
private const val EMAIL_KEY = "email"
private const val OAUTH_STARTED_KEY = "oauth_started"
internal const val TOKEN_KEY = "auth_token"

internal const val ACTIVITY_RESULT: String = "oAuthActivityResult"
internal const val RESULT_CANCELED: Int = 1000
internal const val RESULT_TOKEN_RETRIEVED: Int = 1001
internal const val RESULT_TOKEN_ERROR: Int = 1002

internal fun createIntent(context: Context, oAuthParams: OAuthParams, email: String): Intent {
return Intent(context, GravatarOAuthActivity::class.java).apply {
putExtra(CLIENT_ID_KEY, oAuthParams.clientId)
putExtra(REDIRECT_URI_KEY, oAuthParams.redirectUri)
putExtra(EMAIL_KEY, email)
}
}
}
}

/**
* Activity result contract to get the result from the [GravatarOAuthActivity].
*
* @see GravatarOAuthActivityParams
* @see GravatarOAuthResult
*/
internal class GravatarOAuthResultContract :
ActivityResultContract<GravatarOAuthActivityParams, GravatarOAuthResult>() {
override fun createIntent(context: Context, input: GravatarOAuthActivityParams): Intent {
return GravatarOAuthActivity.createIntent(
context = context,
oAuthParams = input.oAuthParams,
email = input.email,
)
}

override fun parseResult(resultCode: Int, intent: Intent?): GravatarOAuthResult {
return when (intent?.getIntExtra(GravatarOAuthActivity.ACTIVITY_RESULT, -1)) {
GravatarOAuthActivity.RESULT_TOKEN_RETRIEVED -> GravatarOAuthResult.TOKEN(
intent.getStringExtra(GravatarOAuthActivity.TOKEN_KEY)!!,
)

GravatarOAuthActivity.RESULT_TOKEN_ERROR -> GravatarOAuthResult.ERROR
else -> GravatarOAuthResult.DISMISSED
}
}
}

/**
* Parameters for the [GravatarOAuthActivity].
*/
internal class GravatarOAuthActivityParams(
val oAuthParams: OAuthParams,
val email: String,
)

/**
* Result enum for the [GravatarQuickEditorActivity].
*/
internal sealed class GravatarOAuthResult {
data class TOKEN(val token: String) : GravatarOAuthResult()

data object DISMISSED : GravatarOAuthResult()

data object ERROR : GravatarOAuthResult()
}
Loading

0 comments on commit e4974bc

Please sign in to comment.