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
27 changes: 21 additions & 6 deletions src/SDK/Language/Kotlin.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,21 @@ protected function getNumberLiteral(mixed $value, string $type, string $lang): s
: $literal;
}

/**
* A double-quoted string literal. Kotlin also interpolates `$`, so it is
* escaped there; Java takes it as-is.
*/
protected function getStringLiteral(string $value, string $lang): string
{
$escaped = \str_replace(['\\', '"', "\n"], ['\\\\', '\\"', '\\n'], $value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Escape carriage returns in string literals too

An example containing \r\n still emits a raw carriage return, which is a line terminator in Java and breaks the generated quoted literal. Please escape \r alongside \n so Windows-style multiline examples compile too.

Suggested change
$escaped = \str_replace(['\\', '"', "\n"], ['\\\\', '\\"', '\\n'], $value);
$escaped = \str_replace(['\\', '"', "\n", "\r"], ['\\\\', '\\"', '\\n', '\\r'], $value);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/SDK/Language/Kotlin.php
Line: 169

Comment:
**Escape carriage returns in string literals too**

An example containing `\r\n` still emits a raw carriage return, which is a line terminator in Java and breaks the generated quoted literal. Please escape `\r` alongside `\n` so Windows-style multiline examples compile too.

```suggestion
        $escaped = \str_replace(['\\', '"', "\n", "\r"], ['\\\\', '\\"', '\\n', '\\r'], $value);
```

---

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

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


if ($lang !== 'java') {
$escaped = \str_replace('$', '\\$', $escaped);
}

return '"' . $escaped . '"';
}

/**
* @param string $lang Language variant: 'kotlin' (default) or 'java'
*/
Expand Down Expand Up @@ -228,7 +243,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = 'kotlin'
$output .= ($example) ? 'true' : 'false';
break;
case self::TYPE_STRING:
$output .= "\"{$example}\"";
$output .= $this->getStringLiteral((string) $example, $lang);
break;
}
}
Expand All @@ -247,9 +262,9 @@ protected function getKotlinMapExample(array $data, int $indentLevel = 0): strin
$baseIndent = str_repeat(' ', $indentLevel + 2);

foreach ($data as $key => $value) {
$formattedKey = '"' . $key . '"';
$formattedKey = $this->getStringLiteral((string) $key, 'kotlin');
if (is_string($value)) {
$formattedValue = '"' . $value . '"';
$formattedValue = $this->getStringLiteral($value, 'kotlin');
} elseif (is_bool($value)) {
$formattedValue = $value ? 'true' : 'false';
} elseif (is_null($value)) {
Expand Down Expand Up @@ -287,9 +302,9 @@ protected function getJavaMapExample(array $data, int $indentLevel = 0): string
$baseIndent = str_repeat(' ', $indentLevel + 2);

foreach ($data as $key => $value) {
$formattedKey = '"' . $key . '"';
$formattedKey = $this->getStringLiteral((string) $key, 'java');
if (is_string($value)) {
$formattedValue = '"' . $value . '"';
$formattedValue = $this->getStringLiteral($value, 'java');
} elseif (is_bool($value)) {
$formattedValue = $value ? 'true' : 'false';
} elseif (is_null($value)) {
Expand Down Expand Up @@ -341,7 +356,7 @@ protected function getArrayExample(string $example, string $lang = 'kotlin'): st
}
} elseif (is_string($item)) {
// Primitive value
$arrayItems[] = '"' . $item . '"';
$arrayItems[] = $this->getStringLiteral($item, $lang);
} elseif (is_bool($item)) {
$arrayItems[] = $item ? 'true' : 'false';
} elseif (is_null($item)) {
Expand Down
5 changes: 0 additions & 5 deletions src/SDK/Language/ReactNative.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,6 @@ public function getFiles(): array
'destination' => 'src/lib/polyfills.ts',
'template' => 'react-native/src/lib/polyfills.ts.twig',
],
[
'scope' => 'copy',
'destination' => 'app.plugin.js',
'template' => 'react-native/app.plugin.js.twig',
],
[
'scope' => 'default',
'destination' => 'src/models.ts',
Expand Down
18 changes: 0 additions & 18 deletions templates/android/README.md.twig
Original file line number Diff line number Diff line change
Expand Up @@ -76,24 +76,6 @@ data class Note(val title: String, val body: String)

### Push notifications

The `Push` service pulls in HiveMQ (which bundles Netty). Netty's jars ship duplicate
`META-INF` metadata that Android's packager rejects with a "More than one file was found
with OS independent path" error. If you use `Push`, add these excludes to the `android` block
of your **application** module's `build.gradle(.kts)` (packaging is resolved when the app is
assembled, so a library dependency cannot apply this for you):

```kotlin
android {
packaging {
resources {
excludes += "/META-INF/INDEX.LIST"
excludes += "/META-INF/io.netty.versions.properties"
excludes += "/META-INF/DEPENDENCIES"
}
}
}
```

#### Background delivery

A subscription with `background = true` keeps delivering after the app is backgrounded, its
Expand Down
4 changes: 2 additions & 2 deletions templates/android/docs/kotlin/example.md.twig
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ val result = {% endif %}{{ service.name | caseCamel }}.{{ (method | methodName)

{%~ for parameter in (method | parameters('all')) %}
{%~ if (parameter | enumValues) is not empty %}
{{ parameter.name }} = {{ (parameter | enumName) }}.{{ ((parameter | enumKeys)[0] ?? (parameter | enumValues)[0]) | caseEnumKey }},{% if not parameter.required %} // (optional){% endif %}
{{ parameter.name | caseCamel }} = {{ (parameter | enumName) }}.{{ ((parameter | enumKeys)[0] ?? (parameter | enumValues)[0]) | caseEnumKey }},{% if not parameter.required %} // (optional){% endif %}
{%~ else %}
{{ parameter.name }} = {{ parameter | paramExample }}, {% if not parameter.required %}// (optional){% endif %}
{{ parameter.name | caseCamel }} = {{ parameter | paramExample }}, {% if not parameter.required %}// (optional){% endif %}
{%~ endif %}

{%~ endfor %}
Expand Down
10 changes: 0 additions & 10 deletions templates/android/example/build.gradle.kts.twig
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,6 @@ android {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

// The push service pulls in HiveMQ (Netty), whose jars ship duplicate META-INF
// metadata; drop the duplicates so the app can be packaged.
packaging {
resources {
excludes += "/META-INF/INDEX.LIST"
excludes += "/META-INF/io.netty.versions.properties"
excludes += "/META-INF/DEPENDENCIES"
}
}
}

dependencies {
Expand Down
2 changes: 1 addition & 1 deletion templates/android/library/build.gradle.kts.twig
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ dependencies {

api("com.squareup.okhttp3:okhttp:5.5.0")
implementation("com.google.code.gson:gson:2.14.0")
implementation("com.hivemq:hivemq-mqtt-client:1.3.6")
implementation("com.hivemq:hivemq-mqtt-client-shaded:1.3.6")

implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.11.0")
implementation("androidx.lifecycle:lifecycle-common-java8:2.11.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ class Push(
private var connectionEpoch = 0

// Local id -> subscription (filter + callback + per-sub options). The id is never sent to
// the broker; it only lets the same topic carry more than one callback.
// the broker; it only lets the same topic carry more than one callback. HiveMQ callbacks
// look their subscription up by id on delivery, because a callback stays attached to its
// filter until the filter itself is unsubscribed, which waits for the filter's last sub.
private val subscriptions = ConcurrentHashMap<String, Sub>()

// Whether the connection currently lives in the process-wide background host (its own MQTT
Expand Down Expand Up @@ -447,7 +449,7 @@ class Push(
client.subscribeWith()
.topicFilter(sub.filter)
.qos(effectiveQos(sub.filter))
.callback { publish -> sub.callback(publish.toPushMessage()) }
.callback { publish -> subscriptions[id]?.callback?.invoke(publish.toPushMessage()) }
.send()
.get()
sub.client = client
Expand Down Expand Up @@ -478,14 +480,14 @@ class Push(
// background subscription goes away and the connection relocates back in-process.
private fun ensureInProcessSubscribed() {
val client = connect()
for (sub in subscriptions.values) {
for ((id, sub) in subscriptions) {
if (sub.client === client) {
continue
}
client.subscribeWith()
.topicFilter(sub.filter)
.qos(effectiveQos(sub.filter))
.callback { publish -> sub.callback(publish.toPushMessage()) }
.callback { publish -> subscriptions[id]?.callback?.invoke(publish.toPushMessage()) }
.send()
.reportFailure(client)
sub.client = client
Expand All @@ -498,11 +500,11 @@ class Push(
val client = mqtt ?: return
client.unsubscribeWith().topicFilter(filter).send().reportFailure(client)
val qos = effectiveQos(filter)
for (sub in subscriptions.values.filter { it.filter == filter }) {
for ((id, sub) in subscriptions.filter { it.value.filter == filter }) {
client.subscribeWith()
.topicFilter(filter)
.qos(qos)
.callback { publish -> sub.callback(publish.toPushMessage()) }
.callback { publish -> subscriptions[id]?.callback?.invoke(publish.toPushMessage()) }
.send()
.reportFailure(client)
sub.client = client
Expand Down Expand Up @@ -658,14 +660,14 @@ class Push(
throw e
}

for (sub in subscriptions.values) {
for ((id, sub) in subscriptions) {
if (sub.client == null || sub.client === client) {
continue
}
client.subscribeWith()
.topicFilter(sub.filter)
.qos(effectiveQos(sub.filter))
.callback { publish -> sub.callback(publish.toPushMessage()) }
.callback { publish -> subscriptions[id]?.callback?.invoke(publish.toPushMessage()) }
.send()
.reportFailure(client)
sub.client = client
Expand Down
23 changes: 12 additions & 11 deletions templates/android/push/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Push: rules applied to apps that shrink with R8.
#
# The HiveMQ MQTT client bundles Netty, which references optional classes that are never loaded
# The shaded HiveMQ MQTT client relocates Netty and JCTools under
# com.hivemq.client.internal.shaded. Netty references optional classes that are never loaded
# on Android: native transports, the HTTP/WebSocket codecs, proxy handlers, alternative TLS
# providers (tcnative, BouncyCastle, Conscrypt, Jetty ALPN) and logging backends.
-dontwarn io.netty.channel.epoll.**
-dontwarn io.netty.channel.kqueue.**
-dontwarn io.netty.handler.codec.http.**
-dontwarn io.netty.handler.proxy.**
-dontwarn io.netty.internal.tcnative.**
-dontwarn com.hivemq.client.internal.shaded.io.netty.channel.epoll.**
-dontwarn com.hivemq.client.internal.shaded.io.netty.channel.kqueue.**
-dontwarn com.hivemq.client.internal.shaded.io.netty.handler.codec.http.**
-dontwarn com.hivemq.client.internal.shaded.io.netty.handler.proxy.**
-dontwarn com.hivemq.client.internal.shaded.io.netty.internal.tcnative.**
-dontwarn org.bouncycastle.**
-dontwarn org.conscrypt.**
-dontwarn org.eclipse.jetty.**
Expand All @@ -17,10 +18,10 @@
-dontwarn reactor.blockhound.**

# Netty and JCTools reach their own fields by name, through atomic field updaters and Unsafe offsets.
-keepclassmembernames class io.netty.** { <fields>; }
-keepclassmembers class org.jctools.** { <fields>; }
-keepclassmembernames class com.hivemq.client.internal.shaded.io.netty.** { <fields>; }
-keepclassmembers class com.hivemq.client.internal.shaded.org.jctools.** { <fields>; }

# Netty's leak detector looks these methods up by name when its classes load.
-keepclassmembers class io.netty.buffer.AbstractByteBufAllocator { *** toLeakAwareBuffer(...); }
-keepclassmembers class io.netty.buffer.AdvancedLeakAwareByteBuf { *** recordLeakNonRefCountingOperation(...); }
-keepclassmembers class io.netty.util.ReferenceCountUtil { *** touch(...); }
-keepclassmembers class com.hivemq.client.internal.shaded.io.netty.buffer.AbstractByteBufAllocator { *** toLeakAwareBuffer(...); }
-keepclassmembers class com.hivemq.client.internal.shaded.io.netty.buffer.AdvancedLeakAwareByteBuf { *** recordLeakNonRefCountingOperation(...); }
-keepclassmembers class com.hivemq.client.internal.shaded.io.netty.util.ReferenceCountUtil { *** touch(...); }
14 changes: 3 additions & 11 deletions templates/flutter/README.md.twig
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,15 @@ flutter pub add {{ language.params.packageName }}

### Push on Android

`Push` includes a native Android plugin whose MQTT client bundles Netty. Its jars ship duplicate
`META-INF` files that Android's packager rejects, and `flutter_local_notifications` needs core
library desugaring, so add this to `android/app/build.gradle.kts` (packaging is resolved when the
app is assembled, so a plugin cannot apply it for you):
`Push` includes a native Android plugin, and `flutter_local_notifications` needs core library
desugaring, so add this to `android/app/build.gradle.kts` (desugaring is configured when the app
is assembled, so a plugin cannot apply it for you):

```kotlin
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
packaging {
resources {
excludes += "/META-INF/INDEX.LIST"
excludes += "/META-INF/io.netty.versions.properties"
excludes += "/META-INF/DEPENDENCIES"
}
}
}

dependencies {
Expand Down
2 changes: 1 addition & 1 deletion templates/flutter/android/build.gradle.twig
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ android {
}

dependencies {
implementation("com.hivemq:hivemq-mqtt-client:1.3.6")
implementation("com.hivemq:hivemq-mqtt-client-shaded:1.3.6")
implementation("androidx.core:core-ktx:1.13.1")
}
4 changes: 2 additions & 2 deletions templates/kotlin/docs/kotlin/example.md.twig
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ val {{ service.name | caseCamel }} = {{ service.name | caseUcfirst }}(client)

{% for parameter in (method | parameters('all')) %}
{% if parameter.required %}
{{parameter.name}} = {% if (parameter | enumValues) | length > 0 %}{{ parameter | enumExample }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %}
{{ parameter.name | caseCamel }} = {% if (parameter | enumValues) | length > 0 %}{{ parameter | enumExample }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %}

{% else %}
{{parameter.name}} = {% if (parameter | enumValues) | length > 0 %}{{ parameter | enumExample }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %} // optional
{{ parameter.name | caseCamel }} = {% if (parameter | enumValues) | length > 0 %}{{ parameter | enumExample }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %} // optional
{% endif %}
{% if loop.last %}
)
Expand Down
7 changes: 1 addition & 6 deletions templates/react-native/README.md.twig
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,7 @@ npx expo install react-native-appwrite react-native-url-polyfill
### Push

`Push` connects over a raw TCP socket, so it needs `react-native-tcp-socket` and a development
or standalone build (native modules do not run in Expo Go). Add the SDK's config plugin to your
`app.json`; it adds the Android packaging excludes the push client's Netty dependency needs:

```json
{ "expo": { "plugins": ["{{ language.params.npmPackage }}"] } }
```
or standalone build (native modules do not run in Expo Go).

On Expo SDK 52 and older, enable package exports in `metro.config.js` so `mqtt` resolves to its
React Native build (Expo SDK 53 and later do this by default):
Expand Down
2 changes: 1 addition & 1 deletion templates/react-native/android/build.gradle.twig
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ repositories {

dependencies {
implementation "com.facebook.react:react-android"
implementation "com.hivemq:hivemq-mqtt-client:1.3.6"
implementation "com.hivemq:hivemq-mqtt-client-shaded:1.3.6"
implementation "androidx.core:core-ktx:1.13.1"
}
48 changes: 0 additions & 48 deletions templates/react-native/app.plugin.js.twig

This file was deleted.

1 change: 0 additions & 1 deletion templates/react-native/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export default tseslint.config(
'types/',
'docs/',
'rollup.config.mjs',
'app.plugin.js',
'react-native.config.js',
],
},
Expand Down
4 changes: 0 additions & 4 deletions templates/react-native/package-lock.json.twig

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading