diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a098c8ff9..afb39eef3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -170,7 +170,11 @@ + android:theme="@style/AppTheme" /> + + () { + Single.fromCallable(new Callable() { @Override - public ReadFilesystemOperation call() { - return new ReadFilesystemOperation(okHttpClient, activeUser, url, 0); + public LegacyReadFilesystemOperation call() { + return new LegacyReadFilesystemOperation(okHttpClient, activeUser, url, 0); } }).observeOn(Schedulers.io()) - .subscribe(new SingleObserver() { + .subscribe(new SingleObserver() { @Override public void onSubscribe(@NonNull Disposable d) { // unused atm } @Override - public void onSuccess(@NonNull ReadFilesystemOperation readFilesystemOperation) { + public void onSuccess(@NonNull LegacyReadFilesystemOperation readFilesystemOperation) { DavResponse davResponse = readFilesystemOperation.readRemotePath(); if (davResponse.data != null) { List browserFileList = (List) davResponse.data; diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java index dc69a1422..f368d0b4c 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java @@ -26,7 +26,7 @@ import android.util.Log; import com.nextcloud.talk.components.filebrowser.interfaces.ListingInterface; import com.nextcloud.talk.components.filebrowser.models.DavResponse; -import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation; +import com.nextcloud.talk.components.filebrowser.webdav.LegacyReadFilesystemOperation; import com.nextcloud.talk.models.database.UserEntity; import java.util.concurrent.Callable; @@ -50,20 +50,20 @@ public class DavListing extends ListingAbstractClass { @Override public void getFiles(String path, UserEntity currentUser, @Nullable OkHttpClient okHttpClient) { - Single.fromCallable(new Callable() { + Single.fromCallable(new Callable() { @Override - public ReadFilesystemOperation call() { - return new ReadFilesystemOperation(okHttpClient, currentUser, path, 1); + public LegacyReadFilesystemOperation call() { + return new LegacyReadFilesystemOperation(okHttpClient, currentUser, path, 1); } }).subscribeOn(Schedulers.io()) - .subscribe(new SingleObserver() { + .subscribe(new SingleObserver() { @Override public void onSubscribe(@NonNull Disposable d) { } @Override - public void onSuccess(@NonNull ReadFilesystemOperation readFilesystemOperation) { + public void onSuccess(@NonNull LegacyReadFilesystemOperation readFilesystemOperation) { davResponse = readFilesystemOperation.readRemotePath(); try { listingInterface.listingResult(davResponse); diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/LegacyReadFilesystemOperation.java similarity index 96% rename from app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.java rename to app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/LegacyReadFilesystemOperation.java index ed8e978cf..35766e07f 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.java +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/LegacyReadFilesystemOperation.java @@ -40,14 +40,15 @@ import kotlin.jvm.functions.Function2; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; -public class ReadFilesystemOperation { +@Deprecated +public class LegacyReadFilesystemOperation { private static final String TAG = "ReadFilesystemOperation"; private final OkHttpClient okHttpClient; private final String url; private final int depth; private final String basePath; - public ReadFilesystemOperation(OkHttpClient okHttpClient, UserEntity currentUser, String path, int depth) { + public LegacyReadFilesystemOperation(OkHttpClient okHttpClient, UserEntity currentUser, String path, int depth) { OkHttpClient.Builder okHttpClientBuilder = okHttpClient.newBuilder(); okHttpClientBuilder.followRedirects(false); okHttpClientBuilder.followSslRedirects(false); diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.kt b/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.kt new file mode 100644 index 000000000..b0d86c0ea --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.kt @@ -0,0 +1,184 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * @author Mario Danic + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2017-2019 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.components.filebrowser.webdav + +import android.net.Uri +import android.text.TextUtils +import android.util.Log +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.Response.HrefRelation +import at.bitfire.dav4jvm.exception.DavException +import at.bitfire.dav4jvm.property.DisplayName +import at.bitfire.dav4jvm.property.GetContentType +import at.bitfire.dav4jvm.property.GetLastModified +import at.bitfire.dav4jvm.property.ResourceType +import com.nextcloud.talk.components.filebrowser.models.DavResponse +import com.nextcloud.talk.components.filebrowser.models.properties.NCEncrypted +import com.nextcloud.talk.components.filebrowser.models.properties.NCPermission +import com.nextcloud.talk.components.filebrowser.models.properties.NCPreview +import com.nextcloud.talk.components.filebrowser.models.properties.OCFavorite +import com.nextcloud.talk.components.filebrowser.models.properties.OCId +import com.nextcloud.talk.components.filebrowser.models.properties.OCSize +import com.nextcloud.talk.dagger.modules.RestModule.MagicAuthenticator +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import com.nextcloud.talk.utils.ApiUtils +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import java.io.File +import java.io.IOException + +class ReadFilesystemOperation(okHttpClient: OkHttpClient, currentUser: UserEntity, path: String, depth: Int) { + private val okHttpClient: OkHttpClient + private val url: String + private val depth: Int + private val basePath: String + fun readRemotePath(): DavResponse { + val davResponse = DavResponse() + val memberElements: MutableList = ArrayList() + val rootElement = arrayOfNulls(1) + val remoteFiles: MutableList = ArrayList() + try { + DavResource( + okHttpClient, + url.toHttpUrlOrNull()!! + ).propfind( + depth = depth, + reqProp = DavUtils.getAllPropSet() + ) { response: Response, hrefRelation: HrefRelation? -> + davResponse.setResponse(response) + when (hrefRelation) { + HrefRelation.MEMBER -> memberElements.add(response) + HrefRelation.SELF -> rootElement[0] = response + HrefRelation.OTHER -> {} + else -> {} + } + Unit + } + } catch (e: IOException) { + Log.w(TAG, "Error reading remote path") + } catch (e: DavException) { + Log.w(TAG, "Error reading remote path") + } + remoteFiles.add( + getModelFromResponse( + rootElement[0]!!, + rootElement[0]!! + .href + .toString() + .substring(basePath.length) + ) + ) + for (memberElement in memberElements) { + remoteFiles.add( + getModelFromResponse( + memberElement, + memberElement + .href + .toString() + .substring(basePath.length) + ) + ) + } + davResponse.setData(remoteFiles) + return davResponse + } + + companion object { + private const val TAG = "ReadFilesystemOperation" + } + + init { + val okHttpClientBuilder: OkHttpClient.Builder = okHttpClient.newBuilder() + okHttpClientBuilder.followRedirects(false) + okHttpClientBuilder.followSslRedirects(false) + okHttpClientBuilder.authenticator( + MagicAuthenticator( + ApiUtils.getCredentials( + currentUser.username, + currentUser.token + ), + "Authorization" + ) + ) + this.okHttpClient = okHttpClientBuilder.build() + basePath = currentUser.baseUrl + DavUtils.DAV_PATH + currentUser.userId + url = basePath + path + this.depth = depth + } + + private fun getModelFromResponse(response: Response, remotePath: String): RemoteFileBrowserItem { + val remoteFileBrowserItem = RemoteFileBrowserItem() + remoteFileBrowserItem.path = Uri.decode(remotePath) + remoteFileBrowserItem.displayName = Uri.decode(File(remotePath).name) + val properties = response.properties + for (property in properties) { + mapPropertyToBrowserFile(property, remoteFileBrowserItem) + } + if (remoteFileBrowserItem.permissions != null && remoteFileBrowserItem.permissions!!.contains("R")) { + remoteFileBrowserItem.isAllowedToReShare = true + } + if (TextUtils.isEmpty(remoteFileBrowserItem.mimeType) && !remoteFileBrowserItem.isFile) { + remoteFileBrowserItem.mimeType = "inode/directory" + } + + return remoteFileBrowserItem + } + + @Suppress("Detekt.ComplexMethod") + private fun mapPropertyToBrowserFile(property: Property, remoteFileBrowserItem: RemoteFileBrowserItem) { + when (property) { + is OCId -> { + remoteFileBrowserItem.remoteId = property.ocId + } + is ResourceType -> { + remoteFileBrowserItem.isFile = !property.types.contains(ResourceType.COLLECTION) + } + is GetLastModified -> { + remoteFileBrowserItem.modifiedTimestamp = property.lastModified + } + is GetContentType -> { + remoteFileBrowserItem.mimeType = property.type + } + is OCSize -> { + remoteFileBrowserItem.size = property.ocSize + } + is NCPreview -> { + remoteFileBrowserItem.hasPreview = property.isNcPreview + } + is OCFavorite -> { + remoteFileBrowserItem.isFavorite = property.isOcFavorite + } + is DisplayName -> { + remoteFileBrowserItem.displayName = property.displayName + } + is NCEncrypted -> { + remoteFileBrowserItem.isEncrypted = property.isNcEncrypted + } + is NCPermission -> { + remoteFileBrowserItem.permissions = property.ncPermission + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt index 19aa363e0..cfcd00202 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt @@ -29,7 +29,6 @@ import android.graphics.BitmapFactory import android.graphics.Color import android.net.Uri import android.os.Bundle -import android.os.Environment import android.text.Editable import android.text.TextUtils import android.text.TextWatcher @@ -48,8 +47,6 @@ import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.ViewCompat import androidx.recyclerview.widget.RecyclerView import autodagger.AutoInjector -import com.bluelinelabs.conductor.RouterTransaction -import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getError import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getFile @@ -59,7 +56,6 @@ import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.components.filebrowser.controllers.BrowserController.BrowserType -import com.nextcloud.talk.components.filebrowser.controllers.BrowserForAvatarController import com.nextcloud.talk.controllers.base.NewBaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.databinding.ControllerProfileBinding @@ -71,11 +67,15 @@ import com.nextcloud.talk.models.json.userprofile.Scope import com.nextcloud.talk.models.json.userprofile.UserProfileData import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall import com.nextcloud.talk.models.json.userprofile.UserProfileOverall +import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity import com.nextcloud.talk.ui.dialog.ScopeDialog import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.FileUtils import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BROWSER_TYPE +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MIME_TYPE_FILTER import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SINGLE_SELECTION import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY import com.nextcloud.talk.utils.database.user.UserUtils import io.reactivex.Observer @@ -496,12 +496,22 @@ class ProfileController : NewBaseController(R.layout.controller_profile) { KEY_USER_ENTITY, Parcels.wrap(UserEntity::class.java, currentUser) ) + bundle.putBoolean(KEY_SINGLE_SELECTION, true) + bundle.putString(KEY_MIME_TYPE_FILTER, "image/") bundle.putString(KEY_ROOM_TOKEN, "123") + + val avatarIntent = Intent(activity, RemoteFileBrowserActivity::class.java) + avatarIntent.putExtras(bundle) + + startActivityForResult(avatarIntent, RemoteFileBrowserActivity.REQUEST_CODE_SELECT_AVATAR) + + /* router.pushController( RouterTransaction.with(BrowserForAvatarController(bundle, this)) .pushChangeHandler(VerticalChangeHandler()) .popChangeHandler(VerticalChangeHandler()) ) + */ } fun handleAvatar(remotePath: String?) { @@ -526,10 +536,7 @@ class ProfileController : NewBaseController(R.layout.controller_profile) { private fun saveBitmapAndPassToImagePicker(bitmap: Bitmap) { var file: File? = null try { - file = File.createTempFile( - "avatar", "png", - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) - ) + file = FileUtils.getTempCacheFile(context!!, "avatar/avatar.png") try { FileOutputStream(file).use { out -> bitmap.compress(Bitmap.CompressFormat.PNG, FULL_QUALITY, out) } } catch (e: IOException) { @@ -555,6 +562,7 @@ class ProfileController : NewBaseController(R.layout.controller_profile) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (resultCode == Activity.RESULT_OK) { + uploadAvatar(getFile(data)) } else if (resultCode == ImagePicker.RESULT_ERROR) { Toast.makeText(activity, getError(data), Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt index 0e62a8645..267ab6b41 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt @@ -22,6 +22,8 @@ package com.nextcloud.talk.dagger.modules import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepository +import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepositoryImpl import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepositoryImpl import com.nextcloud.talk.shareditems.repositories.SharedItemsRepository @@ -29,6 +31,7 @@ import com.nextcloud.talk.shareditems.repositories.SharedItemsRepositoryImpl import com.nextcloud.talk.utils.database.user.CurrentUserProvider import dagger.Module import dagger.Provides +import okhttp3.OkHttpClient @Module class RepositoryModule { @@ -41,4 +44,10 @@ class RepositoryModule { fun provideUnifiedSearchRepository(ncApi: NcApi, userProvider: CurrentUserProvider): UnifiedSearchRepository { return UnifiedSearchRepositoryImpl(ncApi, userProvider) } + + @Provides + fun provideRemoteFileBrowserItemsRepository(okHttpClient: OkHttpClient, userProvider: CurrentUserProvider): + RemoteFileBrowserItemsRepository { + return RemoteFileBrowserItemsRepositoryImpl(okHttpClient, userProvider) + } } diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt index b0f7170d8..f2356d0ab 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt @@ -23,6 +23,7 @@ package com.nextcloud.talk.dagger.modules import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel import com.nextcloud.talk.messagesearch.MessageSearchViewModel import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel import dagger.Binds @@ -59,4 +60,9 @@ abstract class ViewModelModule { @IntoMap @ViewModelKey(MessageSearchViewModel::class) abstract fun messageSearchViewModel(viewModel: MessageSearchViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(RemoteFileBrowserItemsViewModel::class) + abstract fun remoteFileBrowserItemsViewModel(viewModel: RemoteFileBrowserItemsViewModel): ViewModel } diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt new file mode 100644 index 000000000..47f777c7c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt @@ -0,0 +1,345 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.remotefilebrowser.activities + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.ActivityRemoteFileBrowserBinding +import com.nextcloud.talk.interfaces.SelectionInterface +import com.nextcloud.talk.remotefilebrowser.adapters.RemoteFileBrowserItemsAdapter +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel +import com.nextcloud.talk.ui.dialog.SortingOrderDialogFragment +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.FileSortOrder +import com.nextcloud.talk.utils.FileSortOrderNew +import com.nextcloud.talk.utils.database.user.UserUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import net.orange_box.storebox.listeners.OnPreferenceValueChangedListener +import java.io.File +import java.util.Collections +import java.util.TreeSet +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class RemoteFileBrowserActivity : AppCompatActivity(), SelectionInterface, SwipeRefreshLayout.OnRefreshListener { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var userUtils: UserUtils + + private lateinit var binding: ActivityRemoteFileBrowserBinding + private lateinit var viewModel: RemoteFileBrowserItemsViewModel + + private var filesSelectionDoneMenuItem: MenuItem? = null + + private val selectedPaths: MutableSet = Collections.synchronizedSet(TreeSet()) + private var currentPath: String = "/" + + private var browserItems: List = emptyList() + private var adapter: RemoteFileBrowserItemsAdapter? = null + + private var sortingChangeListener: OnPreferenceValueChangedListener? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + binding = ActivityRemoteFileBrowserBinding.inflate(layoutInflater) + setSupportActionBar(binding.remoteFileBrowserItemsToolbar) + setContentView(binding.root) + + DisplayUtils.applyColorToStatusBar( + this, + ResourcesCompat.getColor( + resources, R.color.appbar, null + ) + ) + DisplayUtils.applyColorToNavigationBar( + this.window, + ResourcesCompat.getColor(resources, R.color.bg_default, null) + ) + + supportActionBar?.title = "current patch" + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + initViewModel() + + binding.swipeRefreshList.setOnRefreshListener(this) + binding.swipeRefreshList.setColorSchemeResources(R.color.colorPrimary) + binding.swipeRefreshList.setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background) + + appPreferences.registerSortingChangeListener( + SortingChangeListener(this).also { + sortingChangeListener = it + } + ) + + viewModel.loadItems(currentPath) + } + + private fun initViewModel() { + viewModel = ViewModelProvider(this, viewModelFactory)[RemoteFileBrowserItemsViewModel::class.java] + + viewModel.viewState.observe(this) { state -> + clearEmptyLoading() + when (state) { + is RemoteFileBrowserItemsViewModel.LoadingItemsState, RemoteFileBrowserItemsViewModel.InitialState -> { + showLoading() + } + + is RemoteFileBrowserItemsViewModel.NoRemoteFileItemsState -> { + showEmpty() + } + + is RemoteFileBrowserItemsViewModel.LoadedState -> { + val remoteFileBrowserItems = state.items + Log.d(TAG, "Items received: $remoteFileBrowserItems") + + // TODO make shwoGrid based on preferences + val showGrid = false + val layoutManager = if (showGrid) { + GridLayoutManager(this, SPAN_COUNT) + } else { + LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + } + + // TODO make mimeTypeSelectionFilter a bundled arg for the activity + val mimeTypeSelectionFilter = "image/" + val adapter = RemoteFileBrowserItemsAdapter( + showGrid = showGrid, + mimeTypeSelectionFilter = mimeTypeSelectionFilter, + userEntity = userUtils.currentUser!!, + selectionInterface = this + ) { remoteFileBrowserItem -> + onItemClicked(remoteFileBrowserItem) + } + .apply { + items = if (remoteFileBrowserItems.size > 1) { + remoteFileBrowserItems.subList(1, remoteFileBrowserItems.size) + } else { + ArrayList() + } + browserItems = items + } + + binding.recyclerView.adapter = adapter + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.setHasFixedSize(true) + + showList() + } + } + } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + super.onCreateOptionsMenu(menu) + menuInflater.inflate(R.menu.menu_share_files, menu) + filesSelectionDoneMenuItem = menu?.findItem(R.id.files_selection_done) + filesSelectionDoneMenuItem?.isVisible = selectedPaths.size > 0 + return true + } + + private fun onItemClicked(remoteFileBrowserItem: RemoteFileBrowserItem) { + if ("inode/directory" == remoteFileBrowserItem.mimeType) { + currentPath = remoteFileBrowserItem.path!! + viewModel.loadItems(currentPath) + } else { + toggleBrowserItemSelection(remoteFileBrowserItem.path!!) + } + } + + override fun onResume() { + super.onResume() + + binding.pathNavigationBackButton.setOnClickListener { goBack() } + binding.sortButton.setOnClickListener { changeSorting() } + + binding.sortButton.setText( + DisplayUtils.getSortOrderStringId(FileSortOrder.getFileSortOrder(appPreferences.sorting)) + ) + + refreshCurrentPath() + } + + fun changeSorting() { + val newFragment: DialogFragment = SortingOrderDialogFragment + .newInstance(FileSortOrder.getFileSortOrder(appPreferences.sorting)) + newFragment.show( + supportFragmentManager, + SortingOrderDialogFragment.SORTING_ORDER_FRAGMENT + ) + } + + private fun goBack(): Boolean { + if (currentPath != "/") { + viewModel.loadItems(File(currentPath).parent!!) + } + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + onBackPressed() + true + } + R.id.files_selection_done -> { + onFileSelectionDone() + true + } + else -> { + return super.onOptionsItemSelected(item) + } + } + } + + private fun onFileSelectionDone() { + val data = Intent() + data.putStringArrayListExtra(EXTRA_SELECTED_PATHS, ArrayList(selectedPaths)) + setResult(Activity.RESULT_OK, data) + finish() + } + + private fun clearEmptyLoading() { + binding.emptyContainer.emptyListView.visibility = View.GONE + } + + private fun showLoading() { + binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.file_list_loading) + binding.emptyContainer.emptyListView.visibility = View.VISIBLE + binding.recyclerView.visibility = View.GONE + } + + private fun showEmpty() { + binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.nc_shared_items_empty) + binding.emptyContainer.emptyListView.visibility = View.VISIBLE + binding.recyclerView.visibility = View.GONE + } + + private fun showList() { + binding.recyclerView.visibility = View.VISIBLE + } + + override fun onRefresh() { + refreshCurrentPath() + } + + private fun refreshCurrentPath() { + viewModel.loadItems(currentPath) + } + + private fun shouldPathBeSelectedDueToParent(currentPath: String): Boolean { + var file = File(currentPath) + if (selectedPaths.size > 0 && file.parent != "/") { + while (file.parent != null) { + var parent = file.parent!! + if (File(file.parent!!).parent != null) { + parent += "/" + } + if (selectedPaths.contains(parent)) { + return true + } + file = File(file.parent!!) + } + } + return false + } + + private fun checkAndRemoveAnySelectedParents(currentPath: String) { + var file = File(currentPath) + selectedPaths.remove(currentPath) + while (file.parent != null) { + selectedPaths.remove(file.parent!! + "/") + file = File(file.parent!!) + } + runOnUiThread { + binding.recyclerView.adapter!!.notifyDataSetChanged() + } + } + + companion object { + private val TAG = RemoteFileBrowserActivity::class.simpleName + const val SPAN_COUNT: Int = 4 + const val EXTRA_SELECTED_PATHS = "EXTRA_SELECTED_PATH" + const val REQUEST_CODE_SELECT_AVATAR = 22 + } + + override fun toggleBrowserItemSelection(path: String) { + if (selectedPaths.contains(path) || shouldPathBeSelectedDueToParent(path)) { + checkAndRemoveAnySelectedParents(path) + } else { + // TODO if it's a folder, remove all the children we added manually + selectedPaths.add(path) + } + filesSelectionDoneMenuItem?.isVisible = selectedPaths.size > 0 + } + + override fun isPathSelected(path: String): Boolean { + return selectedPaths.contains(path) || shouldPathBeSelectedDueToParent(path) + } + + override fun shouldOnlySelectOneImageFile(): Boolean { + return true + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private class SortingChangeListener(private val activity: RemoteFileBrowserActivity) : + OnPreferenceValueChangedListener { + override fun onChanged(newValue: String) { + try { + val sortOrder = FileSortOrderNew.getFileSortOrder(newValue) + + activity.binding.sortButton.setText(DisplayUtils.getSortOrderStringId(sortOrder)) + activity.browserItems = sortOrder.sortCloudFiles(activity.browserItems) + + activity.runOnUiThread { + activity.adapter!!.updateDataSet(activity.browserItems) + } + } catch (npe: NullPointerException) { + // view binding can be null + // since this is called asynchronously and UI might have been destroyed in the meantime + Log.i(TAG, "UI destroyed - view binding already gone") + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsAdapter.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsAdapter.kt new file mode 100644 index 000000000..0af1015ee --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsAdapter.kt @@ -0,0 +1,86 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.remotefilebrowser.adapters + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.databinding.RvItemBrowserFileBinding +import com.nextcloud.talk.interfaces.SelectionInterface +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem + +class RemoteFileBrowserItemsAdapter( + private val showGrid: Boolean = false, + private val mimeTypeSelectionFilter: String? = null, + private val userEntity: UserEntity, + private val selectionInterface: SelectionInterface, + private val onItemClicked: (RemoteFileBrowserItem) -> Unit +) : RecyclerView.Adapter() { + + var items: List = emptyList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RemoteFileBrowserItemsViewHolder { + + return if (showGrid) { + RemoteFileBrowserItemsListViewHolder( + RvItemBrowserFileBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + mimeTypeSelectionFilter, + userEntity, + selectionInterface + ) { + onItemClicked(items[it]) + } + } else { + RemoteFileBrowserItemsListViewHolder( + RvItemBrowserFileBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + mimeTypeSelectionFilter, + userEntity, + selectionInterface + ) { + onItemClicked(items[it]) + } + } + } + + override fun onBindViewHolder(holder: RemoteFileBrowserItemsViewHolder, position: Int) { + holder.onBind(items[position]) + } + + override fun getItemCount(): Int { + return items.size + } + + @SuppressLint("NotifyDataSetChanged") + fun updateDataSet(browserItems: List) { + items = browserItems + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt new file mode 100644 index 000000000..7317c2a3f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt @@ -0,0 +1,153 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 202 Andy Scherzinger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.remotefilebrowser.adapters + +import android.text.format.Formatter +import android.util.Log +import android.view.View +import androidx.appcompat.content.res.AppCompatResources +import autodagger.AutoInjector +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.drawee.interfaces.DraweeController +import com.facebook.drawee.view.SimpleDraweeView +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.RvItemBrowserFileBinding +import com.nextcloud.talk.interfaces.SelectionInterface +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DateUtils.getLocalDateTimeStringFromTimestamp +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType + +@AutoInjector(NextcloudTalkApplication::class) +class RemoteFileBrowserItemsListViewHolder( + override val binding: RvItemBrowserFileBinding, + mimeTypeSelectionFilter: String?, + currentUser: UserEntity, + selectionInterface: SelectionInterface, + onItemClicked: (Int) -> Unit +) : RemoteFileBrowserItemsViewHolder(binding, mimeTypeSelectionFilter, currentUser, selectionInterface) { + + override val fileIcon: SimpleDraweeView + get() = binding.fileIcon + + private var selectable : Boolean = true + private var clickable : Boolean = true + + init { + itemView.setOnClickListener { + if (clickable) { + onItemClicked(bindingAdapterPosition) + if (selectable) { + binding.selectFileCheckbox.toggle() + } + } + } + } + + override fun onBind(item: RemoteFileBrowserItem) { + + super.onBind(item) + + binding.fileIcon.controller = null + if (!item.isAllowedToReShare || item.isEncrypted) { + binding.root.isEnabled = false + binding.root.alpha = DISABLED_ALPHA + } else { + binding.root.isEnabled = true + binding.root.alpha = ENABLED_ALPHA + } + + binding.fileEncryptedImageView.visibility = + if (item.isEncrypted) { + View.VISIBLE + } else { + View.GONE + } + + binding.fileFavoriteImageView.visibility = + if (item.isFavorite) { + View.VISIBLE + } else { + View.GONE + } + + calculateSelectability(item) + calculateClickability(item, selectable) + setSelectability() + + binding.fileIcon + .hierarchy + .setPlaceholderImage( + AppCompatResources.getDrawable( + binding.fileIcon.context, getDrawableResourceIdForMimeType(item.mimeType) + ) + ) + + if (item.hasPreview) { + val path = ApiUtils.getUrlForFilePreviewWithRemotePath( + currentUser.baseUrl, + item.path, + binding.fileIcon.context.resources.getDimensionPixelSize(R.dimen.small_item_height) + ) + if (path.isNotEmpty()) { + val draweeController: DraweeController = Fresco.newDraweeControllerBuilder() + .setAutoPlayAnimations(true) + .setImageRequest(DisplayUtils.getImageRequestForUrl(path, null)) + .build() + binding.fileIcon.controller = draweeController + } + } + + binding.filenameTextView.text = item.displayName + binding.fileModifiedInfo.text = String.format( + binding.fileModifiedInfo.context.getString(R.string.nc_last_modified), + Formatter.formatShortFileSize(binding.fileModifiedInfo.context, item.size), + getLocalDateTimeStringFromTimestamp(item.modifiedTimestamp) + ) + + binding.selectFileCheckbox.isChecked = selectionInterface.isPathSelected(item.path!!) + } + + private fun setSelectability() { + if (selectable) { + binding.selectFileCheckbox.visibility = View.VISIBLE + } else { + binding.selectFileCheckbox.visibility = View.GONE + } + } + + private fun calculateSelectability(item: RemoteFileBrowserItem) { + selectable = item.isFile && + (mimeTypeSelectionFilter == null || item.mimeType?.startsWith(mimeTypeSelectionFilter) == true) + } + + private fun calculateClickability(item: RemoteFileBrowserItem, selectableItem: Boolean) { + clickable = selectableItem || "inode/directory" == item.mimeType + } + + companion object { + private const val DISABLED_ALPHA: Float = 0.38f + private const val ENABLED_ALPHA: Float = 1.0f + } +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt new file mode 100644 index 000000000..4ebc5f7b9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt @@ -0,0 +1,53 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.remotefilebrowser.adapters + +import android.graphics.drawable.Drawable +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.facebook.drawee.view.SimpleDraweeView +import com.nextcloud.talk.interfaces.SelectionInterface +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import com.nextcloud.talk.utils.DrawableUtils + +abstract class RemoteFileBrowserItemsViewHolder( + open val binding: ViewBinding, + val mimeTypeSelectionFilter: String? = null, + val currentUser: UserEntity, + val selectionInterface: SelectionInterface, +) : RecyclerView.ViewHolder(binding.root) { + + abstract val fileIcon: SimpleDraweeView + + open fun onBind(item: RemoteFileBrowserItem) { + fileIcon.hierarchy.setPlaceholderImage(staticImage(item.mimeType, fileIcon)) + } + + private fun staticImage( + mimeType: String?, + image: SimpleDraweeView + ): Drawable { + val drawableResourceId = DrawableUtils.getDrawableResourceIdForMimeType(mimeType) + return ContextCompat.getDrawable(image.context, drawableResourceId)!! + } +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/model/RemoteFileBrowserItem.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/model/RemoteFileBrowserItem.kt new file mode 100644 index 000000000..32e06fba8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/model/RemoteFileBrowserItem.kt @@ -0,0 +1,49 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * @author Mario Danic + * Copyright (C) 202 Andy Scherzinger + * Copyright (C) 2017-2018 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.remotefilebrowser.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.android.parcel.Parcelize + +@Parcelize +@JsonObject +data class RemoteFileBrowserItem( + var path: String? = null, + var displayName: String? = null, + var mimeType: String? = null, + var modifiedTimestamp: Long = 0, + var size: Long = 0, + var isFile: Boolean = false, + + // Used for remote files + var remoteId: String? = null, + var hasPreview: Boolean = false, + var isFavorite: Boolean = false, + var isEncrypted: Boolean = false, + var permissions: String? = null, + var isAllowedToReShare: Boolean = false +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, 0, 0, false, null, false, false, false, null, false) +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepository.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepository.kt new file mode 100644 index 000000000..a2ea15009 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepository.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 202 Andy Scherzinger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.remotefilebrowser.repositories + +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import io.reactivex.Observable + +interface RemoteFileBrowserItemsRepository { + + fun listFolder(path: String): Observable> +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepositoryImpl.kt new file mode 100644 index 000000000..c992462aa --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepositoryImpl.kt @@ -0,0 +1,56 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 202 Andy Scherzinger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.remotefilebrowser.repositories + +import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import com.nextcloud.talk.utils.database.user.CurrentUserProvider +import io.reactivex.Observable +import okhttp3.OkHttpClient +import javax.inject.Inject + +class RemoteFileBrowserItemsRepositoryImpl @Inject constructor( + private val okHttpClient: OkHttpClient, + private val userProvider: CurrentUserProvider +) : RemoteFileBrowserItemsRepository { + + private val userEntity: UserEntity + get() = userProvider.currentUser!! + + override fun listFolder(path: String): + Observable> { + return Observable.fromCallable { + val operation = + ReadFilesystemOperation( + okHttpClient, + userEntity, + path, + 1 + ) + val davResponse = operation.readRemotePath() + if (davResponse.getData() != null) { + return@fromCallable davResponse.getData() as List + } + return@fromCallable emptyList() + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt new file mode 100644 index 000000000..f37edcfbb --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt @@ -0,0 +1,93 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 202 Andy Scherzinger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.remotefilebrowser.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepository +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class RemoteFileBrowserItemsViewModel @Inject constructor( + private val repository: RemoteFileBrowserItemsRepository +) : + ViewModel() { + + sealed interface ViewState + object InitialState : ViewState + object NoRemoteFileItemsState : ViewState + object LoadingItemsState : ViewState + class LoadedState(val items: List) : ViewState + + private val _viewState: MutableLiveData = MutableLiveData(InitialState) + + val viewState: LiveData + get() = _viewState + + fun loadItems(path: String) { + _viewState.value = LoadingItemsState + repository.listFolder(path).subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(RemoteFileBrowserItemsObserver()) + } + + inner class RemoteFileBrowserItemsObserver : Observer> { + + var newRemoteFileBrowserItems: List? = null + + override fun onSubscribe(d: Disposable) = Unit + + override fun onNext(response: List) { + newRemoteFileBrowserItems = response + } + + override fun onError(e: Throwable) { + Log.d(TAG, "An error occurred: $e") + } + + override fun onComplete() { + if (newRemoteFileBrowserItems.isNullOrEmpty()) { + this@RemoteFileBrowserItemsViewModel._viewState.value = NoRemoteFileItemsState + } else { + setCurrentState(newRemoteFileBrowserItems!!) + } + } + + private fun setCurrentState(items: List) { + when (this@RemoteFileBrowserItemsViewModel._viewState.value) { + is LoadedState, LoadingItemsState -> { + this@RemoteFileBrowserItemsViewModel._viewState.value = LoadedState(items) + } + else -> return + } + } + } + + companion object { + private val TAG = RemoteFileBrowserItemsViewModel::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java index b51b589e2..de2f682ee 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java @@ -636,6 +636,25 @@ public class DisplayUtils { } } + public static @StringRes + int getSortOrderStringId(FileSortOrderNew sortOrder) { + switch (sortOrder.name) { + case sort_z_to_a_id: + return R.string.menu_item_sort_by_name_z_a; + case sort_new_to_old_id: + return R.string.menu_item_sort_by_date_newest_first; + case sort_old_to_new_id: + return R.string.menu_item_sort_by_date_oldest_first; + case sort_big_to_small_id: + return R.string.menu_item_sort_by_size_biggest_first; + case sort_small_to_big_id: + return R.string.menu_item_sort_by_size_smallest_first; + case sort_a_to_z_id: + default: + return R.string.menu_item_sort_by_name_a_z; + } + } + /** * calculates the relative time string based on the given modification timestamp. * diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDateNew.java b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDateNew.java new file mode 100644 index 000000000..bcd2444e1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDateNew.java @@ -0,0 +1,52 @@ +/* + * Nextcloud Talk application + * + * @author Sven R. Kunze + * @author Andy Scherzinger + * Copyright (C) 2021 Andy Scherzinger + * Copyright (C) 2017 Sven R. Kunze + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.utils; + +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem; + +import java.util.Collections; +import java.util.List; + +/** + * Created by srkunze on 28.08.17. + */ +public class FileSortOrderByDateNew extends FileSortOrderNew { + + FileSortOrderByDateNew(String name, boolean ascending) { + super(name, ascending); + } + + /** + * Sorts list by Date. + * + * @param files list of files to sort + */ + public List sortCloudFiles(List files) { + final int multiplier = isAscending ? 1 : -1; + + Collections.sort(files, (o1, o2) -> + multiplier * Long.compare(o1.getModifiedTimestamp(), o2.getModifiedTimestamp())); + + return super.sortCloudFiles(files); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByNameNew.java b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByNameNew.java new file mode 100644 index 000000000..7d36e6ef4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByNameNew.java @@ -0,0 +1,63 @@ +/* + * Nextcloud Talk application + * + * @author Sven R. Kunze + * @author Andy Scherzinger + * Copyright (C) 2021 Andy Scherzinger + * Copyright (C) 2017 Sven R. Kunze + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.utils; + +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem; + +import java.util.Collections; +import java.util.List; + +import third_parties.daveKoeller.AlphanumComparator; + +/** + * Created by srkunze on 28.08.17. + */ +public class FileSortOrderByNameNew extends FileSortOrderNew { + + FileSortOrderByNameNew(String name, boolean ascending) { + super(name, ascending); + } + + /** + * Sorts list by Name. + * + * @param files files to sort + */ + @SuppressWarnings("Bx") + public List sortCloudFiles(List files) { + final int multiplier = isAscending ? 1 : -1; + + Collections.sort(files, (o1, o2) -> { + if (!o1.isFile() && !o2.isFile()) { + return multiplier * new AlphanumComparator().compare(o1, o2); + } else if (!o1.isFile()) { + return -1; + } else if (!o2.isFile()) { + return 1; + } + return multiplier * new AlphanumComparator().compare(o1, o2); + }); + + return super.sortCloudFiles(files); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySizeNew.java b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySizeNew.java new file mode 100644 index 000000000..fc071c058 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySizeNew.java @@ -0,0 +1,61 @@ +/* + * Nextcloud Talk application + * + * @author Sven R. Kunze + * @author Andy Scherzinger + * Copyright (C) 2021 Andy Scherzinger + * Copyright (C) 2017 Sven R. Kunze + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.utils; + +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem; + +import java.util.Collections; +import java.util.List; + +/** + * Sorts files by sizes + */ +public class FileSortOrderBySizeNew extends FileSortOrderNew { + + FileSortOrderBySizeNew(String name, boolean ascending) { + super(name, ascending); + } + + /** + * Sorts list by Size. + * + * @param files list of files to sort + */ + public List sortCloudFiles(List files) { + final int multiplier = isAscending ? 1 : -1; + + Collections.sort(files, (o1, o2) -> { + if (!o1.isFile() && !o2.isFile()) { + return multiplier * Long.compare(o1.getSize(), o2.getSize()); + } else if (!o1.isFile()) { + return -1; + } else if (!o2.isFile()) { + return 1; + } else { + return multiplier * Long.compare(o1.getSize(), o2.getSize()); + } + }); + + return super.sortCloudFiles(files); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderNew.java b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderNew.java new file mode 100644 index 000000000..876fa581b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderNew.java @@ -0,0 +1,108 @@ +/* + * Nextcloud Talk application + * + * @author Sven R. Kunze + * @author Andy Scherzinger + * Copyright (C) 2021 Andy Scherzinger + * Copyright (C) 2017 Sven R. Kunze + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.utils; + +import android.text.TextUtils; + +import com.nextcloud.talk.components.filebrowser.adapters.items.BrowserFileItem; +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import androidx.annotation.Nullable; + +/** + * Sort order + */ +public class FileSortOrderNew { + public static final String sort_a_to_z_id = "sort_a_to_z"; + public static final String sort_z_to_a_id = "sort_z_to_a"; + public static final String sort_old_to_new_id = "sort_old_to_new"; + public static final String sort_new_to_old_id = "sort_new_to_old"; + public static final String sort_small_to_big_id = "sort_small_to_big"; + public static final String sort_big_to_small_id = "sort_big_to_small"; + + public static final FileSortOrderNew sort_a_to_z = new FileSortOrderByNameNew(sort_a_to_z_id, true); + public static final FileSortOrderNew sort_z_to_a = new FileSortOrderByNameNew(sort_z_to_a_id, false); + public static final FileSortOrderNew sort_old_to_new = new FileSortOrderByDateNew(sort_old_to_new_id, true); + public static final FileSortOrderNew sort_new_to_old = new FileSortOrderByDateNew(sort_new_to_old_id, false); + public static final FileSortOrderNew sort_small_to_big = new FileSortOrderBySizeNew(sort_small_to_big_id, true); + public static final FileSortOrderNew sort_big_to_small = new FileSortOrderBySizeNew(sort_big_to_small_id, false); + + public static final Map sortOrders; + + static { + HashMap temp = new HashMap<>(); + temp.put(sort_a_to_z.name, sort_a_to_z); + temp.put(sort_z_to_a.name, sort_z_to_a); + temp.put(sort_old_to_new.name, sort_old_to_new); + temp.put(sort_new_to_old.name, sort_new_to_old); + temp.put(sort_small_to_big.name, sort_small_to_big); + temp.put(sort_big_to_small.name, sort_big_to_small); + + sortOrders = Collections.unmodifiableMap(temp); + } + + public String name; + public boolean isAscending; + + public FileSortOrderNew(String name, boolean ascending) { + this.name = name; + isAscending = ascending; + } + + public static FileSortOrderNew getFileSortOrder(@Nullable String key) { + if (TextUtils.isEmpty(key) || !sortOrders.containsKey(key)) { + return sort_a_to_z; + } else { + return sortOrders.get(key); + } + } + + public List sortCloudFiles(List files) { + return sortCloudFilesByFavourite(files); + } + + /** + * Sorts list by Favourites. + * + * @param files files to sort + */ + public static List sortCloudFilesByFavourite(List files) { + Collections.sort(files, (o1, o2) -> { + if (o1.isFavorite() && o2.isFavorite()) { + return 0; + } else if (o1.isFavorite()) { + return -1; + } else if (o2.isFavorite()) { + return 1; + } + return 0; + }); + + return files; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt index 02e1b0abe..dad594bdd 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt @@ -74,4 +74,6 @@ object BundleKeys { val KEY_FORWARD_HIDE_SOURCE_ROOM = "KEY_FORWARD_HIDE_SOURCE_ROOM" val KEY_SYSTEM_NOTIFICATION_ID = "KEY_SYSTEM_NOTIFICATION_ID" const val KEY_MESSAGE_ID = "KEY_MESSAGE_ID" + const val KEY_SINGLE_SELECTION = "KEY_SINGLE_SELECTION" + const val KEY_MIME_TYPE_FILTER = "KEY_MIME_TYPE_FILTER" } diff --git a/app/src/main/res/layout/activity_remote_file_browser.xml b/app/src/main/res/layout/activity_remote_file_browser.xml new file mode 100644 index 000000000..52db0abfc --- /dev/null +++ b/app/src/main/res/layout/activity_remote_file_browser.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +