Skip to content

Commit e4974bc

Browse files
QuickEditor: Make sure the QE works well when the app get's killed during 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]>
1 parent 03d62bc commit e4974bc

File tree

7 files changed

+314
-64
lines changed

7 files changed

+314
-64
lines changed

demo-app/src/main/AndroidManifest.xml

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
xmlns:tools="http://schemas.android.com/tools">
44

55
<uses-permission android:name="android.permission.INTERNET" />
6-
<!-- <uses-permission android:name="android.permission.CAMERA" />-->
6+
<!-- <uses-permission android:name="android.permission.CAMERA" />-->
77

88
<application
99
android:allowBackup="true"
@@ -19,59 +19,64 @@
1919
<activity
2020
android:name=".MainActivity"
2121
android:exported="true"
22-
android:launchMode="singleTask"
23-
android:windowSoftInputMode="adjustResize"
24-
android:theme="@style/Theme.Gravatar">
22+
android:theme="@style/Theme.Gravatar"
23+
android:windowSoftInputMode="adjustResize">
2524
<intent-filter>
2625
<action android:name="android.intent.action.MAIN" />
2726

2827
<category android:name="android.intent.category.LAUNCHER" />
2928
</intent-filter>
30-
<intent-filter>
31-
<action android:name="android.intent.action.VIEW" />
32-
33-
<category android:name="android.intent.category.DEFAULT" />
34-
<category android:name="android.intent.category.BROWSABLE" />
35-
36-
<data
37-
android:host="${DEMO_OAUTH_REDIRECT_URI_HOST}"
38-
android:scheme="${DEMO_OAUTH_REDIRECT_URI_SCHEME}" />
39-
</intent-filter>
4029
</activity>
4130
<activity
4231
android:name=".ui.activity.QuickEditorTestActivity"
43-
android:launchMode="singleTask"
32+
android:exported="true"
4433
android:label="Old good Activity"
45-
android:theme="@style/Theme.Gravatar"
46-
android:exported="true">
47-
<!-- <intent-filter>-->
48-
<!-- <action android:name="android.intent.action.VIEW" />-->
34+
android:launchMode="singleTask"
35+
android:theme="@style/Theme.Gravatar">
36+
<!-- <intent-filter>-->
37+
<!-- <action android:name="android.intent.action.VIEW" />-->
4938

50-
<!-- <category android:name="android.intent.category.DEFAULT" />-->
51-
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
39+
<!-- <category android:name="android.intent.category.DEFAULT" />-->
40+
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
5241

53-
<!-- <data-->
54-
<!-- android:host="${DEMO_OAUTH_REDIRECT_URI_HOST}"-->
55-
<!-- android:scheme="${DEMO_OAUTH_REDIRECT_URI_SCHEME}" />-->
56-
<!-- </intent-filter>-->
42+
<!-- <data-->
43+
<!-- android:host="${DEMO_OAUTH_REDIRECT_URI_HOST}"-->
44+
<!-- android:scheme="${DEMO_OAUTH_REDIRECT_URI_SCHEME}" />-->
45+
<!-- </intent-filter>-->
5746
</activity>
5847

5948
<!-- Gravatar QE Activity -->
60-
<!-- <activity-->
61-
<!-- android:name="com.gravatar.quickeditor.ui.GravatarQuickEditorActivity"-->
62-
<!-- tools:node="merge">-->
49+
<!-- <activity-->
50+
<!-- android:name="com.gravatar.quickeditor.ui.GravatarQuickEditorActivity"-->
51+
<!-- tools:node="merge">-->
52+
53+
<!-- <intent-filter>-->
54+
<!-- <action android:name="android.intent.action.VIEW" />-->
55+
56+
<!-- <category android:name="android.intent.category.DEFAULT" />-->
57+
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
6358

64-
<!-- <intent-filter>-->
65-
<!-- <action android:name="android.intent.action.VIEW" />-->
59+
<!-- <data-->
60+
<!-- android:host="${DEMO_OAUTH_REDIRECT_URI_HOST}"-->
61+
<!-- android:scheme="${DEMO_OAUTH_REDIRECT_URI_SCHEME}" />-->
62+
<!-- </intent-filter>-->
63+
<!-- </activity>-->
64+
65+
<activity
66+
android:name="com.gravatar.quickeditor.ui.oauth.GravatarOAuthActivity"
67+
tools:node="merge">
68+
69+
<intent-filter>
70+
<action android:name="android.intent.action.VIEW" />
6671

67-
<!-- <category android:name="android.intent.category.DEFAULT" />-->
68-
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
72+
<category android:name="android.intent.category.DEFAULT" />
73+
<category android:name="android.intent.category.BROWSABLE" />
6974

70-
<!-- <data-->
71-
<!-- android:host="${DEMO_OAUTH_REDIRECT_URI_HOST}"-->
72-
<!-- android:scheme="${DEMO_OAUTH_REDIRECT_URI_SCHEME}" />-->
73-
<!-- </intent-filter>-->
74-
<!-- </activity>-->
75+
<data
76+
android:host="${DEMO_OAUTH_REDIRECT_URI_HOST}"
77+
android:scheme="${DEMO_OAUTH_REDIRECT_URI_SCHEME}" />
78+
</intent-filter>
79+
</activity>
7580

7681

7782
<!-- Lib activities -->
@@ -82,8 +87,8 @@
8287
<provider
8388
android:name=".DemoFileProvider"
8489
android:authorities="${applicationId}.fileprovider"
85-
android:grantUriPermissions="true"
86-
android:exported="false">
90+
android:exported="false"
91+
android:grantUriPermissions="true">
8792
<meta-data
8893
android:name="android.support.FILE_PROVIDER_PATHS"
8994
android:resource="@xml/filepaths" />

gravatar-quickeditor/api/gravatar-quickeditor.api

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,11 @@ public final class com/gravatar/quickeditor/ui/oauth/ComposableSingletons$OAuthP
451451
public final fun getLambda-2$gravatar_quickeditor_release ()Lkotlin/jvm/functions/Function2;
452452
}
453453

454+
public final class com/gravatar/quickeditor/ui/oauth/GravatarOAuthActivity : androidx/appcompat/app/AppCompatActivity {
455+
public static final field $stable I
456+
public fun <init> ()V
457+
}
458+
454459
public final class com/gravatar/quickeditor/ui/oauth/OAuthParams : android/os/Parcelable {
455460
public static final field $stable I
456461
public static final field CREATOR Landroid/os/Parcelable$Creator;

gravatar-quickeditor/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
android:launchMode="singleTask"
1616
android:theme="@style/GravatarQETheme.Transparent" />
1717

18+
<activity
19+
android:name=".ui.oauth.GravatarOAuthActivity"
20+
android:exported="true"
21+
android:launchMode="singleTask"
22+
android:theme="@style/GravatarQETheme.Transparent" />
23+
1824
<provider
1925
android:name="androidx.startup.InitializationProvider"
2026
android:authorities="${applicationId}.androidx-startup"
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package com.gravatar.quickeditor.ui.oauth
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import android.net.Uri
6+
import android.os.Build
7+
import android.os.Build.VERSION.SDK_INT
8+
import android.os.Bundle
9+
import androidx.activity.result.contract.ActivityResultContract
10+
import androidx.appcompat.app.AppCompatActivity
11+
import androidx.browser.customtabs.CustomTabsIntent
12+
import com.gravatar.quickeditor.ui.GravatarQuickEditorActivity
13+
import com.gravatar.types.Email
14+
import java.net.URLDecoder
15+
16+
/**
17+
* Activity to handle OAuth authentication with Gravatar.
18+
*
19+
* This activity launches a custom tab for the user to authenticate with Gravatar.
20+
* Once the authentication is complete, it retrieves the access token from the redirect URI
21+
* and returns it as a result.
22+
*
23+
* It's used internally, but it's exposed as a public as it needs to be declared in your AndroidManifest.xml file
24+
* with the proper deep linking setup.
25+
*/
26+
public class GravatarOAuthActivity : AppCompatActivity() {
27+
private var oAuthStarted = false
28+
private var clientId: String? = null
29+
private var redirectUri: String? = null
30+
private var email: String? = null
31+
32+
override fun onCreate(savedInstanceState: Bundle?) {
33+
super.onCreate(savedInstanceState)
34+
if (SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
35+
overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, 0, 0)
36+
} else {
37+
@Suppress("DEPRECATION")
38+
overridePendingTransition(0, 0)
39+
}
40+
41+
oAuthStarted = savedInstanceState?.getBoolean(OAUTH_STARTED_KEY) ?: false
42+
43+
clientId = intent.getStringExtra(CLIENT_ID_KEY)
44+
redirectUri = intent.getStringExtra(REDIRECT_URI_KEY)
45+
email = intent.getStringExtra(EMAIL_KEY)
46+
47+
if (clientId == null || redirectUri == null || email == null) {
48+
setResult(
49+
RESULT_OK,
50+
Intent().apply {
51+
putExtra(ACTIVITY_RESULT, RESULT_CANCELED)
52+
},
53+
)
54+
finish()
55+
return
56+
}
57+
58+
addOnNewIntentListener { newIntent ->
59+
val token = newIntent.data
60+
?.encodedFragment
61+
?.split("&")
62+
?.associate {
63+
val split = it.split("=")
64+
split.first() to split.last()
65+
}
66+
?.get("access_token")
67+
?.let { URLDecoder.decode(it, "UTF-8") }
68+
69+
if (token != null) {
70+
val resultIntent = Intent().apply {
71+
putExtra(ACTIVITY_RESULT, RESULT_TOKEN_RETRIEVED)
72+
putExtra(TOKEN_KEY, token)
73+
}
74+
75+
setResult(RESULT_OK, resultIntent)
76+
} else {
77+
setResult(
78+
RESULT_OK,
79+
Intent().apply {
80+
putExtra(ACTIVITY_RESULT, RESULT_TOKEN_ERROR)
81+
},
82+
)
83+
}
84+
finish()
85+
}
86+
}
87+
88+
override fun onSaveInstanceState(outState: Bundle) {
89+
super.onSaveInstanceState(outState)
90+
91+
outState.putBoolean(OAUTH_STARTED_KEY, oAuthStarted)
92+
}
93+
94+
override fun onResume() {
95+
super.onResume()
96+
97+
if (!oAuthStarted) {
98+
launchCustomTab(
99+
this,
100+
clientId = clientId!!,
101+
redirectUri = redirectUri!!,
102+
email = Email(email!!),
103+
)
104+
oAuthStarted = true
105+
} else {
106+
setResult(
107+
RESULT_OK,
108+
Intent().apply {
109+
putExtra(ACTIVITY_RESULT, RESULT_CANCELED)
110+
},
111+
)
112+
finish()
113+
}
114+
}
115+
116+
private fun launchCustomTab(context: Context, clientId: String, redirectUri: String, email: Email) {
117+
val customTabIntent: CustomTabsIntent = CustomTabsIntent.Builder()
118+
.build()
119+
customTabIntent.launchUrl(
120+
context,
121+
Uri.parse(WordPressOauth.buildUrl(clientId, redirectUri, email)),
122+
)
123+
}
124+
125+
internal companion object {
126+
private const val CLIENT_ID_KEY = "client_id"
127+
private const val REDIRECT_URI_KEY = "redirect_uri"
128+
private const val EMAIL_KEY = "email"
129+
private const val OAUTH_STARTED_KEY = "oauth_started"
130+
internal const val TOKEN_KEY = "auth_token"
131+
132+
internal const val ACTIVITY_RESULT: String = "oAuthActivityResult"
133+
internal const val RESULT_CANCELED: Int = 1000
134+
internal const val RESULT_TOKEN_RETRIEVED: Int = 1001
135+
internal const val RESULT_TOKEN_ERROR: Int = 1002
136+
137+
internal fun createIntent(context: Context, oAuthParams: OAuthParams, email: String): Intent {
138+
return Intent(context, GravatarOAuthActivity::class.java).apply {
139+
putExtra(CLIENT_ID_KEY, oAuthParams.clientId)
140+
putExtra(REDIRECT_URI_KEY, oAuthParams.redirectUri)
141+
putExtra(EMAIL_KEY, email)
142+
}
143+
}
144+
}
145+
}
146+
147+
/**
148+
* Activity result contract to get the result from the [GravatarOAuthActivity].
149+
*
150+
* @see GravatarOAuthActivityParams
151+
* @see GravatarOAuthResult
152+
*/
153+
internal class GravatarOAuthResultContract :
154+
ActivityResultContract<GravatarOAuthActivityParams, GravatarOAuthResult>() {
155+
override fun createIntent(context: Context, input: GravatarOAuthActivityParams): Intent {
156+
return GravatarOAuthActivity.createIntent(
157+
context = context,
158+
oAuthParams = input.oAuthParams,
159+
email = input.email,
160+
)
161+
}
162+
163+
override fun parseResult(resultCode: Int, intent: Intent?): GravatarOAuthResult {
164+
return when (intent?.getIntExtra(GravatarOAuthActivity.ACTIVITY_RESULT, -1)) {
165+
GravatarOAuthActivity.RESULT_TOKEN_RETRIEVED -> GravatarOAuthResult.TOKEN(
166+
intent.getStringExtra(GravatarOAuthActivity.TOKEN_KEY)!!,
167+
)
168+
169+
GravatarOAuthActivity.RESULT_TOKEN_ERROR -> GravatarOAuthResult.ERROR
170+
else -> GravatarOAuthResult.DISMISSED
171+
}
172+
}
173+
}
174+
175+
/**
176+
* Parameters for the [GravatarOAuthActivity].
177+
*/
178+
internal class GravatarOAuthActivityParams(
179+
val oAuthParams: OAuthParams,
180+
val email: String,
181+
)
182+
183+
/**
184+
* Result enum for the [GravatarQuickEditorActivity].
185+
*/
186+
internal sealed class GravatarOAuthResult {
187+
data class TOKEN(val token: String) : GravatarOAuthResult()
188+
189+
data object DISMISSED : GravatarOAuthResult()
190+
191+
data object ERROR : GravatarOAuthResult()
192+
}

0 commit comments

Comments
 (0)