Skip to content
12 changes: 12 additions & 0 deletions gradle/verification-metadata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
<trusted-key id="19BEAB2D799C020F17C69126B16698A4ADF4D638" group="org.checkerframework"/>
<trusted-key id="1A55F091AD28C07F831FA44D7905DE25C78AD456" group="com.google.protobuf"/>
<trusted-key id="1D0A8B5E77C678A7C724445ABF984B4145EA13F7" group="com.squareup" name="javapoet" version="1.13.0"/>
<trusted-key id="1D217F8475EEE9F19AB8DD6B793FD5751A0F0780" group="^com[.]squareup($|([.].*))" regex="true"/>
<trusted-key id="1D2C7EF8ADA0F794B58C7C63436902AF59EDF60E">
<trusting group="dev.equo.ide" name="solstice" version="1.7.5"/>
<trusting group="dev.equo.ide" name="solstice" version="1.8.0"/>
Expand Down Expand Up @@ -236,6 +237,7 @@
<trusting group="androidx.graphics" name="graphics-path" version="1.0.1"/>
<trusting group="androidx.lifecycle"/>
<trusting group="androidx.profileinstaller"/>
<trusting group="androidx.startup" name="startup-runtime" version="1.2.0"/>
<trusting group="androidx.transition" name="transition" version="1.5.0"/>
<trusting group="androidx.webkit"/>
<trusting group="^androidx[.]compose($|([.].*))" regex="true"/>
Expand Down Expand Up @@ -324,6 +326,11 @@
<sha256 value="c8923871e556cd5467addabac6773e778f3a4d3da19bfc8153bbaee0d145298f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.activity" name="activity-compose" version="1.7.0">
<artifact name="activity-compose-1.7.0.module">
<sha256 value="f7a29bcba338575dcf89a553cff9cfad3f140340eaf2b56fd0193244da602c0a" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.activity" name="activity-compose" version="1.8.2">
<artifact name="activity-compose-1.8.2.aar">
<sha256 value="5a67e984f14ed2afc585aa3a23edff1c1791c80caa2bf68a0f799c1b11a39038" origin="Generated by Gradle"/>
Expand Down Expand Up @@ -20709,6 +20716,11 @@
<sha256 value="35bbd365a61afa59ad8c116528bced5bd628d3351704173abfc8b75fec04ffad" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin.plugin.serialization" name="org.jetbrains.kotlin.plugin.serialization.gradle.plugin" version="2.3.21">
<artifact name="org.jetbrains.kotlin.plugin.serialization.gradle.plugin-2.3.21.pom">
<sha256 value="d1e12aeedc9fab44409b6c08b2a45384a4bb851fe4e90391efade14d347c48d7" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="atomicfu" version="0.17.3">
<artifact name="atomicfu-0.17.3.module">
<sha256 value="854a75a9ebf30cb588e8ceda7da1b7089d4272a12324d3cffcaf5b62902738bd" origin="Generated by Gradle"/>
Expand Down
1 change: 1 addition & 0 deletions material-color-utilities/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

/*
* Nextcloud Android Common Library
*
Expand Down
1 change: 1 addition & 0 deletions sample/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.10.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
implementation project(path: ':ui')
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
Expand Down
1 change: 1 addition & 0 deletions sample/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.Androidcommon">
<activity
android:name=".MainActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ class MainActivity : AppCompatActivity() {
material.colorMaterialButtonPrimaryBorderless(negativeButton)
}

binding.testApiBtn.setOnClickListener {
val baseUrl = binding.baseUrl.text?.toString().orEmpty()
val username = binding.username.text?.toString().orEmpty()
val token = binding.token.text?.toString().orEmpty()
mainViewModel.testPredefinedStatuses(baseUrl, username, token)
}

mainViewModel.apiTestResult.observe(this) { result ->
Toast.makeText(this, result, Toast.LENGTH_LONG).show()
}

setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
mainViewModel.color.observe(this) { applyTheme(it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,41 @@ package com.nextcloud.android.common.sample

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nextcloud.android.common.ui.network.auth.ServerCredentials
import com.nextcloud.android.common.ui.network.model.NetworkResult
import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient
import com.nextcloud.android.common.ui.network.UserStatusRepository
import kotlinx.coroutines.launch

class MainViewModel : ViewModel() {
val color = MutableLiveData<Int>()
val apiTestResult = MutableLiveData<String>()

fun testPredefinedStatuses(
baseUrl: String,
username: String,
token: String
) {
viewModelScope.launch {
val credentials = ServerCredentials(baseUrl, username, token)
val client = NextcloudHttpClient.create(credentials, enableLogging = true)
val service = UserStatusRepository(client)

when (val result = service.fetchPredefinedStatuses()) {
is NetworkResult.Success ->
apiTestResult.value =
"✅ Success (${result.data.size} statuses):\n" +
result.data.joinToString("\n") { "${it.icon} ${it.message}" }

is NetworkResult.ServerError ->
apiTestResult.value =
"❌ Error ${result.response.ocs.meta.statusCode}: ${result.response.ocs.meta.message}"

is NetworkResult.NetworkException ->
apiTestResult.value =
"❌ Exception: ${result.throwable.message}"
}
}
}
}
61 changes: 61 additions & 0 deletions sample/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,67 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/circular_progress_bar" />

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/baseUrlTil"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/hint_base_url"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialogBtn">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/baseUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:text="https://cloud.example.com" />

</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/usernameTil"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/hint_username"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/baseUrlTil">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />

</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tokenTil"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/hint_token"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/usernameTil">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/token"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />

</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.button.MaterialButton
android:id="@+id/testApiBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/test_user_status_api"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tokenTil" />

</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

Expand Down
4 changes: 4 additions & 0 deletions sample/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
<string name="suggestion_chip">Suggestion Chip</string>
<string name="filter_chip">Filter Chip</string>
<string name="hint_color">Color</string>
<string name="hint_base_url">Base URL</string>
<string name="hint_username">Username</string>
<string name="hint_token">App token</string>
<string name="test_user_status_api">Test User Status API</string>
<string name="headline_theming">Theming</string>
<string name="headline_ui_module">UI Module</string>
</resources>
10 changes: 10 additions & 0 deletions ui/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

plugins {
id 'org.jetbrains.kotlin.plugin.compose'
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlinVersion"
id 'com.android.library'
id 'com.android.built-in-kotlin'
id 'com.android.legacy-kapt'
Expand Down Expand Up @@ -45,12 +46,15 @@ android {
}

dependencies {
implementation 'androidx.compose.ui:ui-tooling-preview:1.11.0'
debugImplementation 'androidx.compose.ui:ui-tooling:1.11.0'
kapt "org.jetbrains.kotlin:kotlin-metadata-jvm:${kotlinVersion}"

implementation(platform("androidx.compose:compose-bom:2026.04.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-core")

implementation("com.vanniktech:ui:0.10.0")

Expand All @@ -60,6 +64,12 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'

implementation(platform("com.squareup.okhttp3:okhttp-bom:5.3.2"))
implementation("com.squareup.okhttp3:okhttp")
implementation("com.squareup.okhttp3:logging-interceptor")

implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")

implementation project(':core')
api project(':material-color-utilities')

Expand Down
2 changes: 1 addition & 1 deletion ui/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
~ SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
~ SPDX-License-Identifier: MIT
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Nextcloud Android Common Library
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: MIT
*/

package com.nextcloud.android.common.ui.network

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonPrimitive

object ClearAtTimeSerializer : KSerializer<String> {
override val descriptor = PrimitiveSerialDescriptor("ClearAtTime", PrimitiveKind.STRING)

override fun deserialize(decoder: Decoder): String {
val jsonDecoder = decoder as? JsonDecoder ?: return decoder.decodeString()
return (jsonDecoder.decodeJsonElement() as? JsonPrimitive)?.content ?: ""
}

override fun serialize(encoder: Encoder, value: String) = encoder.encodeString(value)
}

@Serializable
data class ClearAt(
val type: String,
@Serializable(with = ClearAtTimeSerializer::class)
val time: String
)

@Serializable
data class PredefinedStatus(
val id: String,
val icon: String,
val message: String,
val clearAt: ClearAt? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Nextcloud Android Common Library
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: MIT
*/

package com.nextcloud.android.common.ui.network

import com.nextcloud.android.common.ui.network.http.HttpMethod
import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient
import com.nextcloud.android.common.ui.network.model.NetworkResult
import com.nextcloud.android.common.ui.network.model.OcsResponse
import com.nextcloud.android.common.ui.network.serialization.OCSSerializer

class UserStatusRepository(private val client: NextcloudHttpClient) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

For test activity


private companion object {
private const val PREDEFINED_STATUSES_ENDPOINT =
"/ocs/v2.php/apps/user_status/api/v1/predefined_statuses"
}

suspend fun fetchPredefinedStatuses(): NetworkResult<List<PredefinedStatus>> =
client.executeRequest(
endpoint = PREDEFINED_STATUSES_ENDPOINT,
method = HttpMethod.GET
) { body ->
OCSSerializer.json.decodeFromString<OcsResponse<List<PredefinedStatus>>>(body).ocs.data
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Nextcloud Android Common Library
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: MIT
*/

package com.nextcloud.android.common.ui.network.auth

import okhttp3.Credentials
import okhttp3.Interceptor
import okhttp3.Response

class AuthInterceptor(private val credentials: ServerCredentials) : Interceptor {

private companion object {
private const val HTTP_PREFIX = "http://"
private const val HTTPS_PREFIX = "https://"
private const val DELIMITER = '/'

private const val HEADER_AUTHORIZATION = "Authorization"
private const val HEADER_OCS_REQUEST = "OCS-APIRequest"
private const val HEADER_OCS_REQUEST_VALUE = "true"
}

override fun intercept(chain: Interceptor.Chain): Response {
val basicCredentials = Credentials.basic(credentials.username, credentials.token)

val request = chain.request()
.newBuilder()
.header(HEADER_AUTHORIZATION, basicCredentials)
.header(HEADER_OCS_REQUEST, HEADER_OCS_REQUEST_VALUE)
.url(resolveUrl(chain.request().url.toString()))
.build()

return chain.proceed(request)
}

/**
* Prepends [ServerCredentials.baseURL] to relative URLs.
* Absolute URLs (starting with http/https) are passed through unchanged.
*/
private fun resolveUrl(requestUrl: String): String =
if (requestUrl.startsWith(HTTP_PREFIX) || requestUrl.startsWith(HTTPS_PREFIX)) {
requestUrl
} else {
"${credentials.baseURL.trimEnd(DELIMITER)}/${requestUrl.trimStart(DELIMITER)}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Nextcloud Android Common Library
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: MIT
*/

package com.nextcloud.android.common.ui.network.auth

data class ServerCredentials(
val baseURL: String,
val username: String,
val token: String
)
Loading
Loading