added DEV version to repo

This commit is contained in:
Fr4nz D13trich 2025-09-18 18:43:03 +02:00
parent 1ef725ef20
commit 23e673bfdf
2135 changed files with 97033 additions and 21206 deletions

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.android.files

View file

@ -4,7 +4,7 @@
* SPDX-FileCopyrightText: 2019 David Luhmer <david-dev@live.de>
* SPDX-FileCopyrightText: 2019 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2019 Edvard Holst <edvard.holst@gmail.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.android.sso;

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 David Luhmer <david-dev@live.de>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*
* More information here: https://github.com/abeluck/android-streams-ipc
*/
@ -237,7 +237,7 @@ public class InputStreamBinder extends IInputStreamService.Stub {
case "POST":
method = new PostMethod(requestUrl);
if (requestBodyInputStream != null) {
RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream, -1);
RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream);
((PostMethod) method).setRequestEntity(requestEntity);
} else if (request.getRequestBody() != null) {
StringRequestEntity requestEntity = new StringRequestEntity(
@ -251,7 +251,7 @@ public class InputStreamBinder extends IInputStreamService.Stub {
case "PATCH":
method = new PatchMethod(requestUrl);
if (requestBodyInputStream != null) {
RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream, -1);
RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream);
((PatchMethod) method).setRequestEntity(requestEntity);
} else if (request.getRequestBody() != null) {
StringRequestEntity requestEntity = new StringRequestEntity(
@ -265,7 +265,7 @@ public class InputStreamBinder extends IInputStreamService.Stub {
case "PUT":
method = new PutMethod(requestUrl);
if (requestBodyInputStream != null) {
RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream, -1);
RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream);
((PutMethod) method).setRequestEntity(requestEntity);
} else if (request.getRequestBody() != null) {
StringRequestEntity requestEntity = new StringRequestEntity(
@ -502,12 +502,15 @@ public class InputStreamBinder extends IInputStreamService.Stub {
@VisibleForTesting
public static NameValuePair[] convertMapToNVP(Map<String, String> map) {
NameValuePair[] nvp = new NameValuePair[map.size()];
final var nvp = new NameValuePair[map.size()];
int i = 0;
for (String key : map.keySet()) {
nvp[i] = new NameValuePair(key, map.get(key));
for (Map.Entry<String, String> entry : map.entrySet()) {
final var nameValuePair = new NameValuePair(entry.getKey(), entry.getValue());
nvp[i] = nameValuePair;
i++;
}
return nvp;
}

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2021 Timo Triebensky <timo@binsky.org>
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*
* More information here: https://github.com/abeluck/android-streams-ipc
*/

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2019 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.android.sso;

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2021 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.android.sso;

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2019 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.android.sso;

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2017 David Luhmer <david-dev@live.de>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.android.sso.aidl;

View file

@ -4,7 +4,7 @@
* SPDX-FileCopyrightText: 2021 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH
* SPDX-FileCopyrightText: 2017 David Luhmer <david-dev@live.de>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.android.sso.aidl;

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2017 David Luhmer <david-dev@live.de>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.android.sso.aidl;

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2023 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.appReview

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2023 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.appReview

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2023 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.appReview
@ -18,7 +18,6 @@ class InAppReviewModule {
@Provides
@Singleton
internal fun providesInAppReviewHelper(appPreferences: AppPreferences): InAppReviewHelper {
return InAppReviewHelperImpl(appPreferences)
}
internal fun providesInAppReviewHelper(appPreferences: AppPreferences): InAppReviewHelper =
InAppReviewHelperImpl(appPreferences)
}

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2023 ZetaTom
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client
@ -12,7 +12,7 @@ import com.google.gson.annotations.SerializedName
import com.owncloud.android.MainApp
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.internal.http.HTTP_OK
import java.net.HttpURLConnection.HTTP_OK
import java.net.URLEncoder
class NominatimClient constructor(geocoderBaseUrl: String, email: String) {

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.account
@ -23,7 +23,9 @@ import java.net.URI
* It serves as a semantically correct "empty value", allowing simplification of logic
* in various components requiring user data, such as DB queries.
*/
internal data class AnonymousUser(private val accountType: String) : User, Parcelable {
internal data class AnonymousUser(private val accountType: String) :
User,
Parcelable {
companion object {
@JvmStatic
@ -47,21 +49,14 @@ internal data class AnonymousUser(private val accountType: String) : User, Parce
override val server = Server(URI.create(""), MainApp.MINIMUM_SUPPORTED_SERVER_VERSION)
override val isAnonymous = true
override fun toPlatformAccount(): Account {
return Account(accountName, accountType)
}
override fun toPlatformAccount(): Account = Account(accountName, accountType)
override fun toOwnCloudAccount(): OwnCloudAccount {
return OwnCloudAccount(Uri.EMPTY, OwnCloudBasicCredentials("", ""))
}
override fun toOwnCloudAccount(): OwnCloudAccount = OwnCloudAccount(Uri.EMPTY, OwnCloudBasicCredentials("", ""))
override fun nameEquals(user: User?): Boolean {
return user?.accountName.equals(accountName, true)
}
override fun nameEquals(user: User?): Boolean = user?.accountName.equals(accountName, true)
override fun nameEquals(accountName: CharSequence?): Boolean {
return accountName?.toString().equals(this.accountType, true)
}
override fun nameEquals(accountName: CharSequence?): Boolean =
accountName?.toString().equals(this.accountType, true)
override fun describeContents() = 0

View file

@ -2,14 +2,13 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.account;
import android.accounts.Account;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* This interface provides access to currently selected user.

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.account
@ -20,7 +20,9 @@ import java.net.URI
* This is a mock user object suitable for integration tests. Mocks obtained from code generators
* such as Mockito or MockK cannot be transported in Intent extras.
*/
data class MockUser(override val accountName: String, val accountType: String) : User, Parcelable {
data class MockUser(override val accountName: String, val accountType: String) :
User,
Parcelable {
constructor() : this(DEFAULT_MOCK_ACCOUNT_NAME, DEFAULT_MOCK_ACCOUNT_TYPE)
@ -42,21 +44,14 @@ data class MockUser(override val accountName: String, val accountType: String) :
override val server = Server(URI.create(""), MainApp.MINIMUM_SUPPORTED_SERVER_VERSION)
override val isAnonymous = false
override fun toPlatformAccount(): Account {
return Account(accountName, accountType)
}
override fun toPlatformAccount(): Account = Account(accountName, accountType)
override fun toOwnCloudAccount(): OwnCloudAccount {
return OwnCloudAccount(Uri.EMPTY, OwnCloudBasicCredentials("", ""))
}
override fun toOwnCloudAccount(): OwnCloudAccount = OwnCloudAccount(Uri.EMPTY, OwnCloudBasicCredentials("", ""))
override fun nameEquals(user: User?): Boolean {
return user?.accountName.equals(accountName, true)
}
override fun nameEquals(user: User?): Boolean = user?.accountName.equals(accountName, true)
override fun nameEquals(accountName: CharSequence?): Boolean {
return accountName?.toString().equals(this.accountType, true)
}
override fun nameEquals(accountName: CharSequence?): Boolean =
accountName?.toString().equals(this.accountType, true)
override fun describeContents() = 0

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.account
@ -41,21 +41,14 @@ internal data class RegisteredUser(
return account.name
}
override fun toPlatformAccount(): Account {
return account
}
override fun toPlatformAccount(): Account = account
override fun toOwnCloudAccount(): OwnCloudAccount {
return ownCloudAccount
}
override fun toOwnCloudAccount(): OwnCloudAccount = ownCloudAccount
override fun nameEquals(user: User?): Boolean {
return nameEquals(user?.accountName)
}
override fun nameEquals(user: User?): Boolean = nameEquals(user?.accountName)
override fun nameEquals(accountName: CharSequence?): Boolean {
return accountName?.toString().equals(this.accountName, true)
}
override fun nameEquals(accountName: CharSequence?): Boolean =
accountName?.toString().equals(this.accountName, true)
override fun describeContents() = 0

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.account

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.account
@ -11,7 +11,9 @@ import android.accounts.Account
import android.os.Parcelable
import com.owncloud.android.lib.common.OwnCloudAccount
interface User : Parcelable, com.nextcloud.common.User {
interface User :
Parcelable,
com.nextcloud.common.User {
override val accountName: String
val server: Server
val isAnonymous: Boolean

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.account;

View file

@ -1,9 +1,9 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2023 TSI-mc
* SPDX-FileCopyrightText: 2023-2024 TSI-mc <surinder.kumar@t-systems.com>
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.account;
@ -18,10 +18,11 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import com.nextcloud.client.onboarding.FirstRunActivity;
import com.nextcloud.common.NextcloudClient;
import com.nextcloud.utils.extensions.AccountExtensionsKt;
import com.nmc.android.ui.LauncherActivity;
import com.owncloud.android.MainApp;
import com.owncloud.android.R;
import com.owncloud.android.authentication.AuthenticatorActivity;
@ -112,25 +113,78 @@ public class UserAccountManagerImpl implements UserAccountManager {
@Override
public boolean exists(Account account) {
Account[] nextcloudAccounts = getAccounts();
try {
if (account == null) {
Log_OC.d(TAG, "account is null");
return false;
}
Account[] nextcloudAccounts = getAccounts();
if (nextcloudAccounts.length == 0) {
Log_OC.d(TAG, "nextcloudAccounts are empty");
return false;
}
if (account.name.isEmpty()) {
Log_OC.d(TAG, "account name is empty");
return false;
}
if (account != null && account.name != null) {
int lastAtPos = account.name.lastIndexOf('@');
if (lastAtPos == -1) {
Log_OC.d(TAG, "lastAtPos cannot be found");
return false;
}
boolean isLastAtPosInBoundsForHostAndPort = lastAtPos + 1 < account.name.length();
if (!isLastAtPosInBoundsForHostAndPort) {
Log_OC.d(TAG, "lastAtPos not in bounds");
return false;
}
String hostAndPort = account.name.substring(lastAtPos + 1);
String username = account.name.substring(0, lastAtPos);
if (hostAndPort.isEmpty() || username.isEmpty()) {
Log_OC.d(TAG, "hostAndPort or username is empty");
return false;
}
String otherHostAndPort;
String otherUsername;
for (Account otherAccount : nextcloudAccounts) {
// Skip null accounts or accounts with null names
if (otherAccount == null || otherAccount.name.isEmpty()) {
continue;
}
lastAtPos = otherAccount.name.lastIndexOf('@');
// Skip invalid account names
if (lastAtPos == -1) {
continue;
}
boolean isLastAtPosInBoundsForOtherHostAndPort = lastAtPos + 1 < otherAccount.name.length();
if (!isLastAtPosInBoundsForOtherHostAndPort) {
continue;
}
otherHostAndPort = otherAccount.name.substring(lastAtPos + 1);
otherUsername = otherAccount.name.substring(0, lastAtPos);
if (otherHostAndPort.equals(hostAndPort) &&
otherUsername.equalsIgnoreCase(username)) {
return true;
}
}
return false;
} catch (Exception e) {
Log_OC.d(TAG, "Exception caught at UserAccountManagerImpl.exists(): " + e);
return false;
}
return false;
}
@Override
@ -180,19 +234,20 @@ public class UserAccountManagerImpl implements UserAccountManager {
*/
@Nullable
private User createUserFromAccount(@NonNull Account account) {
if (AccountExtensionsKt.isAnonymous(account, context)) {
Context safeContext = context != null ? context : MainApp.getAppContext();
if (safeContext == null) {
Log_OC.e(TAG, "Unable to obtain a valid context");
return null;
}
if (context == null) {
Log_OC.d(TAG, "Context is null MainApp.getAppContext() used");
context = MainApp.getAppContext();
if (AccountExtensionsKt.isAnonymous(account, safeContext)) {
return null;
}
OwnCloudAccount ownCloudAccount;
try {
ownCloudAccount = new OwnCloudAccount(account, context);
} catch (AccountUtils.AccountNotFoundException ex) {
ownCloudAccount = new OwnCloudAccount(account, safeContext);
} catch (Exception ex) {
return null;
}
@ -212,7 +267,7 @@ public class UserAccountManagerImpl implements UserAccountManager {
*/
String serverAddressStr = accountManager.getUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL);
if (serverAddressStr == null || serverAddressStr.isEmpty()) {
return AnonymousUser.fromContext(context);
return AnonymousUser.fromContext(safeContext);
}
URI serverUri = URI.create(serverAddressStr); // TODO: validate
@ -398,6 +453,10 @@ public class UserAccountManagerImpl implements UserAccountManager {
@Override
public void startAccountCreation(final Activity activity) {
// skipping AuthenticatorActivity redirection when user is on Launcher or FirstRun Activity
if (activity instanceof LauncherActivity || activity instanceof FirstRunActivity) return;
Intent intent = new Intent(context, AuthenticatorActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.appinfo

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.appinfo
@ -16,17 +16,11 @@ class AppInfoImpl : AppInfo {
override val versionCode: Int = BuildConfig.VERSION_CODE
override val isDebugBuild: Boolean = BuildConfig.DEBUG
override fun getAppVersion(context: Context): String {
return try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
if (pInfo != null) {
pInfo.versionName
} else {
"n/a"
}
} catch (e: PackageManager.NameNotFoundException) {
Log_OC.e(this, "Trying to get packageName", e.cause)
"n/a"
}
override fun getAppVersion(context: Context): String = try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
packageInfo.versionName ?: "n/a"
} catch (e: PackageManager.NameNotFoundException) {
Log_OC.e(this, "Trying to get packageName", e.cause)
"n/a"
}
}

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.appinfo
@ -12,7 +12,5 @@ import dagger.Provides
@Module
class AppInfoModule {
@Provides
fun appInfo(): AppInfo {
return AppInfoImpl()
}
fun appInfo(): AppInfo = AppInfoImpl()
}

View file

@ -1,66 +1,57 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.assistant
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nextcloud.client.assistant.model.ScreenOverlayState
import com.nextcloud.client.assistant.model.ScreenState
import com.nextcloud.client.assistant.repository.AssistantRepositoryType
import com.owncloud.android.R
import com.owncloud.android.lib.resources.assistant.model.Task
import com.owncloud.android.lib.resources.assistant.model.TaskType
import com.owncloud.android.lib.resources.assistant.v2.model.Task
import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.lang.ref.WeakReference
class AssistantViewModel(
private val repository: AssistantRepositoryType,
private val context: WeakReference<Context>
) : ViewModel() {
class AssistantViewModel(private val repository: AssistantRepositoryType) : ViewModel() {
sealed class State {
data object Idle : State()
data object Loading : State()
data class Error(val messageId: Int) : State()
data class TaskCreated(val messageId: Int) : State()
data class TaskDeleted(val messageId: Int) : State()
}
private val _screenState = MutableStateFlow<ScreenState?>(null)
val screenState: StateFlow<ScreenState?> = _screenState
private val _state = MutableStateFlow<State>(State.Loading)
val state: StateFlow<State> = _state
private val _screenOverlayState = MutableStateFlow<ScreenOverlayState?>(null)
val screenOverlayState: StateFlow<ScreenOverlayState?> = _screenOverlayState
private val _selectedTaskType = MutableStateFlow<TaskType?>(null)
val selectedTaskType: StateFlow<TaskType?> = _selectedTaskType
private val _snackbarMessageId = MutableStateFlow<Int?>(null)
val snackbarMessageId: StateFlow<Int?> = _snackbarMessageId
private val _taskTypes = MutableStateFlow<List<TaskType>?>(null)
val taskTypes: StateFlow<List<TaskType>?> = _taskTypes
private val _selectedTaskType = MutableStateFlow<TaskTypeData?>(null)
val selectedTaskType: StateFlow<TaskTypeData?> = _selectedTaskType
private var _taskList: List<Task>? = null
private val _taskTypes = MutableStateFlow<List<TaskTypeData>?>(null)
val taskTypes: StateFlow<List<TaskTypeData>?> = _taskTypes
private var taskList: List<Task>? = null
private val _filteredTaskList = MutableStateFlow<List<Task>?>(null)
val filteredTaskList: StateFlow<List<Task>?> = _filteredTaskList
init {
fetchTaskTypes()
fetchTaskList()
}
@Suppress("MagicNumber")
fun createTask(
input: String,
type: String
) {
fun createTask(input: String, taskType: TaskTypeData) {
viewModelScope.launch(Dispatchers.IO) {
val result = repository.createTask(input, type)
val result = repository.createTask(input, taskType)
val messageId = if (result.isSuccess) {
R.string.assistant_screen_task_create_success_message
@ -68,62 +59,73 @@ class AssistantViewModel(
R.string.assistant_screen_task_create_fail_message
}
_state.update {
State.TaskCreated(messageId)
}
updateSnackbarMessage(messageId)
delay(2000L)
fetchTaskList()
}
}
fun selectTaskType(task: TaskType) {
fun selectTaskType(task: TaskTypeData) {
_selectedTaskType.update {
filterTaskList(task.id)
task
}
fetchTaskList()
}
private fun fetchTaskTypes() {
viewModelScope.launch(Dispatchers.IO) {
val allTaskType = context.get()?.getString(R.string.assistant_screen_all_task_type)
val excludedIds = listOf("OCA\\ContextChat\\TextProcessing\\ContextChatTaskType")
val result = arrayListOf(TaskType(null, allTaskType, null))
val taskTypesResult = repository.getTaskTypes()
if (taskTypesResult.isSuccess) {
val excludedTaskTypes = taskTypesResult.resultData.types.filter { item -> item.id !in excludedIds }
result.addAll(excludedTaskTypes)
_taskTypes.update {
result.toList()
}
selectTaskType(result.first())
} else {
_state.update {
State.Error(R.string.assistant_screen_task_types_error_state_message)
}
if (taskTypesResult == null) {
updateSnackbarMessage(R.string.assistant_screen_task_types_error_state_message)
return@launch
}
if (taskTypesResult.isEmpty()) {
updateSnackbarMessage(R.string.assistant_screen_task_list_empty_message)
return@launch
}
_taskTypes.update {
taskTypesResult
}
selectTaskType(taskTypesResult.first())
}
}
fun fetchTaskList(appId: String = "assistant", onCompleted: () -> Unit = {}) {
fun fetchTaskList() {
viewModelScope.launch(Dispatchers.IO) {
val result = repository.getTaskList(appId)
if (result.isSuccess) {
_taskList = result.resultData.tasks
_screenState.update {
ScreenState.Refreshing
}
filterTaskList(_selectedTaskType.value?.id)
_state.update {
State.Idle
val taskType = _selectedTaskType.value?.id ?: return@launch
val result = repository.getTaskList(taskType)
if (result != null) {
taskList = result
_filteredTaskList.update {
taskList?.sortedByDescending { task ->
task.id
}
}
onCompleted()
updateSnackbarMessage(null)
} else {
_state.update {
State.Error(R.string.assistant_screen_task_list_error_state_message)
}
updateSnackbarMessage(R.string.assistant_screen_task_list_error_state_message)
}
updateScreenState()
}
}
private fun updateScreenState() {
_screenState.update {
if (_filteredTaskList.value?.isEmpty() == true) {
ScreenState.EmptyContent
} else {
ScreenState.Content
}
}
}
@ -138,9 +140,7 @@ class AssistantViewModel(
R.string.assistant_screen_task_delete_fail_message
}
_state.update {
State.TaskDeleted(messageId)
}
updateSnackbarMessage(messageId)
if (result.isSuccess) {
removeTaskFromList(id)
@ -148,27 +148,15 @@ class AssistantViewModel(
}
}
fun resetState() {
_state.update {
State.Idle
fun updateSnackbarMessage(value: Int?) {
_snackbarMessageId.update {
value
}
}
private fun filterTaskList(taskTypeId: String?) {
if (taskTypeId == null) {
_filteredTaskList.update {
_taskList
}
} else {
_filteredTaskList.update {
_taskList?.filter { it.type == taskTypeId }
}
}
_filteredTaskList.update {
it?.sortedByDescending { task ->
task.id
}
fun updateScreenState(value: ScreenOverlayState?) {
_screenOverlayState.update {
value
}
}

View file

@ -1,14 +1,13 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.assistant
import android.app.Activity
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@ -25,180 +24,250 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.material3.pulltorefresh.pullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.nextcloud.client.assistant.component.AddTaskAlertDialog
import com.nextcloud.client.assistant.component.CenterText
import com.nextcloud.client.assistant.taskTypes.TaskTypesRow
import com.nextcloud.client.assistant.task.TaskView
import com.nextcloud.client.assistant.extensions.getInputTitle
import com.nextcloud.client.assistant.model.ScreenOverlayState
import com.nextcloud.client.assistant.model.ScreenState
import com.nextcloud.client.assistant.repository.AssistantMockRepository
import com.nextcloud.client.assistant.task.TaskView
import com.nextcloud.client.assistant.taskTypes.TaskTypesRow
import com.nextcloud.ui.composeActivity.ComposeActivity
import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog
import com.nextcloud.ui.composeComponents.bottomSheet.MoreActionsBottomSheet
import com.owncloud.android.R
import com.owncloud.android.lib.resources.assistant.model.Task
import com.owncloud.android.lib.resources.assistant.model.TaskType
import com.owncloud.android.lib.resources.assistant.v2.model.Task
import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData
import com.owncloud.android.lib.resources.status.OCCapability
import com.owncloud.android.utils.DisplayUtils
import kotlinx.coroutines.delay
import java.lang.ref.WeakReference
import kotlinx.coroutines.launch
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AssistantScreen(viewModel: AssistantViewModel, activity: Activity) {
val state by viewModel.state.collectAsState()
fun AssistantScreen(viewModel: AssistantViewModel, capability: OCCapability, activity: Activity) {
val messageId by viewModel.snackbarMessageId.collectAsState()
val screenOverlayState by viewModel.screenOverlayState.collectAsState()
val selectedTaskType by viewModel.selectedTaskType.collectAsState()
val filteredTaskList by viewModel.filteredTaskList.collectAsState()
val screenState by viewModel.screenState.collectAsState()
val taskTypes by viewModel.taskTypes.collectAsState()
var showAddTaskAlertDialog by remember { mutableStateOf(false) }
var showDeleteTaskAlertDialog by remember { mutableStateOf(false) }
var taskIdToDeleted: Long? by remember {
mutableStateOf(null)
}
val scope = rememberCoroutineScope()
val pullRefreshState = rememberPullToRefreshState()
@Suppress("MagicNumber")
if (pullRefreshState.isRefreshing) {
LaunchedEffect(true) {
delay(1500)
viewModel.fetchTaskList(onCompleted = {
pullRefreshState.endRefresh()
})
}
Box(
modifier = Modifier.pullToRefresh(
screenState == ScreenState.Refreshing,
pullRefreshState,
onRefresh = {
scope.launch {
delay(1500)
viewModel.fetchTaskList()
}
}
)
) {
ShowScreenState(screenState, selectedTaskType, taskTypes, viewModel, filteredTaskList, capability)
ShowLinearProgressIndicator(screenState, pullRefreshState)
AddFloatingActionButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
selectedTaskType,
viewModel
)
}
Box(Modifier.nestedScroll(pullRefreshState.nestedScrollConnection)) {
if (state == AssistantViewModel.State.Loading || pullRefreshState.isRefreshing) {
showSnackBarMessage(messageId, activity, viewModel)
ShowOverlayState(screenOverlayState, activity, viewModel)
}
@Composable
private fun ShowScreenState(
screenState: ScreenState?,
selectedTaskType: TaskTypeData?,
taskTypes: List<TaskTypeData>?,
viewModel: AssistantViewModel,
filteredTaskList: List<Task>?,
capability: OCCapability
) {
when (screenState) {
ScreenState.Refreshing -> {
CenterText(text = stringResource(id = R.string.assistant_screen_loading))
} else {
if (filteredTaskList.isNullOrEmpty()) {
EmptyTaskList(selectedTaskType, taskTypes, viewModel)
} else {
AssistantContent(
filteredTaskList!!,
taskTypes,
selectedTaskType,
viewModel,
showDeleteTaskAlertDialog = { taskId ->
taskIdToDeleted = taskId
showDeleteTaskAlertDialog = true
}
)
}
}
if (pullRefreshState.isRefreshing) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
} else {
LinearProgressIndicator(progress = { pullRefreshState.progress }, modifier = Modifier.fillMaxWidth())
ScreenState.EmptyContent -> {
EmptyTaskList(selectedTaskType, taskTypes, viewModel)
}
if (selectedTaskType?.name != stringResource(id = R.string.assistant_screen_all_task_type)) {
FloatingActionButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
onClick = {
showAddTaskAlertDialog = true
}
) {
Icon(Icons.Filled.Add, "Add Task Icon")
}
}
}
ScreenState(state, activity, viewModel)
if (showDeleteTaskAlertDialog) {
taskIdToDeleted?.let { id ->
SimpleAlertDialog(
title = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_title),
description = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_description),
dismiss = { showDeleteTaskAlertDialog = false },
onComplete = { viewModel.deleteTask(id) }
ScreenState.Content -> {
AssistantContent(
filteredTaskList ?: listOf(),
taskTypes,
selectedTaskType,
viewModel,
capability
)
}
}
if (showAddTaskAlertDialog) {
selectedTaskType?.let { taskType ->
AddTaskAlertDialog(
title = taskType.name,
description = taskType.description,
addTask = { input ->
taskType.id?.let {
viewModel.createTask(input = input, type = it)
}
},
dismiss = {
showAddTaskAlertDialog = false
}
)
}
null -> Unit
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ShowLinearProgressIndicator(screenState: ScreenState?, pullToRefreshState: PullToRefreshState) {
if (screenState == ScreenState.Refreshing) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
} else {
LinearProgressIndicator(
progress = { pullToRefreshState.distanceFraction },
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable
private fun ScreenState(
state: AssistantViewModel.State,
activity: Activity,
private fun AddFloatingActionButton(
modifier: Modifier,
selectedTaskType: TaskTypeData?,
viewModel: AssistantViewModel
) {
val messageId: Int? = when (state) {
is AssistantViewModel.State.Error -> {
state.messageId
}
is AssistantViewModel.State.TaskCreated -> {
state.messageId
}
is AssistantViewModel.State.TaskDeleted -> {
state.messageId
}
else -> {
null
FloatingActionButton(
modifier = modifier,
onClick = {
selectedTaskType?.let {
val newState = ScreenOverlayState.AddTask(it, "")
viewModel.updateScreenState(newState)
}
}
) {
Icon(Icons.Filled.Add, "Add Task Icon")
}
}
private fun showSnackBarMessage(messageId: Int?, activity: Activity, viewModel: AssistantViewModel) {
messageId?.let {
DisplayUtils.showSnackMessage(
activity,
stringResource(id = messageId)
activity.getString(it)
)
viewModel.resetState()
viewModel.updateSnackbarMessage(null)
}
}
@Suppress("LongMethod")
@Composable
private fun ShowOverlayState(state: ScreenOverlayState?, activity: Activity, viewModel: AssistantViewModel) {
when (state) {
is ScreenOverlayState.AddTask -> {
AddTaskAlertDialog(
title = state.taskType.name,
description = state.taskType.description,
defaultInput = state.input,
addTask = { input ->
state.taskType.let { taskType ->
viewModel.createTask(input = input, taskType = taskType)
}
},
dismiss = {
viewModel.updateScreenState(null)
}
)
}
is ScreenOverlayState.DeleteTask -> {
SimpleAlertDialog(
title = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_title),
description = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_description),
dismiss = { viewModel.updateScreenState(null) },
onComplete = { viewModel.deleteTask(state.id) }
)
}
is ScreenOverlayState.TaskActions -> {
val actions = state.getActions(activity, onEditCompleted = { addTask ->
viewModel.updateScreenState(addTask)
}, onDeleteCompleted = { deleteTask ->
viewModel.updateScreenState(deleteTask)
})
MoreActionsBottomSheet(
title = state.task.getInputTitle(),
actions = actions,
dismiss = { viewModel.updateScreenState(null) }
)
}
else -> Unit
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun AssistantContent(
taskList: List<Task>,
taskTypes: List<TaskType>?,
selectedTaskType: TaskType?,
taskTypes: List<TaskTypeData>?,
selectedTaskType: TaskTypeData?,
viewModel: AssistantViewModel,
showDeleteTaskAlertDialog: (Long) -> Unit
capability: OCCapability
) {
LazyColumn(
Column(modifier = Modifier.fillMaxSize()) {
taskTypes?.let {
TaskTypesRow(selectedTaskType, data = taskTypes) { task ->
viewModel.selectTaskType(task)
}
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(12.dp)
) {
items(taskList) { task ->
TaskView(
task,
capability,
showTaskActions = {
val newState = ScreenOverlayState.TaskActions(task)
viewModel.updateScreenState(newState)
}
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@Composable
private fun EmptyTaskList(
selectedTaskType: TaskTypeData?,
taskTypes: List<TaskTypeData>?,
viewModel: AssistantViewModel
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
stickyHeader {
taskTypes?.let {
TaskTypesRow(selectedTaskType, data = taskTypes) { task ->
viewModel.selectTaskType(task)
}
@ -206,39 +275,15 @@ private fun AssistantContent(
Spacer(modifier = Modifier.height(8.dp))
}
items(taskList) { task ->
TaskView(task, showDeleteTaskAlertDialog = { showDeleteTaskAlertDialog(task.id) })
Spacer(modifier = Modifier.height(8.dp))
}
}
}
@Composable
private fun EmptyTaskList(selectedTaskType: TaskType?, taskTypes: List<TaskType>?, viewModel: AssistantViewModel) {
val text = if (selectedTaskType?.name == stringResource(id = R.string.assistant_screen_all_task_type)) {
stringResource(id = R.string.assistant_screen_no_task_available_for_all_task_filter_text)
} else {
stringResource(
id = R.string.assistant_screen_no_task_available_text,
selectedTaskType?.name ?: ""
CenterText(
text = stringResource(
id = R.string.assistant_screen_create_a_new_task_from_bottom_right_text
)
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
TaskTypesRow(selectedTaskType, data = taskTypes) { task ->
viewModel.selectTaskType(task)
}
Spacer(modifier = Modifier.height(8.dp))
CenterText(text = text)
}
}
@Suppress("MagicNumber")
@Composable
@Preview
private fun AssistantScreenPreview() {
@ -246,16 +291,17 @@ private fun AssistantScreenPreview() {
MaterialTheme(
content = {
AssistantScreen(
viewModel = AssistantViewModel(
repository = mockRepository,
context = WeakReference(LocalContext.current)
),
activity = ComposeActivity()
viewModel = AssistantViewModel(repository = mockRepository),
activity = ComposeActivity(),
capability = OCCapability().apply {
versionMayor = 30
}
)
}
)
}
@Suppress("MagicNumber")
@Composable
@Preview
private fun AssistantEmptyScreenPreview() {
@ -263,11 +309,11 @@ private fun AssistantEmptyScreenPreview() {
MaterialTheme(
content = {
AssistantScreen(
viewModel = AssistantViewModel(
repository = mockRepository,
context = WeakReference(LocalContext.current)
),
activity = ComposeActivity()
viewModel = AssistantViewModel(repository = mockRepository),
activity = ComposeActivity(),
capability = OCCapability().apply {
versionMayor = 30
}
)
}
)

View file

@ -1,9 +1,9 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.assistant.component
@ -22,13 +22,19 @@ import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog
import com.owncloud.android.R
@Composable
fun AddTaskAlertDialog(title: String?, description: String?, addTask: (String) -> Unit, dismiss: () -> Unit) {
fun AddTaskAlertDialog(
title: String,
description: String?,
defaultInput: String = "",
addTask: (String) -> Unit,
dismiss: () -> Unit
) {
var input by remember {
mutableStateOf("")
mutableStateOf(defaultInput)
}
SimpleAlertDialog(
title = title ?: "",
title = title,
description = description ?: "",
dismiss = { dismiss() },
onComplete = {

View file

@ -1,9 +1,9 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.assistant.component
@ -13,8 +13,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import com.owncloud.android.R
@Composable
fun CenterText(text: String) {
@ -22,7 +24,8 @@ fun CenterText(text: String) {
Text(
text = text,
fontSize = 18.sp,
textAlign = TextAlign.Center
textAlign = TextAlign.Center,
color = colorResource(R.color.text_color)
)
}
}

View file

@ -3,40 +3,130 @@
*
* SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud
* contributors
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: MIT
*/
package com.nextcloud.client.assistant.extensions
import android.content.Context
import com.nextcloud.utils.date.DateFormatPattern
import com.nextcloud.utils.date.DateFormatter
import com.owncloud.android.R
import com.owncloud.android.lib.resources.assistant.model.Task
import com.owncloud.android.lib.resources.assistant.v2.model.Task
import com.owncloud.android.lib.resources.status.NextcloudVersion
import com.owncloud.android.lib.resources.status.OCCapability
import java.util.concurrent.TimeUnit
fun Task.getInputAndOutput(): String {
val inputText = input?.input ?: ""
val outputText = output?.output ?: ""
return "$inputText\n\n$outputText"
}
fun Task.getInput(): String? = input?.input
@Suppress("MagicNumber")
fun Task.statusData(): Pair<Int, Int> {
return when (status) {
0L -> {
Pair(R.drawable.ic_unknown, R.string.assistant_screen_unknown_task_status_text)
}
1L -> {
Pair(R.drawable.ic_clock, R.string.assistant_screen_scheduled_task_status_text)
}
2L -> {
Pair(R.drawable.ic_modification_desc, R.string.assistant_screen_running_task_text)
}
3L -> {
Pair(R.drawable.ic_info, R.string.assistant_screen_successful_task_text)
}
4L -> {
Pair(R.drawable.image_fail, R.string.assistant_screen_failed_task_text)
}
else -> {
Pair(R.drawable.ic_unknown, R.string.assistant_screen_unknown_task_status_text)
}
fun Task.getInputTitle(): String {
val maxTitleLength = 20
val title = getInput() ?: ""
return if (title.length > maxTitleLength) {
title.take(maxTitleLength) + "..."
} else {
title
}
}
// TODO add
fun Task.completionDateRepresentation(): String {
return completionExpectedAt ?: "TODO IMPLEMENT IT"
fun Task.getStatusIcon(capability: OCCapability): Int =
if (capability.version.isNewerOrEqual(NextcloudVersion.nextcloud_30)) {
getStatusIconV2()
} else {
getStatusIconV1()
}
private fun Task.getStatusIconV1(): Int = when (status) {
"0" -> {
R.drawable.ic_unknown
}
"1" -> {
R.drawable.ic_clock
}
"2" -> {
R.drawable.ic_modification_desc
}
"3" -> {
R.drawable.ic_check_circle_outline
}
"4" -> {
R.drawable.image_fail
}
else -> {
R.drawable.ic_unknown
}
}
private fun Task.getStatusIconV2(): Int = when (status) {
"STATUS_UNKNOWN" -> {
R.drawable.ic_unknown
}
"STATUS_SCHEDULED" -> {
R.drawable.ic_clock
}
"STATUS_RUNNING" -> {
R.drawable.ic_modification_desc
}
"STATUS_SUCCESSFUL" -> {
R.drawable.ic_check_circle_outline
}
"STATUS_FAILED" -> {
R.drawable.image_fail
}
else -> {
R.drawable.ic_unknown
}
}
@Suppress("MagicNumber")
fun Task.getModifiedAtRepresentation(context: Context): String? {
if (lastUpdated == null) {
return null
}
val modifiedAt = lastUpdated!!.toLong()
val currentTime = System.currentTimeMillis() / 1000
val timeDifference = (currentTime - modifiedAt).toInt()
val timeDifferenceInMinutes = (timeDifference / 60)
val timeDifferenceInHours = (timeDifference / 3600)
return when {
timeDifference < 0 -> {
context.getString(R.string.common_now)
}
timeDifference < TimeUnit.MINUTES.toSeconds(1) -> {
context.resources.getQuantityString(R.plurals.time_seconds_ago, timeDifference, timeDifference)
}
timeDifference < TimeUnit.HOURS.toSeconds(1) -> {
context.resources.getQuantityString(
R.plurals.time_minutes_ago,
timeDifferenceInMinutes,
timeDifferenceInMinutes
)
}
timeDifference < TimeUnit.DAYS.toSeconds(1) -> {
context.resources.getQuantityString(
R.plurals.time_hours_ago,
timeDifferenceInHours,
timeDifferenceInHours
)
}
else -> {
DateFormatter.timestampToDateRepresentation(modifiedAt, DateFormatPattern.MonthWithDate)
}
}
}

View file

@ -0,0 +1,79 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.assistant.model
import android.app.Activity
import com.nextcloud.client.assistant.extensions.getInput
import com.nextcloud.client.assistant.extensions.getInputAndOutput
import com.nextcloud.utils.extensions.showShareIntent
import com.owncloud.android.R
import com.owncloud.android.lib.resources.assistant.v2.model.Task
import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData
import com.owncloud.android.utils.ClipboardUtil
sealed class ScreenOverlayState {
data class DeleteTask(val id: Long) : ScreenOverlayState()
data class AddTask(val taskType: TaskTypeData, val input: String) : ScreenOverlayState()
data class TaskActions(val task: Task) : ScreenOverlayState() {
private fun getInputAndOutput(): String = task.getInputAndOutput()
private fun getInput(): String? = task.getInput()
private fun getCopyToClipboardAction(activity: Activity): Triple<Int, Int, () -> Unit> = Triple(
R.drawable.ic_content_copy,
R.string.common_copy
) {
ClipboardUtil.copyToClipboard(activity, getInputAndOutput(), showToast = false)
}
private fun getShareAction(activity: Activity): Triple<Int, Int, () -> Unit> = Triple(
R.drawable.ic_share,
R.string.common_share
) {
activity.showShareIntent(getInputAndOutput())
}
private fun getEditAction(activity: Activity, onComplete: (AddTask) -> Unit): Triple<Int, Int, () -> Unit> =
Triple(
R.drawable.ic_edit,
R.string.action_edit
) {
val taskType = TaskTypeData(
task.type,
activity.getString(R.string.assistant_screen_add_task_alert_dialog_title),
null,
emptyMap(),
emptyMap()
)
val newState = AddTask(taskType, getInput() ?: "")
onComplete(newState)
}
private fun getDeleteAction(onComplete: (DeleteTask) -> Unit): Triple<Int, Int, () -> Unit> = Triple(
R.drawable.ic_delete,
R.string.assistant_screen_task_more_actions_bottom_sheet_delete_action
) {
val newState = DeleteTask(task.id)
onComplete(newState)
}
fun getActions(
activity: Activity,
onEditCompleted: (AddTask) -> Unit,
onDeleteCompleted: (DeleteTask) -> Unit
): List<Triple<Int, Int, () -> Unit>> = listOf(
getShareAction(activity),
getCopyToClipboardAction(activity),
getEditAction(activity, onComplete = {
onEditCompleted(it)
}),
getDeleteAction(onComplete = {
onDeleteCompleted(it)
})
)
}
}

View file

@ -0,0 +1,14 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.assistant.model
enum class ScreenState {
Refreshing,
EmptyContent,
Content
}

View file

@ -1,128 +1,68 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.assistant.repository
import com.nextcloud.utils.extensions.getRandomString
import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.resources.assistant.model.Task
import com.owncloud.android.lib.resources.assistant.model.TaskList
import com.owncloud.android.lib.resources.assistant.model.TaskType
import com.owncloud.android.lib.resources.assistant.model.TaskTypes
import com.owncloud.android.lib.resources.assistant.v2.model.Shape
import com.owncloud.android.lib.resources.assistant.v2.model.Task
import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput
import com.owncloud.android.lib.resources.assistant.v2.model.TaskOutput
import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData
@Suppress("MagicNumber")
class AssistantMockRepository(private val giveEmptyTasks: Boolean = false) : AssistantRepositoryType {
override fun getTaskTypes(): RemoteOperationResult<TaskTypes> {
return RemoteOperationResult<TaskTypes>(RemoteOperationResult.ResultCode.OK).apply {
resultData = TaskTypes(
listOf(
TaskType("1", "FreePrompt", "You can create free prompt text"),
TaskType("2", "Generate Headline", "You can create generate headline text")
override fun getTaskTypes(): List<TaskTypeData> = listOf(
TaskTypeData(
id = "core:text2text",
name = "Free text to text prompt",
description = "Runs an arbitrary prompt through a language model that returns a reply",
inputShape = mapOf(
"input" to Shape(
name = "Prompt",
description = "Describe a task that you want the assistant to do or ask a question",
type = "Text"
)
),
outputShape = mapOf(
"output" to Shape(
name = "Generated reply",
description = "The generated text from the assistant",
type = "Text"
)
)
}
}
)
)
override fun createTask(input: String, type: String): RemoteOperationResult<Void> {
return RemoteOperationResult<Void>(RemoteOperationResult.ResultCode.OK)
}
override fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult<Void> =
RemoteOperationResult<Void>(RemoteOperationResult.ResultCode.OK)
override fun getTaskList(appId: String): RemoteOperationResult<TaskList> {
val taskList = if (giveEmptyTasks) {
TaskList(listOf())
} else {
TaskList(
listOf(
Task(
1,
"FreePrompt",
null,
"12",
"",
"Give me some long text 1",
"Lorem ipsum".getRandomString(100),
""
),
Task(
2,
"GenerateHeadline",
null,
"12",
"",
"Give me some text 2",
"Lorem".getRandomString(100),
"",
""
),
Task(
3,
"FreePrompt",
null,
"12",
"",
"Give me some text 3",
"Lorem".getRandomString(300),
"",
""
),
Task(
4,
"FreePrompt",
null,
"12",
"",
"Give me some text 4",
"Lorem".getRandomString(300),
"",
""
),
Task(
5,
"FreePrompt",
null,
"12",
"",
"Give me some text 5",
"Lorem".getRandomString(300),
"",
""
),
Task(
6,
"FreePrompt",
null,
"12",
"",
"Give me some text 6",
"Lorem".getRandomString(300),
"",
""
),
Task(
7,
"FreePrompt",
null,
"12",
"",
"Give me some text 7",
"Lorem".getRandomString(300),
"",
""
)
)
override fun getTaskList(taskType: String): List<Task> = if (giveEmptyTasks) {
listOf()
} else {
listOf(
Task(
1,
"FreePrompt",
null,
"12",
"",
TaskInput("Give me some long text 1"),
TaskOutput("Lorem ipsum".getRandomString(100)),
1707692337,
1707692337,
1707692337,
1707692337,
1707692337
)
}
return RemoteOperationResult<TaskList>(RemoteOperationResult.ResultCode.OK).apply {
resultData = taskList
}
)
}
override fun deleteTask(id: Long): RemoteOperationResult<Void> {
return RemoteOperationResult<Void>(RemoteOperationResult.ResultCode.OK)
}
override fun deleteTask(id: Long): RemoteOperationResult<Void> =
RemoteOperationResult<Void>(RemoteOperationResult.ResultCode.OK)
}

View file

@ -1,39 +1,80 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.assistant.repository
import com.nextcloud.common.NextcloudClient
import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.resources.assistant.CreateTaskRemoteOperation
import com.owncloud.android.lib.resources.assistant.DeleteTaskRemoteOperation
import com.owncloud.android.lib.resources.assistant.GetTaskListRemoteOperation
import com.owncloud.android.lib.resources.assistant.GetTaskTypesRemoteOperation
import com.owncloud.android.lib.resources.assistant.model.TaskList
import com.owncloud.android.lib.resources.assistant.model.TaskTypes
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
import com.owncloud.android.lib.resources.assistant.v1.CreateTaskRemoteOperationV1
import com.owncloud.android.lib.resources.assistant.v1.DeleteTaskRemoteOperationV1
import com.owncloud.android.lib.resources.assistant.v1.GetTaskListRemoteOperationV1
import com.owncloud.android.lib.resources.assistant.v1.GetTaskTypesRemoteOperationV1
import com.owncloud.android.lib.resources.assistant.v1.model.toV2
import com.owncloud.android.lib.resources.assistant.v2.CreateTaskRemoteOperationV2
import com.owncloud.android.lib.resources.assistant.v2.DeleteTaskRemoteOperationV2
import com.owncloud.android.lib.resources.assistant.v2.GetTaskListRemoteOperationV2
import com.owncloud.android.lib.resources.assistant.v2.GetTaskTypesRemoteOperationV2
import com.owncloud.android.lib.resources.assistant.v2.model.Task
import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData
import com.owncloud.android.lib.resources.status.NextcloudVersion
import com.owncloud.android.lib.resources.status.OCCapability
class AssistantRepository(private val client: NextcloudClient) : AssistantRepositoryType {
class AssistantRepository(private val client: NextcloudClient, capability: OCCapability) : AssistantRepositoryType {
override fun getTaskTypes(): RemoteOperationResult<TaskTypes> {
return GetTaskTypesRemoteOperation().execute(client)
private val supportsV2 = capability.version.isNewerOrEqual(NextcloudVersion.nextcloud_30)
@Suppress("ReturnCount")
override fun getTaskTypes(): List<TaskTypeData>? {
if (supportsV2) {
val result = GetTaskTypesRemoteOperationV2().execute(client)
if (result.isSuccess) {
return result.resultData
}
} else {
val result = GetTaskTypesRemoteOperationV1().execute(client)
if (result.isSuccess) {
return result.resultData.toV2()
}
}
return null
}
override fun createTask(
input: String,
type: String
): RemoteOperationResult<Void> {
return CreateTaskRemoteOperation(input, type).execute(client)
override fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult<Void> = if (supportsV2) {
CreateTaskRemoteOperationV2(input, taskType).execute(client)
} else {
if (taskType.id.isNullOrEmpty()) {
RemoteOperationResult<Void>(ResultCode.CANCELLED)
} else {
CreateTaskRemoteOperationV1(input, taskType.id!!).execute(client)
}
}
override fun getTaskList(appId: String): RemoteOperationResult<TaskList> {
return GetTaskListRemoteOperation(appId).execute(client)
@Suppress("ReturnCount")
override fun getTaskList(taskType: String): List<Task>? {
if (supportsV2) {
val result = GetTaskListRemoteOperationV2(taskType).execute(client)
if (result.isSuccess) {
return result.resultData.tasks.filter { it.appId == "assistant" }
}
} else {
val result = GetTaskListRemoteOperationV1("assistant").execute(client)
if (result.isSuccess) {
return result.resultData.toV2().tasks.filter { it.type == taskType }
}
}
return null
}
override fun deleteTask(id: Long): RemoteOperationResult<Void> {
return DeleteTaskRemoteOperation(id).execute(client)
override fun deleteTask(id: Long): RemoteOperationResult<Void> = if (supportsV2) {
DeleteTaskRemoteOperationV2(id).execute(client)
} else {
DeleteTaskRemoteOperationV1(id).execute(client)
}
}

View file

@ -1,25 +1,21 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.assistant.repository
import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.resources.assistant.model.TaskList
import com.owncloud.android.lib.resources.assistant.model.TaskTypes
import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData
interface AssistantRepositoryType {
fun getTaskTypes(): RemoteOperationResult<TaskTypes>
fun getTaskTypes(): List<TaskTypeData>?
fun createTask(
input: String,
type: String
): RemoteOperationResult<Void>
fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult<Void>
fun getTaskList(appId: String): RemoteOperationResult<TaskList>
fun getTaskList(taskType: String): List<com.owncloud.android.lib.resources.assistant.v2.model.Task>?
fun deleteTask(id: Long): RemoteOperationResult<Void>
}

View file

@ -0,0 +1,151 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.assistant.task
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.nextcloud.client.assistant.extensions.getModifiedAtRepresentation
import com.nextcloud.client.assistant.extensions.getStatusIcon
import com.owncloud.android.R
import com.owncloud.android.lib.resources.assistant.v2.model.Task
import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput
import com.owncloud.android.lib.resources.assistant.v2.model.TaskOutput
import com.owncloud.android.lib.resources.status.OCCapability
import java.util.concurrent.TimeUnit
@Composable
fun TaskStatusView(task: Task, capability: OCCapability) {
val context = LocalContext.current
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
val iconId = task.getStatusIcon(capability)
val description = task.getModifiedAtRepresentation(context)
Image(
painter = painterResource(id = iconId),
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(color = colorResource(R.color.text_color)),
contentDescription = "status icon"
)
description?.let {
Spacer(modifier = Modifier.width(6.dp))
Text(text = description, color = colorResource(R.color.text_color))
}
}
}
@Suppress("LongMethod", "MagicNumber")
@Composable
@Preview
private fun TaskStatusViewPreview() {
val currentTime = System.currentTimeMillis() / 1000
val tasks = listOf(
Task(
id = 1L,
type = "type1",
status = "STATUS_RUNNING",
userId = "user1",
appId = "app1",
input = TaskInput("input1"),
output = TaskOutput("output1"),
scheduledAt = currentTime.toInt(),
lastUpdated = currentTime.toInt()
),
Task(
id = 2L,
type = "type2",
status = "STATUS_SUCCESSFUL",
userId = "user2",
appId = "app2",
input = TaskInput("input2"),
output = TaskOutput("output2"),
lastUpdated = (currentTime - TimeUnit.MINUTES.toSeconds(5)).toInt()
),
Task(
id = 3L,
type = "type3",
status = "STATUS_RUNNING",
userId = "user3",
appId = "app3",
input = TaskInput("input3"),
output = TaskOutput("output3"),
lastUpdated = (currentTime - TimeUnit.HOURS.toSeconds(5)).toInt()
),
Task(
id = 4L,
type = "type4",
status = "STATUS_SUCCESSFUL",
userId = "user4",
appId = "app4",
input = TaskInput("input4"),
output = TaskOutput("output4"),
lastUpdated = (currentTime - TimeUnit.DAYS.toSeconds(5)).toInt()
),
Task(
id = 5L,
type = "type5",
status = "STATUS_SUCCESSFUL",
userId = "user5",
appId = "app5",
input = TaskInput("input5"),
output = TaskOutput("output5"),
lastUpdated = (currentTime - TimeUnit.DAYS.toSeconds(60)).toInt()
),
Task(
id = 6L,
type = "type7",
status = "STATUS_UNKNOWN",
userId = "user7",
appId = "app7",
input = TaskInput("input7"),
output = TaskOutput("output7"),
scheduledAt = null,
lastUpdated = null
)
)
LazyColumn {
items(tasks) {
TaskStatusView(
it,
OCCapability().apply {
versionMayor = 30
}
)
}
}
}

View file

@ -1,118 +1,120 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.assistant.task
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nextcloud.client.assistant.taskDetail.TaskDetailBottomSheet
import com.nextcloud.ui.composeComponents.bottomSheet.MoreActionsBottomSheet
import com.nextcloud.utils.extensions.getRandomString
import com.nextcloud.utils.extensions.truncateWithEllipsis
import com.owncloud.android.R
import com.owncloud.android.lib.resources.assistant.model.Task
import com.owncloud.android.lib.resources.assistant.v2.model.Task
import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput
import com.owncloud.android.lib.resources.assistant.v2.model.TaskOutput
import com.owncloud.android.lib.resources.status.OCCapability
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongMethod", "MagicNumber")
@Composable
fun TaskView(
task: Task,
showDeleteTaskAlertDialog: (Long) -> Unit
) {
fun TaskView(task: Task, capability: OCCapability, showTaskActions: () -> Unit) {
var showTaskDetailBottomSheet by remember { mutableStateOf(false) }
var showMoreActionsBottomSheet by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.primary)
.combinedClickable(onClick = {
showTaskDetailBottomSheet = true
}, onLongClick = {
showMoreActionsBottomSheet = true
})
.padding(start = 8.dp)
) {
Spacer(modifier = Modifier.height(8.dp))
task.input?.let {
Text(
text = it,
color = Color.White,
fontSize = 18.sp
)
}
Spacer(modifier = Modifier.height(16.dp))
task.output?.let {
HorizontalDivider(modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp))
Text(
text = it.take(100),
fontSize = 12.sp,
color = Color.White,
modifier = Modifier
.height(100.dp)
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow
)
)
)
}
TaskStatus(task, foregroundColor = Color.White)
if (showMoreActionsBottomSheet) {
val bottomSheetAction = listOf(
Triple(
R.drawable.ic_delete,
R.string.assistant_screen_task_more_actions_bottom_sheet_delete_action
) {
showDeleteTaskAlertDialog(task.id)
Box {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(color = colorResource(R.color.task_container))
.clickable {
showTaskDetailBottomSheet = true
}
)
.padding(16.dp)
) {
Spacer(modifier = Modifier.height(8.dp))
MoreActionsBottomSheet(
title = task.input,
actions = bottomSheetAction,
dismiss = { showMoreActionsBottomSheet = false }
)
task.input?.input?.let {
Text(
text = it.truncateWithEllipsis(30),
color = colorResource(R.color.text_color),
fontSize = 18.sp,
textAlign = TextAlign.Left,
maxLines = 1,
fontWeight = FontWeight.Bold,
modifier = Modifier.width(300.dp)
)
}
Spacer(modifier = Modifier.height(12.dp))
task.output?.output?.let {
Text(
text = it.truncateWithEllipsis(100),
fontSize = 18.sp,
color = colorResource(R.color.text_color),
textAlign = TextAlign.Left,
modifier = Modifier
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow
)
)
)
}
TaskStatusView(task, capability)
if (showTaskDetailBottomSheet) {
TaskDetailBottomSheet(task, showTaskActions = {
showTaskDetailBottomSheet = false
showTaskActions()
}) {
showTaskDetailBottomSheet = false
}
}
}
if (showTaskDetailBottomSheet) {
TaskDetailBottomSheet(task) {
showTaskDetailBottomSheet = false
}
IconButton(
modifier = Modifier.align(Alignment.TopEnd),
onClick = showTaskActions
) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = "More button",
tint = colorResource(R.color.text_color)
)
}
}
}
@ -121,20 +123,28 @@ fun TaskView(
@Preview
@Composable
private fun TaskViewPreview() {
val output = "Lorem".getRandomString(100)
TaskView(
task = Task(
1,
"Free Prompt",
0,
"STATUS_COMPLETED",
"1",
"1",
"Give me text",
output,
"",
""
)
) {
}
TaskInput("What about other promising tokens like"),
TaskOutput(
"Several tokens show promise for future growth in the" +
"cryptocurrency market"
),
1707692337,
1707692337,
1707692337,
1707692337,
1707692337
),
OCCapability().apply {
versionMayor = 30
},
showTaskActions = {
}
)
}

View file

@ -1,17 +1,15 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.assistant.taskDetail
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -19,45 +17,41 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nextcloud.client.assistant.task.TaskStatus
import com.nextcloud.utils.extensions.getRandomString
import com.owncloud.android.R
import com.owncloud.android.lib.resources.assistant.model.Task
import com.owncloud.android.lib.resources.assistant.v2.model.Task
import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput
import com.owncloud.android.lib.resources.assistant.v2.model.TaskOutput
@Suppress("LongMethod")
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun TaskDetailBottomSheet(task: Task, dismiss: () -> Unit) {
var showInput by remember { mutableStateOf(true) }
fun TaskDetailBottomSheet(task: Task, showTaskActions: () -> Unit, dismiss: () -> Unit) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
modifier = Modifier.padding(top = 32.dp),
containerColor = Color.White,
onDismissRequest = {
dismiss()
},
containerColor = colorResource(R.color.bg_default),
onDismissRequest = { dismiss() },
sheetState = sheetState
) {
LazyColumn(
@ -67,80 +61,68 @@ fun TaskDetailBottomSheet(task: Task, dismiss: () -> Unit) {
) {
stickyHeader {
Row(
modifier = Modifier
.fillMaxWidth()
.background(color = colorResource(id = R.color.light_grey), shape = RoundedCornerShape(8.dp))
modifier = Modifier.fillMaxWidth()
) {
TextInputSelectButton(
Modifier.weight(1f),
R.string.assistant_task_detail_screen_input_button_title,
showInput,
onClick = {
showInput = true
}
)
Spacer(modifier = Modifier.weight(1f))
TextInputSelectButton(
Modifier.weight(1f),
R.string.assistant_task_detail_screen_output_button_title,
!showInput,
onClick = {
showInput = false
}
)
IconButton(onClick = showTaskActions) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = "More button",
tint = colorResource(R.color.text_color)
)
}
}
}
item {
Spacer(modifier = Modifier.height(16.dp))
Column(
modifier = Modifier
.fillMaxSize()
.background(color = colorResource(id = R.color.light_grey), shape = RoundedCornerShape(8.dp))
.padding(16.dp)
) {
Text(
text = if (showInput) {
task.input ?: ""
} else {
task.output ?: ""
},
fontSize = 12.sp,
color = Color.Black,
modifier = Modifier
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow
)
)
)
}
TaskStatus(task, foregroundColor = Color.Black)
Spacer(modifier = Modifier.height(32.dp))
InputOutputCard(task)
}
}
}
}
@Composable
private fun TextInputSelectButton(modifier: Modifier, titleId: Int, highlightCondition: Boolean, onClick: () -> Unit) {
Button(
onClick = onClick,
shape = RoundedCornerShape(8.dp),
colors = if (highlightCondition) {
ButtonDefaults.buttonColors(containerColor = Color.White)
} else {
ButtonDefaults.buttonColors(containerColor = colorResource(id = R.color.light_grey))
},
modifier = modifier
.widthIn(min = 0.dp, max = 200.dp)
.padding(horizontal = 4.dp)
fun InputOutputCard(task: Task) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color.Transparent, shape = RoundedCornerShape(8.dp))
) {
Text(text = stringResource(id = titleId), color = Color.Black)
TitleDescriptionBox(
title = stringResource(R.string.assistant_task_detail_screen_input_button_title),
description = task.input?.input ?: ""
)
Spacer(modifier = Modifier.height(16.dp))
TitleDescriptionBox(
title = stringResource(R.string.assistant_task_detail_screen_output_button_title),
description = task.output?.output ?: stringResource(R.string.assistant_screen_task_output_empty_text)
)
}
}
@Composable
private fun TitleDescriptionBox(title: String, description: String?) {
Text(
text = title,
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
color = colorResource(R.color.text_color)
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
.background(color = colorResource(R.color.task_container), RoundedCornerShape(8.dp))
.padding(12.dp)
) {
Text(
text = description ?: "",
color = colorResource(R.color.text_color)
)
}
}
@ -152,14 +134,19 @@ private fun TaskDetailScreenPreview() {
task = Task(
1,
"Free Prompt",
0,
null,
"1",
"1",
"Give me text".getRandomString(100),
"output".getRandomString(300),
"",
""
)
TaskInput("Give me text".getRandomString(100)),
TaskOutput("output".getRandomString(300)),
1707692337,
1707692337,
1707692337,
1707692337,
1707692337
),
showTaskActions = {
}
) {
}
}

View file

@ -1,51 +1,66 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper_ozturk@proton.me>
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.assistant.taskTypes
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilledTonalButton
import android.annotation.SuppressLint
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.owncloud.android.lib.resources.assistant.model.TaskType
import com.owncloud.android.R
import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData
@SuppressLint("ResourceType")
@Composable
fun TaskTypesRow(selectedTaskType: TaskType?, data: List<TaskType>?, selectTaskType: (TaskType) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
) {
data?.forEach { taskType ->
taskType.name?.let { taskTypeName ->
FilledTonalButton(
onClick = { selectTaskType(taskType) },
colors = ButtonDefaults.buttonColors(
containerColor = if (selectedTaskType?.id == taskType.id) {
Color.Unspecified
} else {
Color.Gray
}
)
) {
Text(text = taskTypeName)
}
fun TaskTypesRow(selectedTaskType: TaskTypeData?, data: List<TaskTypeData>, selectTaskType: (TaskTypeData) -> Unit) {
val selectedTabIndex = data.indexOfFirst { it.id == selectedTaskType?.id }.takeIf { it >= 0 } ?: 0
Spacer(modifier = Modifier.padding(end = 8.dp))
ScrollableTabRow(
selectedTabIndex = selectedTabIndex,
edgePadding = 0.dp,
containerColor = colorResource(R.color.actionbar_color),
indicator = {
TabRowDefaults.SecondaryIndicator(
Modifier.tabIndicatorOffset(it[selectedTabIndex]),
color = colorResource(R.color.primary)
)
}
) {
data.forEach { taskType ->
if (taskType.name.isNotEmpty()) {
Tab(
selected = selectedTaskType?.id == taskType.id,
onClick = { selectTaskType(taskType) },
selectedContentColor = colorResource(R.color.text_color),
unselectedContentColor = colorResource(R.color.disabled_text),
text = { Text(text = taskType.name) }
)
}
}
}
}
@Composable
@Preview
private fun TaskTypesRowPreview() {
val selectedTaskType = TaskTypeData("1", "Free text to text prompt", "", emptyMap(), emptyMap())
val taskTypes = listOf(
TaskTypeData("1", "Free text to text prompt", "", emptyMap(), emptyMap()),
TaskTypeData("2", "Extract topics", "", emptyMap(), emptyMap()),
TaskTypeData("3", "Generate Headline", "", emptyMap(), emptyMap()),
TaskTypeData("4", "Summarize", "", emptyMap(), emptyMap())
)
TaskTypesRow(selectedTaskType, taskTypes) { }
}

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.core

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.core

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.core

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.core

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.core

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.core
@ -19,9 +19,7 @@ import android.os.IBinder
*
* One can subclass it to create own service interaction API.
*/
abstract class LocalConnection<S : Service>(
protected val context: Context
) : ServiceConnection {
abstract class LocalConnection<S : Service>(protected val context: Context) : ServiceConnection {
private var serviceBinder: LocalBinder<S>? = null
val service: S? get() = serviceBinder?.service
@ -35,9 +33,7 @@ abstract class LocalConnection<S : Service>(
*
* @see [bind]
*/
protected open fun createBindIntent(): Intent? {
return null
}
protected open fun createBindIntent(): Intent? = null
/**
* Bind local service. If [createBindIntent] returns null, it no-ops.

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.core
@ -20,14 +20,12 @@ class ManualAsyncRunner : AsyncRunner {
task: () -> T,
onResult: OnResultCallback<T>?,
onError: OnErrorCallback?
): Cancellable {
return postTask(
task = { _: OnProgressCallback<Any>, _: IsCancelled -> task.invoke() },
onResult = onResult,
onError = onError,
onProgress = null
)
}
): Cancellable = postTask(
task = { _: OnProgressCallback<Any>, _: IsCancelled -> task.invoke() },
onResult = onResult,
onError = onError,
onProgress = null
)
override fun <T, P> postTask(
task: TaskFunction<T, P>,

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.core
@ -20,7 +20,8 @@ internal class Task<T, P>(
private val onSuccess: OnResultCallback<T>?,
private val onError: OnErrorCallback?,
private val onProgress: OnProgressCallback<P>?
) : Runnable, Cancellable {
) : Runnable,
Cancellable {
val isCancelled: Boolean
get() = cancelled.get()

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.core

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database
@ -11,6 +11,7 @@ import android.content.Context
import com.nextcloud.client.core.Clock
import com.nextcloud.client.database.dao.ArbitraryDataDao
import com.nextcloud.client.database.dao.FileDao
import com.nextcloud.client.database.dao.OfflineOperationDao
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@ -20,17 +21,15 @@ class DatabaseModule {
@Provides
@Singleton
fun database(context: Context, clock: Clock): NextcloudDatabase {
return NextcloudDatabase.getInstance(context, clock)
}
fun database(context: Context, clock: Clock): NextcloudDatabase = NextcloudDatabase.getInstance(context, clock)
@Provides
fun arbitraryDataDao(nextcloudDatabase: NextcloudDatabase): ArbitraryDataDao {
return nextcloudDatabase.arbitraryDataDao()
}
fun arbitraryDataDao(nextcloudDatabase: NextcloudDatabase): ArbitraryDataDao = nextcloudDatabase.arbitraryDataDao()
@Provides
fun fileDao(nextcloudDatabase: NextcloudDatabase): FileDao {
return nextcloudDatabase.fileDao()
}
fun fileDao(nextcloudDatabase: NextcloudDatabase): FileDao = nextcloudDatabase.fileDao()
@Provides
fun offlineOperationsDao(nextcloudDatabase: NextcloudDatabase): OfflineOperationDao =
nextcloudDatabase.offlineOperationDao()
}

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database
@ -12,23 +12,31 @@ import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.nextcloud.client.core.Clock
import com.nextcloud.client.core.ClockImpl
import com.nextcloud.client.database.dao.ArbitraryDataDao
import com.nextcloud.client.database.dao.FileDao
import com.nextcloud.client.database.dao.OfflineOperationDao
import com.nextcloud.client.database.dao.RecommendedFileDao
import com.nextcloud.client.database.dao.UploadDao
import com.nextcloud.client.database.entity.ArbitraryDataEntity
import com.nextcloud.client.database.entity.CapabilityEntity
import com.nextcloud.client.database.entity.ExternalLinkEntity
import com.nextcloud.client.database.entity.FileEntity
import com.nextcloud.client.database.entity.FilesystemEntity
import com.nextcloud.client.database.entity.OfflineOperationEntity
import com.nextcloud.client.database.entity.RecommendedFileEntity
import com.nextcloud.client.database.entity.ShareEntity
import com.nextcloud.client.database.entity.SyncedFolderEntity
import com.nextcloud.client.database.entity.UploadEntity
import com.nextcloud.client.database.entity.VirtualEntity
import com.nextcloud.client.database.migrations.DatabaseMigrationUtil
import com.nextcloud.client.database.migrations.MIGRATION_88_89
import com.nextcloud.client.database.migrations.Migration67to68
import com.nextcloud.client.database.migrations.RoomMigration
import com.nextcloud.client.database.migrations.addLegacyMigrations
import com.nextcloud.client.database.typeConverter.OfflineOperationTypeConverter
import com.owncloud.android.db.ProviderMeta
@Database(
@ -41,7 +49,9 @@ import com.owncloud.android.db.ProviderMeta
ShareEntity::class,
SyncedFolderEntity::class,
UploadEntity::class,
VirtualEntity::class
VirtualEntity::class,
OfflineOperationEntity::class,
RecommendedFileEntity::class
],
version = ProviderMeta.DB_VERSION,
autoMigrations = [
@ -59,40 +69,55 @@ import com.owncloud.android.db.ProviderMeta
AutoMigration(from = 77, to = 78),
AutoMigration(from = 78, to = 79, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 79, to = 80),
AutoMigration(from = 80, to = 81)
AutoMigration(from = 80, to = 81),
AutoMigration(from = 81, to = 82),
AutoMigration(from = 82, to = 83),
AutoMigration(from = 83, to = 84),
AutoMigration(from = 84, to = 85, spec = DatabaseMigrationUtil.DeleteColumnSpec::class),
AutoMigration(from = 85, to = 86, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 86, to = 87, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 87, to = 88, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
// manual migration used for 88 to 89
AutoMigration(from = 89, to = 90),
AutoMigration(from = 90, to = 91),
AutoMigration(from = 91, to = 92),
AutoMigration(from = 92, to = 93, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class)
],
exportSchema = true
)
@Suppress("Detekt.UnnecessaryAbstractClass") // needed by Room
@TypeConverters(OfflineOperationTypeConverter::class)
abstract class NextcloudDatabase : RoomDatabase() {
abstract fun arbitraryDataDao(): ArbitraryDataDao
abstract fun fileDao(): FileDao
abstract fun offlineOperationDao(): OfflineOperationDao
abstract fun uploadDao(): UploadDao
abstract fun recommendedFileDao(): RecommendedFileDao
companion object {
const val FIRST_ROOM_DB_VERSION = 65
private var INSTANCE: NextcloudDatabase? = null
private var instance: NextcloudDatabase? = null
@JvmStatic
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("Here for legacy purposes, inject this class or use getInstance(context, clock) instead")
fun getInstance(context: Context): NextcloudDatabase {
return getInstance(context, ClockImpl())
}
fun getInstance(context: Context): NextcloudDatabase = getInstance(context, ClockImpl())
@JvmStatic
fun getInstance(context: Context, clock: Clock): NextcloudDatabase {
if (INSTANCE == null) {
INSTANCE = Room
if (instance == null) {
instance = Room
.databaseBuilder(context, NextcloudDatabase::class.java, ProviderMeta.DB_NAME)
.allowMainThreadQueries()
.addTypeConverter(OfflineOperationTypeConverter())
.addLegacyMigrations(clock, context)
.addMigrations(RoomMigration())
.addMigrations(Migration67to68())
.fallbackToDestructiveMigration()
.addMigrations(MIGRATION_88_89)
.build()
}
return INSTANCE!!
return instance!!
}
}
}

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.dao

View file

@ -3,20 +3,38 @@
*
* SPDX-FileCopyrightText: 2022 Dariusz Olszewski <starypatyk@users.noreply.github.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Update
import com.nextcloud.client.database.entity.FileEntity
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
import com.owncloud.android.utils.MimeType
@Suppress("TooManyFunctions")
@Dao
interface FileDao {
@Query(
"""
SELECT DISTINCT parent
FROM filelist
WHERE path IN (:subfilePaths)
"""
)
fun getParentIdsOfSubfiles(subfilePaths: List<String>): List<Long>
@Update
fun update(entity: FileEntity)
@Query("SELECT * FROM filelist WHERE _id = :id LIMIT 1")
fun getFileById(id: Long): FileEntity?
@Query("SELECT * FROM filelist WHERE local_id = :localId LIMIT 1")
fun getFileByLocalId(localId: Long): FileEntity?
@Query("SELECT * FROM filelist WHERE path = :path AND file_owner = :fileOwner LIMIT 1")
fun getFileByEncryptedRemotePath(path: String, fileOwner: String): FileEntity?
@ -49,4 +67,45 @@ interface FileDao {
@Query("SELECT * FROM filelist where file_owner = :fileOwner AND etag_in_conflict IS NOT NULL")
fun getFilesWithSyncConflict(fileOwner: String): List<FileEntity>
@Query(
"SELECT * FROM filelist where file_owner = :fileOwner AND internal_two_way_sync_timestamp >= 0 " +
"ORDER BY internal_two_way_sync_timestamp DESC"
)
fun getInternalTwoWaySyncFolders(fileOwner: String): List<FileEntity>
@Query(
"""
SELECT *
FROM filelist
WHERE parent = :parentId
AND file_owner = :accountName
AND is_encrypted = 0
AND (content_type = :dirType OR content_type = :webdavType)
ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}
"""
)
fun getNonEncryptedSubfolders(
parentId: Long,
accountName: String,
dirType: String = MimeType.DIRECTORY,
webdavType: String = MimeType.WEBDAV_FOLDER
): List<FileEntity>
@Query(
"""
SELECT *
FROM filelist
WHERE parent = :parentId
AND file_owner = :accountName
AND (content_type != :dirType AND content_type != :webdavType)
ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}
"""
)
fun getSubfiles(
parentId: Long,
accountName: String,
dirType: String = MimeType.DIRECTORY,
webdavType: String = MimeType.WEBDAV_FOLDER
): List<FileEntity>
}

View file

@ -0,0 +1,46 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.database.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.nextcloud.client.database.entity.OfflineOperationEntity
@Dao
interface OfflineOperationDao {
@Query("SELECT * FROM offline_operations")
fun getAll(): List<OfflineOperationEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg entity: OfflineOperationEntity)
@Update
fun update(entity: OfflineOperationEntity)
@Delete
fun delete(entity: OfflineOperationEntity)
@Query("DELETE FROM offline_operations WHERE offline_operations_path = :path")
fun deleteByPath(path: String)
@Query("SELECT * FROM offline_operations WHERE offline_operations_path = :path LIMIT 1")
fun getByPath(path: String): OfflineOperationEntity?
@Query("SELECT * FROM offline_operations WHERE offline_operations_parent_oc_file_id = :parentOCFileId")
fun getSubEntitiesByParentOCFileId(parentOCFileId: Long): List<OfflineOperationEntity>
@Query("DELETE FROM offline_operations")
fun clearTable()
@Query("DELETE FROM offline_operations WHERE _id = :id")
fun deleteById(id: Int)
}

View file

@ -0,0 +1,26 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.nextcloud.client.database.entity.RecommendedFileEntity
import com.owncloud.android.db.ProviderMeta
@Dao
interface RecommendedFileDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(recommendedFiles: List<RecommendedFileEntity>)
@Query(
"SELECT * FROM ${ProviderMeta.ProviderTableMeta.RECOMMENDED_FILE_TABLE_NAME} WHERE account_name = :accountName"
)
suspend fun getAll(accountName: String): List<RecommendedFileEntity>
}

View file

@ -0,0 +1,30 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.database.dao
import androidx.room.Dao
import androidx.room.Query
import com.nextcloud.client.database.entity.UploadEntity
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
@Dao
interface UploadDao {
@Query(
"SELECT _id FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME +
" WHERE " + ProviderTableMeta.UPLOADS_STATUS + " = :status AND " +
ProviderTableMeta.UPLOADS_ACCOUNT_NAME + " = :accountName AND _id IS NOT NULL"
)
fun getAllIds(status: Int, accountName: String): List<Int>
@Query(
"SELECT * FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME +
" WHERE " + ProviderTableMeta._ID + " IN (:ids) AND " +
ProviderTableMeta.UPLOADS_ACCOUNT_NAME + " = :accountName"
)
fun getUploadsByIds(ids: LongArray, accountName: String): List<UploadEntity>
}

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.entity

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.entity
@ -122,5 +122,25 @@ data class CapabilityEntity(
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT)
val dropAccount: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SECURITY_GUARD)
val securityGuard: Int?
val securityGuard: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS)
val forbiddenFileNameCharacters: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAMES)
val forbiddenFileNames: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_EXTENSIONS)
val forbiddenFileNameExtensions: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_BASE_NAMES)
val forbiddenFilenameBaseNames: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FILES_DOWNLOAD_LIMIT)
val filesDownloadLimit: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FILES_DOWNLOAD_LIMIT_DEFAULT)
val filesDownloadLimitDefault: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_RECOMMENDATION)
val recommendation: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_NOTES_FOLDER_PATH)
val notesFolderPath: String?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_DEFAULT_PERMISSIONS)
val defaultPermissions: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_USER_STATUS_SUPPORTS_BUSY)
val userStatusSupportsBusy: Int?
)

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.entity

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.entity
@ -50,7 +50,7 @@ data class FileEntity(
@ColumnInfo(name = ProviderTableMeta.FILE_ETAG_ON_SERVER)
val etagOnServer: String?,
@ColumnInfo(name = ProviderTableMeta.FILE_SHARED_VIA_LINK)
val sharedViaLink: Int?,
var sharedViaLink: Int?,
@ColumnInfo(name = ProviderTableMeta.FILE_PERMISSIONS)
val permissions: String?,
@ColumnInfo(name = ProviderTableMeta.FILE_REMOTE_ID)
@ -73,7 +73,7 @@ data class FileEntity(
@ColumnInfo(name = ProviderTableMeta.FILE_ETAG_IN_CONFLICT)
val etagInConflict: String?,
@ColumnInfo(name = ProviderTableMeta.FILE_SHARED_WITH_SHAREE)
val sharedWithSharee: Int?,
var sharedWithSharee: Int?,
@ColumnInfo(name = ProviderTableMeta.FILE_MOUNT_TYPE)
val mountType: Int?,
@ColumnInfo(name = ProviderTableMeta.FILE_HAS_PREVIEW)
@ -115,5 +115,11 @@ data class FileEntity(
@ColumnInfo(name = ProviderTableMeta.FILE_METADATA_GPS)
val metadataGPS: String?,
@ColumnInfo(name = ProviderTableMeta.FILE_E2E_COUNTER)
val e2eCounter: Long?
val e2eCounter: Long?,
@ColumnInfo(name = ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP)
val internalTwoWaySync: Long?,
@ColumnInfo(name = ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT)
val internalTwoWaySyncResult: String?,
@ColumnInfo(name = ProviderTableMeta.FILE_UPLOADED)
val uploaded: Long?
)

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.entity

View file

@ -0,0 +1,68 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.database.entity
import android.content.Context
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.nextcloud.model.OfflineOperationType
import com.owncloud.android.R
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
@Entity(tableName = ProviderTableMeta.OFFLINE_OPERATION_TABLE_NAME)
data class OfflineOperationEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = ProviderTableMeta._ID)
val id: Int? = null,
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PARENT_OC_FILE_ID)
var parentOCFileId: Long? = null,
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PATH)
var path: String? = null,
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_TYPE)
var type: OfflineOperationType? = null,
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_FILE_NAME)
var filename: String? = null,
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_CREATED_AT)
var createdAt: Long? = null,
@ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_MODIFIED_AT)
var modifiedAt: Long? = null
) {
fun isRenameOrRemove(): Boolean =
(type is OfflineOperationType.RenameFile || type is OfflineOperationType.RemoveFile)
fun isCreate(): Boolean = (type is OfflineOperationType.CreateFile || type is OfflineOperationType.CreateFolder)
fun getConflictText(context: Context): String {
val resId = when (type) {
is OfflineOperationType.RemoveFile -> {
R.string.offline_operations_worker_notification_remove_conflict_text
}
is OfflineOperationType.RenameFile -> {
R.string.offline_operations_worker_notification_rename_conflict_text
}
is OfflineOperationType.CreateFile -> {
R.string.offline_operations_worker_notification_create_file_conflict_text
}
else -> {
R.string.offline_operations_worker_notification_create_folder_conflict_text
}
}
return context.getString(resId, filename)
}
}

View file

@ -0,0 +1,72 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.nextcloud.android.lib.resources.recommendations.Recommendation
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
@Entity(tableName = ProviderTableMeta.RECOMMENDED_FILE_TABLE_NAME)
data class RecommendedFileEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = ProviderTableMeta._ID)
val id: Long,
@ColumnInfo(name = ProviderTableMeta.RECOMMENDED_FILE_NAME)
val name: String,
@ColumnInfo(name = ProviderTableMeta.RECOMMENDED_FILE_DIRECTORY)
val directory: String,
@ColumnInfo(name = ProviderTableMeta.RECOMMENDED_FILE_EXTENSIONS)
val extension: String,
@ColumnInfo(name = ProviderTableMeta.RECOMMENDED_FILE_MIME_TYPE)
val mimeType: String,
@ColumnInfo(name = ProviderTableMeta.RECOMMENDED_FILE_HAS_PREVIEW)
val hasPreview: Boolean,
@ColumnInfo(name = ProviderTableMeta.RECOMMENDED_FILE_REASON)
val reason: String,
@ColumnInfo(name = ProviderTableMeta.RECOMMENDED_TIMESTAMP)
val timestamp: Long,
@ColumnInfo(name = ProviderTableMeta.RECOMMENDED_FILE_ACCOUNT_NAME)
val accountName: String?
)
fun ArrayList<Recommendation>.toEntity(accountName: String): List<RecommendedFileEntity> = this.map { recommendation ->
RecommendedFileEntity(
id = recommendation.id,
name = recommendation.name,
directory = recommendation.directory,
extension = recommendation.extension,
mimeType = recommendation.mimeType,
hasPreview = recommendation.hasPreview,
reason = recommendation.reason,
timestamp = recommendation.timestamp,
accountName = accountName
)
}
fun List<RecommendedFileEntity>.toOCFile(storageManager: FileDataStorageManager): ArrayList<OCFile> =
mapNotNull { entity ->
entity.id.let {
storageManager.getFileByLocalId(it).apply {
this?.reason = entity.reason
this?.setIsRecommendedFile(true)
}
}
}
.toCollection(ArrayList())

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.entity
@ -54,5 +54,11 @@ data class ShareEntity(
@ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARE_LINK)
val shareLink: String?,
@ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARE_LABEL)
val shareLabel: String?
val shareLabel: String?,
@ColumnInfo(name = ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_LIMIT)
val downloadLimitLimit: Int?,
@ColumnInfo(name = ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT)
val downloadLimitCount: Int?,
@ColumnInfo(name = ProviderTableMeta.OCSHARES_ATTRIBUTES)
val attributes: String?
)

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.entity

View file

@ -3,14 +3,20 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.nextcloud.utils.autoRename.AutoRename
import com.owncloud.android.datamodel.UploadsStorageManager
import com.owncloud.android.db.OCUpload
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
import com.owncloud.android.db.UploadResult
import com.owncloud.android.files.services.NameCollisionPolicy
import com.owncloud.android.lib.resources.status.OCCapability
@Entity(tableName = ProviderTableMeta.UPLOADS_TABLE_NAME)
data class UploadEntity(
@ -48,3 +54,27 @@ data class UploadEntity(
@ColumnInfo(name = ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN)
val folderUnlockToken: String?
)
fun UploadEntity.toOCUpload(capability: OCCapability? = null): OCUpload {
val localPath = localPath
var remotePath = remotePath
if (capability != null && remotePath != null) {
remotePath = AutoRename.rename(remotePath, capability)
}
val upload = OCUpload(localPath, remotePath, accountName)
fileSize?.let { upload.fileSize = it }
id?.let { upload.uploadId = it.toLong() }
status?.let { upload.uploadStatus = UploadsStorageManager.UploadStatus.fromValue(it) }
localBehaviour?.let { upload.localAction = it }
nameCollisionPolicy?.let { upload.nameCollisionPolicy = NameCollisionPolicy.deserialize(it) }
isCreateRemoteFolder?.let { upload.isCreateRemoteFolder = it == 1 }
uploadEndTimestamp?.let { upload.uploadEndTimestamp = it.toLong() }
lastResult?.let { upload.lastResult = UploadResult.fromValue(it) }
createdBy?.let { upload.createdBy = it }
isWifiOnly?.let { upload.isUseWifiOnly = it == 1 }
isWhileChargingOnly?.let { upload.isWhileChargingOnly = it == 1 }
folderUnlockToken?.let { upload.folderUnlockToken = it }
return upload
}

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.entity

View file

@ -3,12 +3,14 @@
*
* SPDX-FileCopyrightText: 2023 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.migrations
import androidx.room.DeleteColumn
import androidx.room.migration.AutoMigrationSpec
import androidx.sqlite.db.SupportSQLiteDatabase
import com.nextcloud.client.database.migrations.model.SQLiteColumnType
object DatabaseMigrationUtil {
@ -17,6 +19,32 @@ object DatabaseMigrationUtil {
const val TYPE_INTEGER_PRIMARY_KEY = "INTEGER PRIMARY KEY"
const val KEYWORD_NOT_NULL = "NOT NULL"
fun addColumnIfNotExists(
db: SupportSQLiteDatabase,
tableName: String,
columnName: String,
columnType: SQLiteColumnType
) {
val cursor = db.query("PRAGMA table_info($tableName)")
var columnExists = false
while (cursor.moveToNext()) {
val nameIndex = cursor.getColumnIndex("name")
if (nameIndex != -1) {
val existingColumnName = cursor.getString(nameIndex)
if (existingColumnName == columnName) {
columnExists = true
break
}
}
}
cursor.close()
if (!columnExists) {
db.execSQL("ALTER TABLE $tableName ADD COLUMN `$columnName` ${columnType.value}")
}
}
/**
* Utility method to add or remove columns from a table
*
@ -46,11 +74,7 @@ object DatabaseMigrationUtil {
/**
* Utility method to create a new table with the given columns
*/
private fun createNewTable(
database: SupportSQLiteDatabase,
newTableName: String,
columns: Map<String, String>
) {
private fun createNewTable(database: SupportSQLiteDatabase, newTableName: String, columns: Map<String, String>) {
val columnsString = columns.entries.joinToString(",") { "${it.key} ${it.value}" }
database.execSQL("CREATE TABLE $newTableName ($columnsString)")
}
@ -80,11 +104,7 @@ object DatabaseMigrationUtil {
/**
* Utility method to replace an old table with a new one, essentially deleting the old one and renaming the new one
*/
private fun replaceTable(
database: SupportSQLiteDatabase,
tableName: String,
newTableTempName: String
) {
private fun replaceTable(database: SupportSQLiteDatabase, tableName: String, newTableTempName: String) {
database.execSQL("DROP TABLE $tableName")
database.execSQL("ALTER TABLE $newTableTempName RENAME TO $tableName")
}
@ -98,4 +118,12 @@ object DatabaseMigrationUtil {
super.onPostMigrate(db)
}
}
@DeleteColumn.Entries(
DeleteColumn(
tableName = "offline_operations",
columnName = "offline_operations_parent_path"
)
)
class DeleteColumnSpec : AutoMigrationSpec
}

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.migrations

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.migrations;

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2023 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.migrations

View file

@ -0,0 +1,31 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.database.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.nextcloud.client.database.migrations.model.SQLiteColumnType
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
@Suppress("MagicNumber")
val MIGRATION_88_89 = object : Migration(88, 89) {
override fun migrate(database: SupportSQLiteDatabase) {
DatabaseMigrationUtil.addColumnIfNotExists(
database,
ProviderTableMeta.FILE_TABLE_NAME,
ProviderTableMeta.FILE_UPLOADED,
SQLiteColumnType.INTEGER_DEFAULT_NULL
)
DatabaseMigrationUtil.addColumnIfNotExists(
database,
ProviderTableMeta.CAPABILITIES_TABLE_NAME,
ProviderTableMeta.CAPABILITIES_NOTES_FOLDER_PATH,
SQLiteColumnType.TEXT_DEFAULT_NULL
)
}
}

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.database.migrations

View file

@ -0,0 +1,13 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.database.migrations.model
enum class SQLiteColumnType(val value: String) {
INTEGER_DEFAULT_NULL("INTEGER DEFAULT NULL"),
TEXT_DEFAULT_NULL("TEXT DEFAULT NULL")
}

View file

@ -0,0 +1,96 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.database.typeAdapter
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.nextcloud.model.OfflineOperationRawType
import com.nextcloud.model.OfflineOperationType
import java.lang.reflect.Type
class OfflineOperationTypeAdapter :
JsonSerializer<OfflineOperationType>,
JsonDeserializer<OfflineOperationType> {
override fun serialize(
src: OfflineOperationType?,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
val jsonObject = JsonObject()
jsonObject.addProperty("type", src?.javaClass?.simpleName)
when (src) {
is OfflineOperationType.CreateFolder -> {
jsonObject.addProperty("type", src.type)
jsonObject.addProperty("path", src.path)
}
is OfflineOperationType.CreateFile -> {
jsonObject.addProperty("type", src.type)
jsonObject.addProperty("localPath", src.localPath)
jsonObject.addProperty("remotePath", src.remotePath)
jsonObject.addProperty("mimeType", src.mimeType)
}
is OfflineOperationType.RenameFile -> {
jsonObject.addProperty("type", src.type)
jsonObject.addProperty("ocFileId", src.ocFileId)
jsonObject.addProperty("newName", src.newName)
}
is OfflineOperationType.RemoveFile -> {
jsonObject.addProperty("type", src.type)
jsonObject.addProperty("path", src.path)
}
null -> Unit
}
return jsonObject
}
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): OfflineOperationType? {
val jsonObject = json?.asJsonObject ?: return null
val type = jsonObject.get("type")?.asString
return when (type) {
OfflineOperationRawType.CreateFolder.name -> OfflineOperationType.CreateFolder(
jsonObject.get("type").asString,
jsonObject.get("path").asString
)
OfflineOperationRawType.CreateFile.name -> OfflineOperationType.CreateFile(
jsonObject.get("type").asString,
jsonObject.get("localPath").asString,
jsonObject.get("remotePath").asString,
jsonObject.get("mimeType").asString
)
OfflineOperationRawType.RenameFile.name -> OfflineOperationType.RenameFile(
jsonObject.get("type").asString,
jsonObject.get("ocFileId").asLong,
jsonObject.get("newName").asString
)
OfflineOperationRawType.RemoveFile.name -> OfflineOperationType.RemoveFile(
jsonObject.get("type").asString,
jsonObject.get("path").asString
)
else -> null
}
}
}

View file

@ -0,0 +1,30 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.database.typeConverter
import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter
import com.google.gson.Gson
import com.nextcloud.model.OfflineOperationType
import com.google.gson.GsonBuilder
import com.nextcloud.client.database.typeAdapter.OfflineOperationTypeAdapter
@ProvidedTypeConverter
class OfflineOperationTypeConverter {
private val gson: Gson = GsonBuilder()
.registerTypeAdapter(OfflineOperationType::class.java, OfflineOperationTypeAdapter())
.create()
@TypeConverter
fun fromOfflineOperationType(type: OfflineOperationType?): String? = gson.toJson(type)
@TypeConverter
fun toOfflineOperationType(type: String?): OfflineOperationType? =
gson.fromJson(type, OfflineOperationType::class.java)
}

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.device

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.device
@ -12,11 +12,10 @@ import android.os.Build
import java.util.Locale
class DeviceInfo {
val vendor: String = Build.MANUFACTURER.toLowerCase(Locale.ROOT)
val vendor: String = Build.MANUFACTURER.lowercase(Locale.ROOT)
val apiLevel: Int = Build.VERSION.SDK_INT
val androidVersion = Build.VERSION.RELEASE
fun hasCamera(context: Context): Boolean {
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
}
fun hasCamera(context: Context): Boolean =
context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
}

View file

@ -2,9 +2,9 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-FileCopyrightText: 2019 Tobias Kaminsky
* SPDX-FileCopyrightText: 2019 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.device

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.device

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.device

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.di

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.di;
@ -16,7 +16,10 @@ import com.nextcloud.client.device.DeviceModule;
import com.nextcloud.client.integrations.IntegrationsModule;
import com.nextcloud.client.jobs.JobsModule;
import com.nextcloud.client.jobs.download.FileDownloadHelper;
import com.nextcloud.client.jobs.offlineOperations.receiver.OfflineOperationReceiver;
import com.nextcloud.client.jobs.upload.FileUploadBroadcastReceiver;
import com.nextcloud.client.jobs.upload.FileUploadHelper;
import com.nextcloud.client.media.BackgroundPlayerService;
import com.nextcloud.client.network.NetworkModule;
import com.nextcloud.client.onboarding.OnboardingModule;
import com.nextcloud.client.preferences.PreferencesModule;
@ -27,6 +30,8 @@ import com.owncloud.android.ui.whatsnew.ProgressIndicator;
import javax.inject.Singleton;
import androidx.annotation.OptIn;
import androidx.media3.common.util.UnstableApi;
import dagger.BindsInstance;
import dagger.Component;
import dagger.android.support.AndroidSupportInjectionModule;
@ -46,7 +51,7 @@ import dagger.android.support.AndroidSupportInjectionModule;
ThemeModule.class,
DatabaseModule.class,
DispatcherModule.class,
VariantModule.class
VariantModule.class,
})
@Singleton
public interface AppComponent {
@ -55,6 +60,9 @@ public interface AppComponent {
void inject(MediaControlView mediaControlView);
@OptIn(markerClass = UnstableApi.class)
void inject(BackgroundPlayerService backgroundPlayerService);
void inject(ThemeableSwitchPreference switchPreference);
void inject(FileUploadHelper fileUploadHelper);
@ -63,6 +71,10 @@ public interface AppComponent {
void inject(ProgressIndicator progressIndicator);
void inject(FileUploadBroadcastReceiver fileUploadBroadcastReceiver);
void inject(OfflineOperationReceiver offlineOperationReceiver);
@Component.Builder
interface Builder {
@BindsInstance

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.di;
@ -28,6 +28,7 @@ import com.nextcloud.client.core.ClockImpl;
import com.nextcloud.client.core.ThreadPoolAsyncRunner;
import com.nextcloud.client.database.dao.ArbitraryDataDao;
import com.nextcloud.client.device.DeviceInfo;
import com.nextcloud.client.jobs.operation.FileOperationHelper;
import com.nextcloud.client.logger.FileLogHandler;
import com.nextcloud.client.logger.Logger;
import com.nextcloud.client.logger.LoggerImpl;
@ -55,6 +56,7 @@ import com.owncloud.android.ui.activities.data.activities.RemoteActivitiesReposi
import com.owncloud.android.ui.activities.data.files.FilesRepository;
import com.owncloud.android.ui.activities.data.files.FilesServiceApiImpl;
import com.owncloud.android.ui.activities.data.files.RemoteFilesRepository;
import com.owncloud.android.ui.dialog.setupEncryption.CertificateValidator;
import com.owncloud.android.utils.theme.ViewThemeUtils;
import org.greenrobot.eventbus.EventBus;
@ -249,10 +251,21 @@ class AppModule {
return new PassCodeManager(preferences, clock);
}
@Provides
FileOperationHelper fileOperationHelper(CurrentAccountProvider currentAccountProvider, Context context) {
return new FileOperationHelper(currentAccountProvider.getUser(), context, fileDataStorageManager(currentAccountProvider, context));
}
@Provides
@Singleton
UsersAndGroupsSearchConfig userAndGroupSearchConfig() {
return new UsersAndGroupsSearchConfig();
}
@Provides
@Singleton
CertificateValidator certificateValidator() {
return new CertificateValidator();
}
}

View file

@ -1,8 +1,9 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.di;
@ -17,6 +18,7 @@ import com.nextcloud.client.jobs.transfer.FileTransferService;
import com.nextcloud.client.jobs.upload.FileUploadHelper;
import com.nextcloud.client.logger.ui.LogsActivity;
import com.nextcloud.client.logger.ui.LogsViewModel;
import com.nextcloud.client.media.BackgroundPlayerService;
import com.nextcloud.client.media.PlayerService;
import com.nextcloud.client.migrations.Migrations;
import com.nextcloud.client.onboarding.FirstRunActivity;
@ -24,11 +26,15 @@ import com.nextcloud.client.onboarding.WhatsNewActivity;
import com.nextcloud.client.widget.DashboardWidgetConfigurationActivity;
import com.nextcloud.client.widget.DashboardWidgetProvider;
import com.nextcloud.client.widget.DashboardWidgetService;
import com.nextcloud.receiver.NetworkChangeReceiver;
import com.nextcloud.ui.ChooseAccountDialogFragment;
import com.nextcloud.ui.ChooseStorageLocationDialogFragment;
import com.nextcloud.ui.ImageDetailFragment;
import com.nextcloud.ui.SetStatusDialogFragment;
import com.nextcloud.ui.SetOnlineStatusBottomSheet;
import com.nextcloud.ui.SetStatusMessageBottomSheet;
import com.nextcloud.ui.composeActivity.ComposeActivity;
import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet;
import com.nmc.android.ui.LauncherActivity;
import com.owncloud.android.MainApp;
import com.owncloud.android.authentication.AuthenticatorActivity;
@ -54,6 +60,7 @@ import com.owncloud.android.ui.activity.FileActivity;
import com.owncloud.android.ui.activity.FileDisplayActivity;
import com.owncloud.android.ui.activity.FilePickerActivity;
import com.owncloud.android.ui.activity.FolderPickerActivity;
import com.owncloud.android.ui.activity.InternalTwoWaySyncActivity;
import com.owncloud.android.ui.activity.ManageAccountsActivity;
import com.owncloud.android.ui.activity.ManageSpaceActivity;
import com.owncloud.android.ui.activity.NotificationsActivity;
@ -83,16 +90,16 @@ import com.owncloud.android.ui.dialog.LocalStoragePathPickerDialogFragment;
import com.owncloud.android.ui.dialog.MultipleAccountsDialog;
import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment;
import com.owncloud.android.ui.dialog.RenameFileDialogFragment;
import com.owncloud.android.ui.dialog.RenamePublicShareDialogFragment;
import com.owncloud.android.ui.dialog.SendFilesDialog;
import com.owncloud.android.ui.dialog.SendShareDialog;
import com.owncloud.android.ui.dialog.SetupEncryptionDialogFragment;
import com.owncloud.android.ui.dialog.SharePasswordDialogFragment;
import com.owncloud.android.ui.dialog.SortingOrderDialogFragment;
import com.owncloud.android.ui.dialog.SslUntrustedCertDialog;
import com.owncloud.android.ui.dialog.StoragePermissionDialogFragment;
import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment;
import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment;
import com.owncloud.android.ui.dialog.TermsOfServiceDialog;
import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment;
import com.owncloud.android.ui.fragment.ExtendedListFragment;
import com.owncloud.android.ui.fragment.FeatureFragment;
import com.owncloud.android.ui.fragment.FileDetailActivitiesFragment;
@ -121,6 +128,8 @@ import com.owncloud.android.ui.preview.PreviewTextStringFragment;
import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment;
import com.owncloud.android.ui.trashbin.TrashbinActivity;
import androidx.annotation.OptIn;
import androidx.media3.common.util.UnstableApi;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
@ -219,6 +228,9 @@ abstract class ComponentsModule {
@ContributesAndroidInjector
abstract TrashbinActivity trashbinActivity();
@ContributesAndroidInjector
abstract TrashbinFileActionsBottomSheet trashbinFileActionsBottomSheet();
@ContributesAndroidInjector
abstract UploadFilesActivity uploadFilesActivity();
@ -289,7 +301,7 @@ abstract class ComponentsModule {
abstract ChooseAccountDialogFragment chooseAccountDialogFragment();
@ContributesAndroidInjector
abstract SetStatusDialogFragment setStatusDialogFragment();
abstract SetOnlineStatusBottomSheet setOnlineStatusBottomSheet();
@ContributesAndroidInjector
abstract PreviewTextFileFragment previewTextFileFragment();
@ -312,6 +324,9 @@ abstract class ComponentsModule {
@ContributesAndroidInjector
abstract BootupBroadcastReceiver bootupBroadcastReceiver();
@ContributesAndroidInjector
abstract NetworkChangeReceiver networkChangeReceiver();
@ContributesAndroidInjector
abstract NotificationWork.NotificationReceiver notificationWorkBroadcastReceiver();
@ -399,15 +414,15 @@ abstract class ComponentsModule {
@ContributesAndroidInjector
abstract RemoveFilesDialogFragment removeFilesDialogFragment();
@ContributesAndroidInjector
abstract RenamePublicShareDialogFragment renamePublicShareDialogFragment();
@ContributesAndroidInjector
abstract SendShareDialog sendShareDialog();
@ContributesAndroidInjector
abstract SetupEncryptionDialogFragment setupEncryptionDialogFragment();
@ContributesAndroidInjector
abstract ChooseStorageLocationDialogFragment chooseStorageLocationDialogFragment();
@ContributesAndroidInjector
abstract SharePasswordDialogFragment sharePasswordDialogFragment();
@ -476,4 +491,18 @@ abstract class ComponentsModule {
@ContributesAndroidInjector
abstract TestJob testJob();
@ContributesAndroidInjector
abstract InternalTwoWaySyncActivity internalTwoWaySyncActivity();
@OptIn(markerClass = UnstableApi.class)
@ContributesAndroidInjector
abstract BackgroundPlayerService backgroundPlayerService();
@ContributesAndroidInjector
abstract TermsOfServiceDialog termsOfServiceDialog();
@ContributesAndroidInjector
abstract SetStatusMessageBottomSheet setStatusMessageBottomSheet();
}

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.di

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.di
@ -12,11 +12,7 @@ import androidx.fragment.app.FragmentManager
import dagger.android.support.AndroidSupportInjection
internal class FragmentInjector : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentPreAttached(
fragmentManager: FragmentManager,
fragment: Fragment,
context: Context
) {
override fun onFragmentPreAttached(fragmentManager: FragmentManager, fragment: Fragment, context: Context) {
super.onFragmentPreAttached(fragmentManager, fragment, context)
if (fragment is Injectable) {
try {

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.di;

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.di;

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.di
@ -27,19 +27,14 @@ internal abstract class ThemeModule {
@Provides
@Singleton
fun themeColorUtils(): ThemeColorUtils {
return ThemeColorUtils()
}
fun themeColorUtils(): ThemeColorUtils = ThemeColorUtils()
@Provides
@Singleton
fun themeUtils(): ThemeUtils {
return ThemeUtils()
}
fun themeUtils(): ThemeUtils = ThemeUtils()
@Provides
fun provideMaterialSchemes(materialSchemesProvider: MaterialSchemesProvider): MaterialSchemes {
return materialSchemesProvider.getMaterialSchemesForCurrentUser()
}
fun provideMaterialSchemes(materialSchemesProvider: MaterialSchemesProvider): MaterialSchemes =
materialSchemesProvider.getMaterialSchemesForCurrentUser()
}
}

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.di

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.di

View file

@ -1,8 +1,9 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.di
@ -13,6 +14,7 @@ import com.nextcloud.client.etm.EtmViewModel
import com.nextcloud.client.logger.ui.LogsViewModel
import com.nextcloud.ui.fileactions.FileActionsViewModel
import com.owncloud.android.ui.preview.pdf.PreviewPdfViewModel
import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsViewModel
import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel
import dagger.Binds
import dagger.Module
@ -50,6 +52,11 @@ abstract class ViewModelModule {
@ViewModelKey(DocumentScanViewModel::class)
abstract fun documentScanViewModel(vm: DocumentScanViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(TrashbinFileActionsViewModel::class)
abstract fun trashbinFileActionsViewModel(vm: TrashbinFileActionsViewModel): ViewModel
@Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
}

View file

@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
/**

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2023 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.documentscan
@ -21,9 +21,8 @@ abstract class AppScanOptionalFeature {
*/
@Suppress("unused") // used only in some variants
object Stub : AppScanOptionalFeature() {
override fun getScanContract(): ActivityResultContract<Unit, String?> {
override fun getScanContract(): ActivityResultContract<Unit, String?> =
throw UnsupportedOperationException("Document scan is not available")
}
override val isAvailable = false
}

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.documentscan
@ -28,9 +28,7 @@ class DocumentPageListAdapter :
holder.bind(currentList[position])
}
override fun getItemCount(): Int {
return currentList.size
}
override fun getItemCount(): Int = currentList.size
class DocumentPageViewHolder(val binding: DocumentPageItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(imagePath: String) {
@ -39,10 +37,8 @@ class DocumentPageListAdapter :
}
private class DiffItemCallback : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String) =
oldItem == newItem
override fun areItemsTheSame(oldItem: String, newItem: String) = oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String) =
oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String) = oldItem == newItem
}
}

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.documentscan
@ -27,7 +27,9 @@ import com.owncloud.android.ui.activity.ToolbarActivity
import com.owncloud.android.utils.theme.ViewThemeUtils
import javax.inject.Inject
class DocumentScanActivity : ToolbarActivity(), Injectable {
class DocumentScanActivity :
ToolbarActivity(),
Injectable {
@Inject
lateinit var vmFactory: ViewModelFactory
@ -96,18 +98,16 @@ class DocumentScanActivity : ToolbarActivity(), Injectable {
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_save -> {
viewModel.onClickDone()
true
}
android.R.id.home -> {
onBackPressed()
true
}
else -> false
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_save -> {
viewModel.onClickDone()
true
}
android.R.id.home -> {
onBackPressed()
true
}
else -> false
}
}
)

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.documentscan
@ -46,15 +46,11 @@ class DocumentScanViewModel @Inject constructor(
get() = pageList.isEmpty()
}
class NormalState(
pageList: List<String> = emptyList(),
val shouldRequestScan: Boolean = false
) : BaseState(pageList)
class NormalState(pageList: List<String> = emptyList(), val shouldRequestScan: Boolean = false) :
BaseState(pageList)
class RequestExportState(
pageList: List<String> = emptyList(),
val shouldRequestExportType: Boolean = true
) : BaseState(pageList)
class RequestExportState(pageList: List<String> = emptyList(), val shouldRequestExportType: Boolean = true) :
BaseState(pageList)
object DoneState : UIState
object CanceledState : UIState

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2023 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.documentscan
@ -22,40 +22,30 @@ class GeneratePDFUseCase @Inject constructor(private val logger: Logger) {
* @param imagePaths list of image paths
* @return `true` if the PDF was generated successfully, `false` otherwise
*/
fun execute(imagePaths: List<String>, filePath: String): Boolean {
return if (imagePaths.isEmpty() || filePath.isBlank()) {
logger.w(TAG, "Invalid parameters: imagePaths: $imagePaths, filePath: $filePath")
false
} else {
val document = PdfDocument()
fillDocumentPages(document, imagePaths)
writePdfToFile(filePath, document)
}
fun execute(imagePaths: List<String>, filePath: String): Boolean = if (imagePaths.isEmpty() || filePath.isBlank()) {
logger.w(TAG, "Invalid parameters: imagePaths: $imagePaths, filePath: $filePath")
false
} else {
val document = PdfDocument()
fillDocumentPages(document, imagePaths)
writePdfToFile(filePath, document)
}
/**
* @return `true` if the PDF was generated successfully, `false` otherwise
*/
private fun writePdfToFile(
filePath: String,
document: PdfDocument
): Boolean {
return try {
val fileOutputStream = FileOutputStream(filePath)
document.writeTo(fileOutputStream)
fileOutputStream.close()
document.close()
true
} catch (ex: IOException) {
logger.e(TAG, "Error generating PDF", ex)
false
}
private fun writePdfToFile(filePath: String, document: PdfDocument): Boolean = try {
val fileOutputStream = FileOutputStream(filePath)
document.writeTo(fileOutputStream)
fileOutputStream.close()
document.close()
true
} catch (ex: IOException) {
logger.e(TAG, "Error generating PDF", ex)
false
}
private fun fillDocumentPages(
document: PdfDocument,
imagePaths: List<String>
) {
private fun fillDocumentPages(document: PdfDocument, imagePaths: List<String>) {
imagePaths.forEach { path ->
val bitmap = BitmapFactory.decodeFile(path)
val pageInfo = PdfDocument.PageInfo.Builder(bitmap.width, bitmap.height, 1).create()

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2022 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.documentscan
@ -112,7 +112,8 @@ class GeneratePdfFromImagesWork(
user,
arrayOf(pdfPath),
arrayOf(uploadPath),
FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, // MIME type will be detected from file name
// MIME type will be detected from file name
FileUploadWorker.LOCAL_BEHAVIOUR_DELETE,
true,
UploadFileOperation.CREATED_BY_USER,
false,

View file

@ -3,7 +3,7 @@
*
* SPDX-FileCopyrightText: 2023 ZetaTom
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.editimage
@ -15,7 +15,6 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
@ -59,9 +58,7 @@ class EditImageActivity :
MimeType.HEIC
)
fun canBePreviewed(file: OCFile): Boolean {
return file.mimeType in supportedMimeTypes
}
fun canBePreviewed(file: OCFile): Boolean = file.mimeType in supportedMimeTypes
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -82,7 +79,6 @@ class EditImageActivity :
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.hide(WindowInsetsCompat.Type.statusBars())
window.statusBarColor = ContextCompat.getColor(this, R.color.black)
window.navigationBarColor = getColor(R.color.black)
setupCropper()
@ -129,9 +125,7 @@ class EditImageActivity :
}
menu?.findItem(R.id.custom_menu_placeholder_item)?.apply {
icon = saveIcon
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
contentDescription = getString(R.string.common_save)
}
contentDescription = getString(R.string.common_save)
}
return true
}

Some files were not shown because too many files have changed in this diff Show more