Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ jobs:
- name: Publish Release Version to MavenCentral
run: |
if ${{ contains(github.event.release.tag_name, '-rc') }}; then
echo "Publising Snapshot Version ${{ github.event.release.tag_name}} to Snapshot repository"
echo "Publishing Snapshot Version $SDK_VERSION to Snapshot repository"
./gradlew publishToSonatype
else
echo "Publising Release Version ${{ github.event.release.tag_name}} to Staging repository"
echo "Publishing Release Version $SDK_VERSION to Staging repository"
./gradlew publishToSonatype --max-workers 1 closeAndReleaseSonatypeStagingRepository
fi
env:
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Change Log

## 31.1.0

* Updated: requests send `x-appwrite-response-format: 2.6.2`
* Fixed: path parameters are percent-encoded as one segment; `.` and `..` throw `AppwriteException`
* Fixed: a chunked upload with a new file ID starts uploading instead of failing on the resume lookup
* Fixed: chunked upload resume lookups percent-encode the file ID
* Fixed: cookie store errors no longer log cookie values
* Updated: README adds install instructions and explains realtime client ownership

## 31.0.1

* Fixed: realtime connections, subscriptions, presence and reconnect state are isolated between clients
Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

![Maven Central](https://img.shields.io/maven-central/v/io.appwrite/sdk-for-android.svg?color=green&style=flat-square)
![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg?style=flat-square)
![Version](https://img.shields.io/badge/api%20version-2.6.1-blue.svg?style=flat-square)
![Version](https://img.shields.io/badge/api%20version-2.6.2-blue.svg?style=flat-square)
[![X Account](https://img.shields.io/badge/follow-%40appwrite-000000?logo=x&style=flat-square)](https://x.com/appwrite)
[![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord)

Expand All @@ -20,15 +20,15 @@ Kotlin DSL (`build.gradle.kts`):

```kotlin
dependencies {
implementation("io.appwrite:sdk-for-android:31.0.1")
implementation("io.appwrite:sdk-for-android:31.1.0")
}
```

Groovy (`build.gradle`):

```groovy
dependencies {
implementation 'io.appwrite:sdk-for-android:31.0.1'
implementation 'io.appwrite:sdk-for-android:31.1.0'
}
```

Expand All @@ -43,6 +43,14 @@ data class Note(val title: String, val body: String)

## Getting Started

### Install the SDK

Add SDK 31.0.1 to your app module's `build.gradle.kts`:

```kotlin
implementation("io.appwrite:sdk-for-android:31.0.1")
Comment on lines +48 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting Started pins the previous SDK version

The Installation examples above now use 31.1.0, but this new Getting Started step tells users to add 31.0.1. Following it pins users to the previous release, so they miss the 31.1.0 fixes documented for this update.

Prompt To Fix With AI
This is a comment left during a code review.
Path: README.md
Line: 48-51

Comment:
**Getting Started pins the previous SDK version**

The Installation examples above now use 31.1.0, but this new Getting Started step tells users to add 31.0.1. Following it pins users to the previous release, so they miss the 31.1.0 fixes documented for this update.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

🟡 Minor · documentation · Reply if this doesn't apply.

```

### Add your Android platform

To let your app talk to Appwrite, register it as a platform. In the [Appwrite Console](https://cloud.appwrite.io/console), open your project (create one if you haven't already), click **Add platform** and choose **Android**.
Expand Down Expand Up @@ -216,6 +224,12 @@ val user = account.get()
Log.d("Appwrite", user.toMap().toString())
```

### Realtime client ownership

In SDK 31.0.1 and later, each `Client` owns its realtime connection, subscriptions, presence, and reconnect state. Separate clients can use different endpoints, projects, or JWTs independently. Configure `setJWT()` before creating a JWT-authenticated subscription.

`Realtime` objects created with the same client share that connection. Disconnecting it affects that client's realtime subscriptions. Close an individual subscription to stop only that subscription; use separate clients for independently managed connections.

## Push notifications

`Push` subscribes to topics over the Appwrite push broker. With no topic it subscribes to the signed-in user's own topic:
Expand Down
52 changes: 39 additions & 13 deletions library/src/main/java/io/appwrite/Client.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import java.io.RandomAccessFile
import java.lang.IllegalArgumentException
import java.net.CookieManager
import java.net.CookiePolicy
import java.net.URLEncoder
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.concurrent.atomic.AtomicInteger
Expand Down Expand Up @@ -66,6 +67,25 @@ class Client @JvmOverloads constructor(
internal const val MAX_CONCURRENT_UPLOADS = 8
internal const val GLOBAL_PREFS = "io.appwrite"
internal const val COOKIE_PREFS = "myCookie"

/**
* Encodes [value] as one path segment. Everything but letters, digits
* and -_.!~*'() is escaped, as encodeURIComponent does, so a value
* fills exactly the segment it replaces.
*/
internal fun encodePath(value: String): String {
if (value == "." || value == "..") {
throw AppwriteException("Path parameter cannot be \".\" or \"..\"")
}
return URLEncoder
.encode(value, "UTF-8")
.replace("+", "%20")
.replace("%21", "!")
.replace("%27", "'")
.replace("%28", "(")
.replace("%29", ")")
.replace("%7E", "~")
}
}

override val coroutineContext: CoroutineContext
Expand Down Expand Up @@ -104,8 +124,8 @@ class Client @JvmOverloads constructor(
"x-sdk-name" to "Android",
"x-sdk-platform" to "client",
"x-sdk-language" to "android",
"x-sdk-version" to "31.0.1",
"x-appwrite-response-format" to "2.6.1"
"x-sdk-version" to "31.1.0",
"x-appwrite-response-format" to "2.6.2"
)
config = mutableMapOf()

Expand Down Expand Up @@ -609,17 +629,23 @@ class Client @JvmOverloads constructor(
val providedUploadId = idParamName?.let { params[it]?.toString() }
if (!providedUploadId.isNullOrEmpty()) {
// Make a request to check if a file already exists
val current = call(
method = "GET",
path = "$path/$providedUploadId",
headers = headers,
params = emptyMap(),
responseType = Map::class.java,
)
val chunksUploaded = current["chunksUploaded"] as Long
offset = chunksUploaded * CHUNK_SIZE
uploadId = providedUploadId
result = current
try {
val current = call(
method = "GET",
path = "$path/${encodePath(providedUploadId)}",
headers = headers,
params = emptyMap(),
responseType = Map::class.java,
)
val chunksUploaded = current["chunksUploaded"] as Long
offset = chunksUploaded * CHUNK_SIZE
uploadId = providedUploadId
result = current
} catch (exception: AppwriteException) {
if (exception.code != 404) {
throw exception
}
}
}

fun readChunk(start: Long, end: Long): ByteArray {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ open class SharedPreferencesCookieStore(
} catch (exception: Throwable) {
Log.e(
javaClass.simpleName,
"Error while loading key = $key, value = $value from cookie store",
"Error while loading key = $key from cookie store",
exception
)
}
Expand Down
50 changes: 25 additions & 25 deletions library/src/main/java/io/appwrite/services/Account.kt
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"consentId\"")
}
val apiPath = ("/account/consents/{consentId}"
.replace("{consentId}", consentId)
.replace("{consentId}", Client.encodePath(consentId))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -209,7 +209,7 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"consentId\"")
}
val apiPath = ("/account/consents/{consentId}"
.replace("{consentId}", consentId)
.replace("{consentId}", Client.encodePath(consentId))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -245,7 +245,7 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"consentId\"")
}
val apiPath = ("/account/consents/{consentId}/tokens"
.replace("{consentId}", consentId)
.replace("{consentId}", Client.encodePath(consentId))
)
val apiParams = mutableMapOf<String, Any?>(
"queries" to queries,
Expand Down Expand Up @@ -288,8 +288,8 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"tokenId\"")
}
val apiPath = ("/account/consents/{consentId}/tokens/{tokenId}"
.replace("{consentId}", consentId)
.replace("{tokenId}", tokenId)
.replace("{consentId}", Client.encodePath(consentId))
.replace("{tokenId}", Client.encodePath(tokenId))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -329,8 +329,8 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"tokenId\"")
}
val apiPath = ("/account/consents/{consentId}/tokens/{tokenId}"
.replace("{consentId}", consentId)
.replace("{tokenId}", tokenId)
.replace("{consentId}", Client.encodePath(consentId))
.replace("{tokenId}", Client.encodePath(tokenId))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -455,7 +455,7 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"identityId\"")
}
val apiPath = ("/account/identities/{identityId}"
.replace("{identityId}", identityId)
.replace("{identityId}", Client.encodePath(identityId))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -605,7 +605,7 @@ class Account(client: Client) : Service(client) {
type: io.appwrite.enums.AuthenticatorType,
): io.appwrite.models.MfaType {
val apiPath = ("/account/mfa/authenticators/{type}"
.replace("{type}", type.value)
.replace("{type}", Client.encodePath(type.value))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -638,7 +638,7 @@ class Account(client: Client) : Service(client) {
type: io.appwrite.enums.AuthenticatorType,
): io.appwrite.models.MfaType {
val apiPath = ("/account/mfa/authenticators/{type}"
.replace("{type}", type.value)
.replace("{type}", Client.encodePath(type.value))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -678,7 +678,7 @@ class Account(client: Client) : Service(client) {
nestedType: Class<T>,
): io.appwrite.models.User<T> {
val apiPath = ("/account/mfa/authenticators/{type}"
.replace("{type}", type.value)
.replace("{type}", Client.encodePath(type.value))
)
val apiParams = mutableMapOf<String, Any?>(
"otp" to otp,
Expand Down Expand Up @@ -737,7 +737,7 @@ class Account(client: Client) : Service(client) {
nestedType: Class<T>,
): io.appwrite.models.User<T> {
val apiPath = ("/account/mfa/authenticators/{type}"
.replace("{type}", type.value)
.replace("{type}", Client.encodePath(type.value))
)
val apiParams = mutableMapOf<String, Any?>(
"otp" to otp,
Expand Down Expand Up @@ -793,7 +793,7 @@ class Account(client: Client) : Service(client) {
type: io.appwrite.enums.AuthenticatorType,
): Any {
val apiPath = ("/account/mfa/authenticators/{type}"
.replace("{type}", type.value)
.replace("{type}", Client.encodePath(type.value))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -821,7 +821,7 @@ class Account(client: Client) : Service(client) {
type: io.appwrite.enums.AuthenticatorType,
): Any {
val apiPath = ("/account/mfa/authenticators/{type}"
.replace("{type}", type.value)
.replace("{type}", Client.encodePath(type.value))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -1355,7 +1355,7 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"passkeyId\"")
}
val apiPath = ("/account/passkeys/{passkeyId}"
.replace("{passkeyId}", passkeyId)
.replace("{passkeyId}", Client.encodePath(passkeyId))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -1392,7 +1392,7 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"passkeyId\"")
}
val apiPath = ("/account/passkeys/{passkeyId}"
.replace("{passkeyId}", passkeyId)
.replace("{passkeyId}", Client.encodePath(passkeyId))
)
val apiParams = mutableMapOf<String, Any?>(
"name" to name,
Expand Down Expand Up @@ -1430,7 +1430,7 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"passkeyId\"")
}
val apiPath = ("/account/passkeys/{passkeyId}"
.replace("{passkeyId}", passkeyId)
.replace("{passkeyId}", Client.encodePath(passkeyId))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -1463,7 +1463,7 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"passkeyId\"")
}
val apiPath = ("/account/passkeys/{passkeyId}/verification"
.replace("{passkeyId}", passkeyId)
.replace("{passkeyId}", Client.encodePath(passkeyId))
)
val apiParams = mutableMapOf<String, Any?>(
"credential" to credential,
Expand Down Expand Up @@ -2103,7 +2103,7 @@ class Account(client: Client) : Service(client) {
state: String? = null,
) {
val apiPath = ("/account/sessions/oauth2/{provider}"
.replace("{provider}", provider.value)
.replace("{provider}", Client.encodePath(provider.value))
)
val apiParams = mutableMapOf<String, Any?>(
"success" to success,
Expand Down Expand Up @@ -2248,7 +2248,7 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"sessionId\"")
}
val apiPath = ("/account/sessions/{sessionId}"
.replace("{sessionId}", sessionId)
.replace("{sessionId}", Client.encodePath(sessionId))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -2283,7 +2283,7 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"sessionId\"")
}
val apiPath = ("/account/sessions/{sessionId}"
.replace("{sessionId}", sessionId)
.replace("{sessionId}", Client.encodePath(sessionId))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -2319,7 +2319,7 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"sessionId\"")
}
val apiPath = ("/account/sessions/{sessionId}"
.replace("{sessionId}", sessionId)
.replace("{sessionId}", Client.encodePath(sessionId))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -2443,7 +2443,7 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"targetId\"")
}
val apiPath = ("/account/targets/{targetId}/push"
.replace("{targetId}", targetId)
.replace("{targetId}", Client.encodePath(targetId))
)
val apiParams = mutableMapOf<String, Any?>(
"identifier" to identifier,
Expand Down Expand Up @@ -2483,7 +2483,7 @@ class Account(client: Client) : Service(client) {
throw AppwriteException("Missing required parameter: \"targetId\"")
}
val apiPath = ("/account/targets/{targetId}/push"
.replace("{targetId}", targetId)
.replace("{targetId}", Client.encodePath(targetId))
)
val apiParams = mutableMapOf<String, Any?>()
val apiHeaders = mutableMapOf<String, String>(
Expand Down Expand Up @@ -2615,7 +2615,7 @@ class Account(client: Client) : Service(client) {
state: String? = null,
) {
val apiPath = ("/account/tokens/oauth2/{provider}"
.replace("{provider}", provider.value)
.replace("{provider}", Client.encodePath(provider.value))
)
val apiParams = mutableMapOf<String, Any?>(
"success" to success,
Expand Down
Loading