Skip to content

Commit eb390c6

Browse files
Merge pull request #109 from antonholmberg/fix-login-activity-not-canceling-when-recreated
Ensure auth progress is stored on config changes
2 parents 82d1b6f + bda83de commit eb390c6

File tree

3 files changed

+129
-13
lines changed

3 files changed

+129
-13
lines changed

auth-lib/src/main/kotlin/com/spotify/sdk/android/auth/AuthorizationClient.kt

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -297,15 +297,13 @@ class AuthorizationClient(
297297
authHandler?.stop()
298298
}
299299

300-
fun notifyInCaseUserCanceledAuth() {
301-
if (currentHandler?.isAuthInProgress() == true) {
302-
Log.i(TAG, "Spotify auth response: User cancelled")
303-
val response = AuthorizationResponse.Builder()
304-
.setType(AuthorizationResponse.Type.CANCELLED)
305-
.build()
306-
complete(response)
307-
}
308-
}
300+
/**
301+
* Returns true when a handler exists but has not yet entered the auth-in-progress state
302+
* (e.g. Custom Tabs service binding is still in progress). In this case cancellation
303+
* should be suppressed because the user hasn't actually seen the auth UI yet.
304+
*/
305+
fun hasHandlerWithPendingAuth(): Boolean =
306+
currentHandler != null && currentHandler?.isAuthInProgress() != true
309307

310308
fun clearAuthInProgress() {
311309
if (currentHandler != null) {

auth-lib/src/main/kotlin/com/spotify/sdk/android/auth/LoginActivity.kt

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,15 @@ class LoginActivity : Activity(), AuthorizationClient.AuthorizationClientListene
4242
private val authorizationClient = AuthorizationClient(this)
4343
private val executorService: ExecutorService = Executors.newSingleThreadExecutor()
4444
private val mainHandler = Handler(Looper.getMainLooper())
45+
private var authInProgress = false
4546

4647
override fun onNewIntent(intent: Intent) {
4748
val originalRequest = getRequestFromIntent()
4849
super.onNewIntent(intent)
4950
val responseUri = intent.data
5051

52+
authInProgress = false
53+
5154
// Clear auth-in-progress state to prevent onResume from thinking user canceled
5255
if (responseUri != null) {
5356
authorizationClient.clearAuthInProgress()
@@ -96,20 +99,40 @@ class LoginActivity : Activity(), AuthorizationClient.AuthorizationClientListene
9699
} else if (savedInstanceState == null) {
97100
Log.d(TAG, String.format("Spotify Auth starting with the request [%s]", request.toUri().toString()))
98101
authorizationClient.authorize(request)
102+
authInProgress = true
103+
} else {
104+
authInProgress = savedInstanceState.getBoolean(KEY_AUTH_IN_PROGRESS, false)
105+
if (authInProgress) {
106+
val responseUri = intent.data
107+
if (responseUri != null) {
108+
authInProgress = false
109+
authorizationClient.clearAuthInProgress()
110+
val response = AuthorizationResponse.fromUri(responseUri)
111+
authorizationClient.complete(response)
112+
}
113+
}
99114
}
100115
}
101116

117+
override fun onSaveInstanceState(outState: Bundle) {
118+
super.onSaveInstanceState(outState)
119+
outState.putBoolean(KEY_AUTH_IN_PROGRESS, authInProgress)
120+
}
121+
102122
private fun getRequestFromIntent(): AuthorizationRequest? {
103123
val requestBundle = intent.getBundleExtra(EXTRA_AUTH_REQUEST) ?: return null
104124
return requestBundle.getParcelable(REQUEST_KEY)
105125
}
106126

107127
override fun onResume() {
108128
super.onResume()
109-
// onResume is called (except other cases) in the case
110-
// of browser based auth flow when user pressed back/closed the Custom Tab and
111-
// LoginActivity came to the foreground again.
112-
authorizationClient.notifyInCaseUserCanceledAuth()
129+
if (authInProgress && !authorizationClient.hasHandlerWithPendingAuth()) {
130+
authInProgress = false
131+
val response = AuthorizationResponse.Builder()
132+
.setType(AuthorizationResponse.Type.CANCELLED)
133+
.build()
134+
authorizationClient.complete(response)
135+
}
113136
}
114137

115138
override fun onDestroy() {
@@ -122,6 +145,7 @@ class LoginActivity : Activity(), AuthorizationClient.AuthorizationClientListene
122145
@Deprecated("Deprecated in Java")
123146
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
124147
super.onActivityResult(requestCode, resultCode, intent)
148+
authInProgress = false
125149
if (requestCode == REQUEST_CODE) {
126150
val response = AuthorizationResponse.Builder()
127151

@@ -290,6 +314,8 @@ class LoginActivity : Activity(), AuthorizationClient.AuthorizationClientListene
290314
}
291315

292316
companion object {
317+
private const val KEY_AUTH_IN_PROGRESS = "KEY_AUTH_IN_PROGRESS"
318+
293319
const val EXTRA_REPLY = "REPLY"
294320
const val EXTRA_ERROR = "ERROR"
295321

auth-lib/src/testAuth/java/com/spotify/sdk/android/auth/LoginActivityAuthTest.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import android.app.Activity;
3131
import android.content.Intent;
32+
import android.net.Uri;
3233
import android.os.Bundle;
3334

3435
import com.spotify.sdk.android.auth.AuthorizationRequest;
@@ -126,4 +127,95 @@ public void shouldReturnResultOkForTechnicalErrors() {
126127
assertCompletion(setup, response, Activity.RESULT_OK);
127128
}
128129

130+
@Test
131+
public void shouldDetectCancellationAfterRecreationWithoutResponse() {
132+
Activity context = Robolectric.buildActivity(Activity.class).create().get();
133+
134+
PKCEInformation pkceInfo = PKCEInformation.sha256("test_verifier", "test_challenge");
135+
AuthorizationRequest request = new AuthorizationRequest.Builder(
136+
"test", AuthorizationResponse.Type.TOKEN, "test://test")
137+
.setPkceInformation(pkceInfo)
138+
.build();
139+
140+
Bundle requestBundle = new Bundle();
141+
requestBundle.putParcelable(LoginActivity.REQUEST_KEY, request);
142+
143+
Intent intent = new Intent(context, LoginActivity.class);
144+
intent.putExtra(LoginActivity.EXTRA_AUTH_REQUEST, requestBundle);
145+
146+
// Create and initialize the first activity (triggers authorize, sets authInProgress=true)
147+
ActivityController<LoginActivity> controller = buildActivity(LoginActivity.class, intent);
148+
shadowOf(controller.get()).setCallingActivity(context.getComponentName());
149+
controller.create();
150+
151+
// Save instance state before "destruction"
152+
Bundle savedState = new Bundle();
153+
controller.saveInstanceState(savedState);
154+
155+
// Simulate recreation: new activity with same intent but no response URI
156+
Intent recreatedIntent = new Intent(context, LoginActivity.class);
157+
recreatedIntent.putExtra(LoginActivity.EXTRA_AUTH_REQUEST, requestBundle);
158+
159+
ActivityController<LoginActivity> controller2 = buildActivity(LoginActivity.class, recreatedIntent);
160+
LoginActivity recreatedActivity = controller2.get();
161+
ShadowActivity recreatedShadow = shadowOf(recreatedActivity);
162+
recreatedShadow.setCallingActivity(context.getComponentName());
163+
controller2.create(savedState).start().resume();
164+
165+
// onResume should detect authInProgress=true with no handler and deliver CANCELLED
166+
assertTrue(recreatedActivity.isFinishing());
167+
assertEquals(Activity.RESULT_CANCELED, recreatedShadow.getResultCode());
168+
AuthorizationResponse response = recreatedShadow.getResultIntent()
169+
.getBundleExtra(LoginActivity.EXTRA_AUTH_RESPONSE)
170+
.getParcelable(LoginActivity.RESPONSE_KEY);
171+
assertEquals(AuthorizationResponse.Type.CANCELLED, response.getType());
172+
}
173+
174+
@Test
175+
public void shouldProcessResponseInIntentDataAfterRecreation() {
176+
Activity context = Robolectric.buildActivity(Activity.class).create().get();
177+
178+
PKCEInformation pkceInfo = PKCEInformation.sha256("test_verifier", "test_challenge");
179+
AuthorizationRequest request = new AuthorizationRequest.Builder(
180+
"test", AuthorizationResponse.Type.TOKEN, "test://test")
181+
.setPkceInformation(pkceInfo)
182+
.build();
183+
184+
Bundle requestBundle = new Bundle();
185+
requestBundle.putParcelable(LoginActivity.REQUEST_KEY, request);
186+
187+
Intent intent = new Intent(context, LoginActivity.class);
188+
intent.putExtra(LoginActivity.EXTRA_AUTH_REQUEST, requestBundle);
189+
190+
// Create and initialize the first activity
191+
ActivityController<LoginActivity> controller = buildActivity(LoginActivity.class, intent);
192+
shadowOf(controller.get()).setCallingActivity(context.getComponentName());
193+
controller.create();
194+
195+
// Save instance state before "destruction"
196+
Bundle savedState = new Bundle();
197+
controller.saveInstanceState(savedState);
198+
199+
// Simulate recreation with a response URI in the intent
200+
// (redirect arrived while activity was destroyed)
201+
Intent recreatedIntent = new Intent(context, LoginActivity.class);
202+
recreatedIntent.putExtra(LoginActivity.EXTRA_AUTH_REQUEST, requestBundle);
203+
recreatedIntent.setData(Uri.parse("test://test?code=test_code"));
204+
205+
ActivityController<LoginActivity> controller2 = buildActivity(LoginActivity.class, recreatedIntent);
206+
LoginActivity recreatedActivity = controller2.get();
207+
ShadowActivity recreatedShadow = shadowOf(recreatedActivity);
208+
recreatedShadow.setCallingActivity(context.getComponentName());
209+
controller2.create(savedState);
210+
211+
// Response should be processed in onCreate, activity should finish with RESULT_OK
212+
assertTrue(recreatedActivity.isFinishing());
213+
assertEquals(Activity.RESULT_OK, recreatedShadow.getResultCode());
214+
AuthorizationResponse response = recreatedShadow.getResultIntent()
215+
.getBundleExtra(LoginActivity.EXTRA_AUTH_RESPONSE)
216+
.getParcelable(LoginActivity.RESPONSE_KEY);
217+
assertEquals(AuthorizationResponse.Type.CODE, response.getType());
218+
assertEquals("test_code", response.getCode());
219+
}
220+
129221
}

0 commit comments

Comments
 (0)