Skip to content

Commit 02aec1a

Browse files
committed
add a default credential store
Signed-off-by: Adam Ratzman <[email protected]>
1 parent ec53afa commit 02aec1a

File tree

1 file changed

+164
-0
lines changed
  • src/androidMain/kotlin/com/adamratzman/spotify/auth

1 file changed

+164
-0
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package com.adamratzman.spotify.auth
2+
3+
import android.annotation.SuppressLint
4+
import android.app.Activity
5+
import android.content.Context
6+
import android.content.Intent
7+
import android.content.SharedPreferences
8+
import android.os.Build
9+
import androidx.annotation.RequiresApi
10+
import androidx.security.crypto.EncryptedSharedPreferences
11+
import androidx.security.crypto.MasterKeys
12+
import com.adamratzman.spotify.SpotifyApiOptions
13+
import com.adamratzman.spotify.SpotifyException
14+
import com.adamratzman.spotify.SpotifyImplicitGrantApi
15+
import com.adamratzman.spotify.auth.SpotifyDefaultAuthHelper.activityBack
16+
import com.adamratzman.spotify.models.Token
17+
import com.adamratzman.spotify.spotifyImplicitGrantApi
18+
19+
// Starting login activity
20+
21+
/**
22+
* Start Spotify login activity within an existing activity.
23+
*/
24+
public inline fun <reified T : AbstractSpotifyLoginActivity> Activity.startSpotifyLoginActivity() {
25+
startSpotifyLoginActivity(T::class.java)
26+
}
27+
28+
/**
29+
* Start Spotify login activity within an existing activity.
30+
*
31+
* @param spotifyLoginImplementationClass Your implementation of [AbstractSpotifyLoginActivity], defining what to do on Spotify login
32+
*/
33+
public fun <T : AbstractSpotifyLoginActivity> Activity.startSpotifyLoginActivity(spotifyLoginImplementationClass: Class<T>) {
34+
startActivity(Intent(this, spotifyLoginImplementationClass))
35+
}
36+
37+
38+
/**
39+
* Basic authentication guard - verifies that the user is logged in to Spotify and uses [SpotifyDefaultAuthHelper] to
40+
* handle re-authentication and redirection back to the activity.
41+
*
42+
* Note: this should only be used for small applications.
43+
*
44+
* @param spotifyLoginImplementationClass Your implementation of [AbstractSpotifyLoginActivity], defining what to do on Spotify login
45+
* @param classBackTo The activity to return to if re-authentication is necessary
46+
* @block The code block to execute
47+
*/
48+
public fun <T> Activity.guardValidSpotifyApi(
49+
spotifyLoginImplementationClass: Class<out AbstractSpotifyLoginActivity>,
50+
classBackTo: Class<out Activity>? = null,
51+
block: () -> T
52+
): T? {
53+
return try {
54+
block()
55+
} catch (e: SpotifyException.ReAuthenticationNeededException) {
56+
activityBack = classBackTo
57+
startSpotifyLoginActivity(spotifyLoginImplementationClass)
58+
null
59+
}
60+
}
61+
62+
/**
63+
* Default authentiction helper for Android. Contains static variables useful in authentication.
64+
*
65+
*/
66+
public object SpotifyDefaultAuthHelper {
67+
/**
68+
* The activity to return to if re-authentication is necessary. Null except during authentication when using [guardValidSpotifyApi]
69+
*/
70+
public var activityBack: Class<out Activity>? = null
71+
}
72+
73+
/**
74+
* Provided credential store for holding current Spotify token credentials, allowing you to easily store and retrieve
75+
* Spotify tokens. Recommended in most use-cases.
76+
*
77+
* @param clientId The client id associated with your application
78+
* @param applicationContext The application context - you can obtain this by storing your application context statically (such as with a companion object)
79+
*
80+
*/
81+
@RequiresApi(Build.VERSION_CODES.M)
82+
public class SpotifyDefaultCredentialStore constructor(private val clientId: String, applicationContext: Context) {
83+
public companion object {
84+
/**
85+
* The key used with spotify token expiry in [EncryptedSharedPreferences]
86+
*/
87+
public const val SpotifyTokenExpiryKey: String = "spotifyTokenExpiry"
88+
89+
/**
90+
* The key used with spotify access token in [EncryptedSharedPreferences]
91+
*/
92+
public const val SpotifyAccessTokenKey: String = "spotifyAccessToken"
93+
94+
}
95+
96+
/**
97+
* The [EncryptedSharedPreferences] that this API saves to/retrieves from.
98+
*/
99+
@Suppress("DEPRECATION")
100+
public val encryptedPreferences: SharedPreferences = EncryptedSharedPreferences
101+
.create(
102+
"spotify-api-encrypted-preferences",
103+
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
104+
applicationContext,
105+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
106+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
107+
)
108+
109+
/**
110+
*
111+
*/
112+
public var spotifyTokenExpiresAt: Long?
113+
get() {
114+
val expiry = encryptedPreferences.getLong(SpotifyTokenExpiryKey, -1)
115+
return if (expiry == -1L) null else expiry
116+
}
117+
set(value) {
118+
if (value == null) encryptedPreferences.edit().remove(SpotifyTokenExpiryKey).apply()
119+
else encryptedPreferences.edit().putLong(SpotifyTokenExpiryKey, value).apply()
120+
}
121+
122+
public var spotifyAccessToken: String?
123+
get() = encryptedPreferences.getString(SpotifyAccessTokenKey, null)
124+
set(value) = encryptedPreferences.edit().putString(SpotifyAccessTokenKey, value).apply()
125+
126+
public var spotifyToken: Token?
127+
get() {
128+
val tokenExpiresAt = spotifyTokenExpiresAt ?: return null
129+
val accessToken = spotifyAccessToken ?: return null
130+
if (tokenExpiresAt < System.currentTimeMillis()) return null
131+
132+
return Token(accessToken, "Bearer", (tokenExpiresAt - System.currentTimeMillis()).toInt() / 1000)
133+
}
134+
set(token) {
135+
if (token == null) {
136+
spotifyAccessToken = null
137+
spotifyTokenExpiresAt = null
138+
} else {
139+
spotifyAccessToken = token.accessToken
140+
spotifyTokenExpiresAt = token.expiresAt
141+
}
142+
}
143+
144+
public fun getSpotifyImplicitGrantApi(block: ((SpotifyApiOptions).() -> Unit)? = null): SpotifyImplicitGrantApi? {
145+
val token = spotifyToken ?: return null
146+
return spotifyImplicitGrantApi(clientId, token, block ?: {})
147+
}
148+
149+
public fun setSpotifyImplicitGrantApi(api: SpotifyImplicitGrantApi) {
150+
spotifyToken = api.token
151+
}
152+
153+
@SuppressLint("ApplySharedPref")
154+
public fun clear(): Boolean = try {
155+
encryptedPreferences.edit().clear().commit()
156+
} catch (e: Exception) {
157+
// This might crash, encrypted preferences is still alpha...
158+
false
159+
}
160+
}
161+
162+
163+
164+

0 commit comments

Comments
 (0)