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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ Use `StateFlow` and `MutableStateFlow` in ViewModels for state management. Colle

Uses Dagger 2 for major Android components (`Activity`, `Fragment`, `Service`, `BroadcastReceiver`, `ContentProvider`). Manual constructor injection for other components.

### Network Clients

Get `OwnCloudClient` and `NextcloudClient` instances from `ClientRepository` (`com.nextcloud.repository`), never from `ClientFactory`.
`ClientFactory` is deprecated and being phased out; do not add new usages of it, and replace it with `ClientRepository` when touching code that still uses it.

- Inject `ClientRepository` with Dagger (`@Inject lateinit var clientRepository: ClientRepository`); it is bound to `RemoteClientRepository`, which resolves the current user via `UserAccountManager` on every call.
- Activities extending `BaseActivity` already expose it via `getClientRepository()`.
- Prefer the `suspend` functions `getOwncloudClient()` / `getNextcloudClient()` from a lifecycle-aware scope (`lifecycleScope`, `viewModelScope`); they switch to `Dispatchers.IO` and return `null` on failure, so handle `null` instead of catching exceptions.
- Do not construct `RemoteClientRepository` manually.

### Extension Functions

The `com.nextcloud.utils.extensions` package contains helper extensions organized by type (e.g., `FileExtensions.kt`, `StringExtensions.kt`, `ViewExtensions.kt`). Create focused extension files rather than putting multiple types in one file.
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/nextcloud/client/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import com.nextcloud.client.notifications.AppNotificationManager
import com.nextcloud.client.notifications.AppNotificationManagerImpl
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.client.utils.Throttler
import com.nextcloud.repository.ClientRepository
import com.nextcloud.repository.RemoteClientRepository
import com.nextcloud.utils.e2ee.E2EEActionResolver
import com.nextcloud.utils.e2ee.E2EEKeyInspector
import com.nextcloud.utils.thumbnail.FolderThumbnailGenerator
Expand Down Expand Up @@ -114,6 +116,9 @@ internal class AppModule {
fun filesRepository(accountManager: UserAccountManager, clientFactory: ClientFactory): FilesRepository =
RemoteFilesRepository(FilesServiceApiImpl(accountManager, clientFactory))

@Provides
fun clientRepository(repository: RemoteClientRepository): ClientRepository = repository

@Provides
fun uploadsStorageManager(currentAccountProvider: CurrentAccountProvider, context: Context): UploadsStorageManager =
UploadsStorageManager(currentAccountProvider, context.contentResolver)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@

import java.io.IOException;

/**
* @deprecated Inject {@link com.nextcloud.repository.ClientRepository} and use its suspend
* {@code getOwncloudClient()} / {@code getNextcloudClient()} functions instead.
*/
@Deprecated
public interface ClientFactory {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,60 +8,66 @@
package com.nextcloud.repository

import android.content.Context
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.di.ApplicationScope
import com.nextcloud.common.NextcloudClient
import com.owncloud.android.lib.common.OwnCloudClient
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
import com.owncloud.android.lib.common.utils.Log_OC
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject

@Suppress("TooGenericExceptionCaught", "DEPRECATION")
class RemoteClientRepository(private val user: User, private val context: Context, lifecycleOwner: LifecycleOwner) :
ClientRepository {
private val tag = "ClientRepository"
class RemoteClientRepository @Inject constructor(
private val context: Context,
private val accountManager: UserAccountManager,
@param:ApplicationScope private val scope: CoroutineScope
) : ClientRepository {
companion object {
private const val TAG = "ClientRepository"
}

private val clientFactory = OwnCloudClientManagerFactory.getDefaultSingleton()
private val scope = lifecycleOwner.lifecycleScope

override fun getNextcloudClient(onComplete: (NextcloudClient) -> Unit) {
scope.launch(Dispatchers.IO) {
try {
val client = clientFactory.getNextcloudClientFor(user.toOwnCloudAccount(), context)
val client = clientFactory.getNextcloudClientFor(accountManager.user.toOwnCloudAccount(), context)
onComplete(client)
} catch (e: Exception) {
Log_OC.d(tag, "Exception caught getNextcloudClient(): $e")
Log_OC.d(TAG, "Exception caught getNextcloudClient(): $e")
}
}
}

override suspend fun getNextcloudClient(): NextcloudClient? = withContext(Dispatchers.IO) {
try {
clientFactory.getNextcloudClientFor(user.toOwnCloudAccount(), context)
clientFactory.getNextcloudClientFor(accountManager.user.toOwnCloudAccount(), context)
} catch (e: Exception) {
Log_OC.d(tag, "Exception caught getNextcloudClient(): $e")
Log_OC.d(TAG, "Exception caught getNextcloudClient(): $e")
null
}
}

override fun getOwncloudClient(onComplete: (OwnCloudClient) -> Unit) {
scope.launch(Dispatchers.IO) {
try {
val client = clientFactory.getClientFor(user.toOwnCloudAccount(), context)
val client = clientFactory.getClientFor(accountManager.user.toOwnCloudAccount(), context)
onComplete(client)
} catch (e: Exception) {
Log_OC.d(tag, "Exception caught getOwncloudClient(): $e")
Log_OC.d(TAG, "Exception caught getOwncloudClient(): $e")
}
}
}

override suspend fun getOwncloudClient(): OwnCloudClient? = withContext(Dispatchers.IO) {
try {
clientFactory.getClientFor(user.toOwnCloudAccount(), context)
clientFactory.getClientFor(accountManager.user.toOwnCloudAccount(), context)
} catch (e: Exception) {
Log_OC.d(tag, "Exception caught getOwncloudClient(): $e")
Log_OC.d(TAG, "Exception caught getOwncloudClient(): $e")
null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import com.nextcloud.client.preferences.AppPreferences;
import com.nextcloud.client.preferences.DarkMode;
import com.nextcloud.repository.ClientRepository;
import com.nextcloud.repository.RemoteClientRepository;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.lib.common.utils.Log_OC;
Expand Down Expand Up @@ -53,6 +52,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
@Inject UserAccountManager accountManager;
@Inject AppPreferences preferences;
@Inject FileDataStorageManager fileDataStorageManager;
@Inject ClientRepository clientRepository;

private final AppPreferences.Listener onPreferencesChanged = new AppPreferences.Listener() {
@Override
Expand All @@ -65,8 +65,6 @@ public UserAccountManager getUserAccountManager() {
return accountManager;
}

private ClientRepository clientRepository;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
AppCompatActivityExtensionsKt.applyEdgeToEdgeWithSystemBarPadding(this);
Expand All @@ -77,8 +75,6 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
if (enableAccountHandling) {
mixinRegistry.onCreate(savedInstanceState);
}

clientRepository = new RemoteClientRepository(accountManager.getUser(), this, this);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,9 @@ import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.jobs.upload.FileUploadWorker
import com.nextcloud.client.network.ClientFactory
import com.nextcloud.client.network.ClientFactory.CreationException
import com.nextcloud.client.utils.IntentUtil
import com.nextcloud.client.utils.Throttler
import com.nextcloud.repository.ClientRepository
import com.nextcloud.ui.albumItemActions.AlbumItemActionsBottomSheet
import com.nextcloud.ui.fileactions.FileActionsBottomSheet
import com.nextcloud.utils.SnackbarUtil
Expand Down Expand Up @@ -122,7 +121,7 @@ class AlbumItemsFragment :
lateinit var accountManager: UserAccountManager

@Inject
lateinit var clientFactory: ClientFactory
lateinit var clientRepository: ClientRepository

@Inject
lateinit var throttler: Throttler
Expand All @@ -135,7 +134,6 @@ class AlbumItemsFragment :

private var adapter: GalleryAdapter? = null
private var addMediaFab: FloatingActionButton? = null
private var client: OwnCloudClient? = null
private var optionalUser: Optional<User>? = null
private var containerActivity: FileFragment.ContainerActivity? = null
private var selectionMode: AlbumItemsMultiChoiceModeListener? = null
Expand Down Expand Up @@ -286,24 +284,7 @@ class AlbumItemsFragment :
binding.listFragmentLayout.addView(addMediaFab, layoutParams)
}

private fun initializeClient() {
if (client != null) {
return
}

val user = optionalUser?.takeIf { it.isPresent }?.get() ?: return

client = try {
clientFactory.create(user)
} catch (e: CreationException) {
Log_OC.e(TAG, "Error initializing client", e)
null
}
}

private fun initializeAdapter() {
initializeClient()

if (adapter == null) {
adapter = GalleryAdapter(
requireContext(),
Expand Down Expand Up @@ -365,7 +346,7 @@ class AlbumItemsFragment :
showLoadingMessageWhenReachable()

lifecycleScope.launch(Dispatchers.IO) {
val client = client ?: run {
val client = clientRepository.getOwncloudClient() ?: run {
withContext(Dispatchers.Main) { onAlbumItemsFailed(null) }
return@launch
}
Expand Down Expand Up @@ -434,7 +415,7 @@ class AlbumItemsFragment :
fun refreshAlbumMetaData() {
lifecycleScope.launch(Dispatchers.IO) {
val albumsRemoteOperation = ReadAlbumsRemoteOperation(albumName)
val result = client?.let { albumsRemoteOperation.execute(it) }
val result = clientRepository.getOwncloudClient()?.let { albumsRemoteOperation.execute(it) }

withContext(Dispatchers.Main) {
photoAlbumEntry = result
Expand Down Expand Up @@ -666,18 +647,16 @@ class AlbumItemsFragment :

@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onMessageEvent(event: FavoriteEvent) {
try {
val client = clientFactory.create(accountManager.user)
lifecycleScope.launch(Dispatchers.IO) {
val client = clientRepository.getOwncloudClient() ?: return@launch
val toggleFavoriteOperation = ToggleAlbumFavoriteRemoteOperation(event.shouldFavorite, event.remotePath)

if (!toggleFavoriteOperation.execute(client).isSuccess) {
return
return@launch
}

Handler(Looper.getMainLooper()).post { selectionMode?.exitSelectionMode() }
adapter?.markAsFavorite(event.remotePath, event.shouldFavorite)
} catch (e: CreationException) {
Log_OC.e(TAG, "Error processing event", e)
}
}
//endregion
Expand Down Expand Up @@ -727,12 +706,7 @@ class AlbumItemsFragment :
lifecycleScope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) { showLoadingDialog() }

val failedFiles = try {
removeFromAlbum(clientFactory.create(accountManager.user), files)
} catch (e: CreationException) {
Log_OC.e(TAG, "Error removing album files", e)
emptyList()
}
val failedFiles = clientRepository.getOwncloudClient()?.let { removeFromAlbum(it, files) }.orEmpty()

Log_OC.d(TAG, "Files that could not be removed: ${failedFiles.size}")

Expand Down
Loading