diff --git a/.gitignore b/.gitignore index c9cccbed..3aa09dae 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ build/ docs/ docs-store/ +auth-lib/stdout +auth-sample/stdout diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bb6ec76..f056fac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ Change Log ========== +## Version 5.0.0 +- Bumped targetSdkVersion to 35 +- Bumped minSdkVersion to 21 +- Bumped AGP to 8.7.3 and Gradle to 8.11.1 +- Bumped Robolectric to 4.14.1 +- **Breaking Change**: The `RedirectUriReceiverActivity` intent-filter must now be declared in the consuming app's `AndroidManifest.xml`. The library no longer provides it automatically. See the [README](README.md#breaking-changes-in-spotify-auth-library-version-500) for migration instructions. ## Version 4.0.1 - Fix: Ensure auth progress is preserved across configuration changes, preventing cases where a headless activity remains open and blocks user input. diff --git a/README.md b/README.md index dbbe2907..b195817c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,32 @@ [![Maven Central](https://img.shields.io/maven-central/v/com.spotify.android/auth.svg)](https://search.maven.org/search?q=g:com.spotify.android) +# Breaking changes in Spotify Auth library version 5.0.0 + +The `RedirectUriReceiverActivity` intent-filter is no longer provided by the library. You must declare it in your app's `AndroidManifest.xml` with your redirect URI values: + +```xml + + + + + + + + +``` + +If you were previously using `manifestPlaceholders` for `redirectSchemeName`, `redirectHostName`, and `redirectPathPattern`, replace them with the values directly in the manifest. The `manifestPlaceholders` configuration in `build.gradle` is no longer needed. + +If your redirect URI uses `https://`, add `android:autoVerify="true"` to the `` to enable [Android App Links](https://developer.android.com/training/app-links) verification. + +If your app is missing this intent-filter, the library will crash at runtime with an `IllegalStateException` when the auth flow starts. + # Breaking changes in Spotify Auth library version 2.0.0 In this version we replaced use of WebView with [Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/) since Google and Facebook Login no longer support WebViews for authenticating users. @@ -24,31 +50,7 @@ repositories { } ``` -Since Spotify Auth library version 2.0.0 you also need to provide the scheme and host of the redirect URI that your app is using for authorizing in your app `build.gradle` file. -Below is an example of how this looks for [the auth sample project](auth-sample) using `spotify-sdk://auth` redirect URI: - -```gradle - defaultConfig { - manifestPlaceholders = [redirectSchemeName: "spotify-sdk", redirectHostName: "auth"] - ... - } -``` - -Since Spotify Auth library version 3.0.0 you also need to provide the path pattern of your redirect URI - -```gradle -defaultConfig { - manifestPlaceholders = [ - redirectSchemeName: "your-redirect-scheme", - redirectHostName: "your-redirect-host", - redirectPathPattern: "your/redirect/path/pattern" // New mandatory field - ] - ... -} -``` - -If you want to retain the previous behavior (accepting any path), use .* as the path pattern. -For more details, see the [Google documentation](https://developer.android.com/guide/topics/manifest/data-element#path). +You must also declare the `RedirectUriReceiverActivity` intent-filter in your app's `AndroidManifest.xml` (see [breaking changes in version 5.0.0](#breaking-changes-in-spotify-auth-library-version-500) above). To learn more see the [Authentication Guide](https://developer.spotify.com/technologies/spotify-android-sdk/android-sdk-authentication-guide/) and the [API reference](https://spotify.github.io/android-auth/auth-lib/docs/index.html). diff --git a/auth-lib/build.gradle.kts b/auth-lib/build.gradle.kts index 3a0842b6..40f314ba 100644 --- a/auth-lib/build.gradle.kts +++ b/auth-lib/build.gradle.kts @@ -35,21 +35,21 @@ plugins { } group = "com.spotify.android" -version = "4.0.1" +version = "5.0.0" val archivesBaseName = "auth" android { - compileSdk = 33 - buildToolsVersion = "33.0.0" + compileSdk = 35 + buildToolsVersion = "35.0.0" buildFeatures { buildConfig = true } defaultConfig { - minSdk = 16 - targetSdk = 33 + minSdk = 21 + targetSdk = 35 buildConfigField("String", "LIB_VERSION_NAME", "\"${project.version}\"") } @@ -82,6 +82,15 @@ android { } } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + lint { lintConfig = file("${project.rootDir}/config/lint.xml") quiet = false @@ -97,20 +106,7 @@ android { } } - val manifestPlaceholdersForTests = mapOf( - "redirectSchemeName" to "spotify-sdk", - "redirectHostName" to "auth", - "redirectPathPattern" to "/.*" - ) - namespace = "com.spotify.sdk.android.auth" - - unitTestVariants.configureEach { - mergedFlavor.manifestPlaceholders.putAll(manifestPlaceholdersForTests) - } - testVariants.configureEach { - mergedFlavor.manifestPlaceholders.putAll(manifestPlaceholdersForTests) - } } val kotlinVersion = rootProject.extra["kotlin_version"] as String @@ -121,7 +117,7 @@ dependencies { testImplementation("junit:junit:4.13.2") testImplementation("org.mockito:mockito-core:2.28.2") - testImplementation("org.robolectric:robolectric:4.11.1") + testImplementation("org.robolectric:robolectric:4.14.1") } /* diff --git a/auth-lib/src/auth/AndroidManifest.xml b/auth-lib/src/auth/AndroidManifest.xml index 80833331..f17c8f62 100644 --- a/auth-lib/src/auth/AndroidManifest.xml +++ b/auth-lib/src/auth/AndroidManifest.xml @@ -34,17 +34,7 @@ - - - - - - - + android:exported="true"/> diff --git a/auth-lib/src/auth/kotlin/com/spotify/sdk/android/auth/browser/CustomTabsSupportChecker.kt b/auth-lib/src/auth/kotlin/com/spotify/sdk/android/auth/browser/CustomTabsSupportChecker.kt index 8ca40890..dc1e3df1 100644 --- a/auth-lib/src/auth/kotlin/com/spotify/sdk/android/auth/browser/CustomTabsSupportChecker.kt +++ b/auth-lib/src/auth/kotlin/com/spotify/sdk/android/auth/browser/CustomTabsSupportChecker.kt @@ -3,9 +3,7 @@ package com.spotify.sdk.android.auth.browser import android.content.Context import android.content.Intent import android.content.pm.PackageManager -import android.content.pm.ResolveInfo import android.net.Uri -import android.text.TextUtils import android.util.Log import androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION import com.spotify.sdk.android.auth.AuthorizationRequest @@ -26,8 +24,7 @@ internal object CustomTabsSupportChecker { ) // CustomTabs seems to have problem with redirecting back the app after auth when URI has http/https scheme return if (!redirectUri.startsWith("http") && !redirectUri.startsWith("https") && - hasBrowserSupportForCustomTabs(packageSupportingCustomTabs) && - hasRedirectUriActivity(context.packageManager, redirectUri) + hasBrowserSupportForCustomTabs(packageSupportingCustomTabs) ) { packageSupportingCustomTabs } else { @@ -83,20 +80,4 @@ internal object CustomTabsSupportChecker { } } - private fun hasRedirectUriActivity(pm: PackageManager?, redirectUri: String): Boolean { - if (pm == null) { - return false - } - val intent = Intent().apply { - action = Intent.ACTION_VIEW - addCategory(Intent.CATEGORY_DEFAULT) - addCategory(Intent.CATEGORY_BROWSABLE) - data = Uri.parse(redirectUri) - } - val infoList = pm.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER) - - return infoList.any { info -> - info.activityInfo.name == RedirectUriReceiverActivity::class.java.name - } - } } diff --git a/auth-lib/src/main/AndroidManifest.xml b/auth-lib/src/main/AndroidManifest.xml index 22db0699..d7e7f530 100644 --- a/auth-lib/src/main/AndroidManifest.xml +++ b/auth-lib/src/main/AndroidManifest.xml @@ -35,6 +35,12 @@ + + + + + + diff --git a/auth-lib/src/main/kotlin/com/spotify/sdk/android/auth/AuthorizationClient.kt b/auth-lib/src/main/kotlin/com/spotify/sdk/android/auth/AuthorizationClient.kt index 56bd94aa..b7ef8fe3 100644 --- a/auth-lib/src/main/kotlin/com/spotify/sdk/android/auth/AuthorizationClient.kt +++ b/auth-lib/src/main/kotlin/com/spotify/sdk/android/auth/AuthorizationClient.kt @@ -219,6 +219,8 @@ class AuthorizationClient( if (authorizationPending) return authorizationPending = true + checkRedirectUriConfiguration(loginActivity.packageManager, request.redirectUri) + val processedRequest = validateAndConvertTokenRequest(request) for (authHandler in authorizationHandlers) { @@ -229,6 +231,7 @@ class AuthorizationClient( } } + fun cancel() { if (!authorizationPending) { return @@ -348,6 +351,25 @@ class AuthorizationClient( companion object { private const val TAG = "Spotify Auth Client" + // Hardcoded because RedirectUriReceiverActivity is in the auth flavor source set + private const val REDIRECT_ACTIVITY_NAME = + "com.spotify.sdk.android.auth.browser.RedirectUriReceiverActivity" + + private fun checkRedirectUriConfiguration(pm: PackageManager, redirectUri: String) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(redirectUri)).apply { + addCategory(Intent.CATEGORY_DEFAULT) + addCategory(Intent.CATEGORY_BROWSABLE) + } + val hasRedirectActivity = pm + .queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER) + .any { it.activityInfo.name == REDIRECT_ACTIVITY_NAME } + check(hasRedirectActivity) { + "No intent-filter found for RedirectUriReceiverActivity matching redirect URI " + + "'$redirectUri'. Add an intent-filter for RedirectUriReceiverActivity " + + "to your AndroidManifest.xml. See https://github.com/spotify/android-auth#breaking-changes-in-spotify-auth-library-version-500" + } + } + const val MARKET_VIEW_PATH = "market://" const val MARKET_SCHEME = "market" const val MARKET_PATH = "details" @@ -391,6 +413,7 @@ class AuthorizationClient( */ @JvmStatic fun openLoginInBrowser(contextActivity: Activity, request: AuthorizationRequest) { + checkRedirectUriConfiguration(contextActivity.packageManager, request.redirectUri) val launchBrowser = Intent(Intent.ACTION_VIEW, request.toUri()) contextActivity.startActivity(launchBrowser) } diff --git a/auth-lib/src/test/AndroidManifest.xml b/auth-lib/src/test/AndroidManifest.xml new file mode 100644 index 00000000..d8c6e748 --- /dev/null +++ b/auth-lib/src/test/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/auth-lib/src/test/java/com/spotify/sdk/android/auth/AuthorizationClientTest.java b/auth-lib/src/test/java/com/spotify/sdk/android/auth/AuthorizationClientTest.java index 0c95c1c5..cfd57c2f 100644 --- a/auth-lib/src/test/java/com/spotify/sdk/android/auth/AuthorizationClientTest.java +++ b/auth-lib/src/test/java/com/spotify/sdk/android/auth/AuthorizationClientTest.java @@ -32,6 +32,7 @@ import org.mockito.Mockito; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; +import static org.robolectric.Shadows.shadowOf; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; @@ -53,16 +54,13 @@ public void shouldLaunchIntentForPassedActivity() { AuthorizationResponse.Type.TOKEN, "to://me" ).build(); - Activity activity = mock(Activity.class); - - Mockito.doNothing().when(activity).startActivity(any(Intent.class)); - ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + Activity activity = Robolectric.buildActivity(Activity.class).create().get(); AuthorizationClient.openLoginInBrowser(activity, authorizationRequest); - verify(activity, times(1)).startActivity(captor.capture()); - assertEquals(Intent.ACTION_VIEW, captor.getValue().getAction()); - assertEquals(authorizationRequest.toUri().toString(), captor.getValue().getData().toString()); + Intent startedIntent = shadowOf(activity).getNextStartedActivity(); + assertEquals(Intent.ACTION_VIEW, startedIntent.getAction()); + assertEquals(authorizationRequest.toUri().toString(), startedIntent.getData().toString()); } @Test(expected = NullPointerException.class) diff --git a/auth-sample/build.gradle.kts b/auth-sample/build.gradle.kts index 86d48675..14ce1170 100644 --- a/auth-sample/build.gradle.kts +++ b/auth-sample/build.gradle.kts @@ -26,8 +26,8 @@ plugins { } android { - compileSdk = 33 - buildToolsVersion = "33.0.0" + compileSdk = 35 + buildToolsVersion = "35.0.0" signingConfigs { getByName("debug") { @@ -40,15 +40,11 @@ android { defaultConfig { applicationId = "com.spotify.sdk.android.authentication.sample" - minSdk = 16 - targetSdk = 33 + minSdk = 21 + targetSdk = 35 versionCode = 1 versionName = "1.0" - manifestPlaceholders["redirectSchemeName"] = "spotify-sdk" - manifestPlaceholders["redirectHostName"] = "auth" - manifestPlaceholders["redirectPathPattern"] = ".*" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" // Specify which auth-lib flavor to use @@ -66,6 +62,15 @@ android { } } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + lint { lintConfig = file("${project.rootDir}/config/lint.xml") quiet = false diff --git a/auth-sample/src/main/AndroidManifest.xml b/auth-sample/src/main/AndroidManifest.xml index 36fc44bd..56202d5c 100644 --- a/auth-sample/src/main/AndroidManifest.xml +++ b/auth-sample/src/main/AndroidManifest.xml @@ -36,6 +36,20 @@ + + + + + + + + + diff --git a/build.gradle.kts b/build.gradle.kts index 0e81e505..435a28ca 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,7 +27,7 @@ buildscript { google() } dependencies { - classpath("com.android.tools.build:gradle:7.4.2") + classpath("com.android.tools.build:gradle:8.7.3") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8049c684..81aa1c04 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists