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" /> + + - * 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.components.filebrowser.adapters.items; - -import android.content.Context; -import android.text.format.Formatter; -import android.view.View; -import android.widget.CheckBox; -import android.widget.Toast; - -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.drawee.interfaces.DraweeController; -import com.nextcloud.talk.R; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.components.filebrowser.models.BrowserFile; -import com.nextcloud.talk.databinding.RvItemBrowserFileBinding; -import com.nextcloud.talk.interfaces.SelectionInterface; -import com.nextcloud.talk.models.database.UserEntity; -import com.nextcloud.talk.utils.ApiUtils; -import com.nextcloud.talk.utils.DateUtils; -import com.nextcloud.talk.utils.DisplayUtils; -import com.nextcloud.talk.utils.DrawableUtils; - -import java.util.List; - -import javax.inject.Inject; - -import androidx.appcompat.content.res.AppCompatResources; -import autodagger.AutoInjector; -import eu.davidea.flexibleadapter.FlexibleAdapter; -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; -import eu.davidea.flexibleadapter.items.IFilterable; -import eu.davidea.flexibleadapter.items.IFlexible; -import eu.davidea.viewholders.FlexibleViewHolder; - -@AutoInjector(NextcloudTalkApplication.class) -public class BrowserFileItem extends AbstractFlexibleItem implements IFilterable { - @Inject - Context context; - private final BrowserFile browserFile; - private final UserEntity activeUser; - private final SelectionInterface selectionInterface; - private boolean selected; - - public BrowserFileItem(BrowserFile browserFile, UserEntity activeUser, SelectionInterface selectionInterface) { - this.browserFile = browserFile; - this.activeUser = activeUser; - this.selectionInterface = selectionInterface; - NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); - } - - @Override - public boolean equals(Object o) { - if (o instanceof BrowserFileItem) { - BrowserFileItem inItem = (BrowserFileItem) o; - return browserFile.getPath().equals(inItem.getModel().getPath()); - } - - return false; - } - - public BrowserFile getModel() { - return browserFile; - } - - @Override - public int getLayoutRes() { - return R.layout.rv_item_browser_file; - } - - @Override - public BrowserFileItemViewHolder createViewHolder(View view, FlexibleAdapter adapter) { - return new BrowserFileItemViewHolder(view, adapter); - } - - private boolean isSelected() { - return selected; - } - - private void setSelected(boolean selected) { - this.selected = selected; - } - - @Override - public void bindViewHolder(FlexibleAdapter adapter, - BrowserFileItemViewHolder holder, - int position, - List payloads) { - holder.binding.fileIcon.setController(null); - if (!browserFile.isAllowedToReShare() || browserFile.isEncrypted()) { - holder.itemView.setEnabled(false); - holder.itemView.setAlpha(0.38f); - } else { - holder.itemView.setEnabled(true); - holder.itemView.setAlpha(1.0f); - } - - if (browserFile.isEncrypted()) { - holder.binding.fileEncryptedImageView.setVisibility(View.VISIBLE); - - } else { - holder.binding.fileEncryptedImageView.setVisibility(View.GONE); - } - - if (browserFile.isFavorite()) { - holder.binding.fileFavoriteImageView.setVisibility(View.VISIBLE); - } else { - holder.binding.fileFavoriteImageView.setVisibility(View.GONE); - } - - if (selectionInterface.shouldOnlySelectOneImageFile()) { - if (browserFile.isFile() && browserFile.getMimeType().startsWith("image/")) { - holder.binding.selectFileCheckbox.setVisibility(View.VISIBLE); - } else { - holder.binding.selectFileCheckbox.setVisibility(View.GONE); - } - } else { - holder.binding.selectFileCheckbox.setVisibility(View.VISIBLE); - } - - if (context != null) { - holder - .binding - .fileIcon - .getHierarchy() - .setPlaceholderImage( - AppCompatResources.getDrawable( - context, DrawableUtils.INSTANCE.getDrawableResourceIdForMimeType(browserFile.getMimeType()))); - } - - if (browserFile.getHasPreview()) { - String path = ApiUtils.getUrlForFilePreviewWithRemotePath(activeUser.getBaseUrl(), - browserFile.getPath(), - context.getResources().getDimensionPixelSize(R.dimen.small_item_height)); - - if (path.length() > 0) { - DraweeController draweeController = Fresco.newDraweeControllerBuilder() - .setAutoPlayAnimations(true) - .setImageRequest(DisplayUtils.getImageRequestForUrl(path, null)) - .build(); - holder.binding.fileIcon.setController(draweeController); - } - } - - holder.binding.filenameTextView.setText(browserFile.getDisplayName()); - holder.binding.fileModifiedInfo.setText(String.format(context.getString(R.string.nc_last_modified), - Formatter.formatShortFileSize(context, browserFile.getSize()), - DateUtils.INSTANCE.getLocalDateTimeStringFromTimestamp(browserFile.getModifiedTimestamp()))); - setSelected(selectionInterface.isPathSelected(browserFile.getPath())); - holder.binding.selectFileCheckbox.setChecked(isSelected()); - - if (!browserFile.isEncrypted()) { - holder.binding.selectFileCheckbox.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (!browserFile.isAllowedToReShare()) { - ((CheckBox) v).setChecked(false); - Toast.makeText( - context, - context.getResources().getString(R.string.nc_file_browser_reshare_forbidden), - Toast.LENGTH_LONG) - .show(); - } else if (((CheckBox) v).isChecked() != isSelected()) { - setSelected(((CheckBox) v).isChecked()); - selectionInterface.toggleBrowserItemSelection(browserFile.getPath()); - } - } - }); - } - - holder.binding.filenameTextView.setSelected(true); - holder.binding.fileModifiedInfo.setSelected(true); - } - - @Override - public boolean filter(String constraint) { - return false; - } - - static class BrowserFileItemViewHolder extends FlexibleViewHolder { - - RvItemBrowserFileBinding binding; - - BrowserFileItemViewHolder(View view, FlexibleAdapter adapter) { - super(view, adapter); - binding = RvItemBrowserFileBinding.bind(view); - } - } -} diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.kt b/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.kt deleted file mode 100644 index c32c534fe..000000000 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.kt +++ /dev/null @@ -1,344 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * @author Andy Scherzinger - * Copyright (C) 2021 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.components.filebrowser.controllers - -import android.annotation.SuppressLint -import android.os.Bundle -import android.os.Parcelable -import android.util.Log -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import androidx.fragment.app.DialogFragment -import androidx.recyclerview.widget.RecyclerView -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import autodagger.AutoInjector -import com.nextcloud.talk.R -import com.nextcloud.talk.activities.MainActivity -import com.nextcloud.talk.application.NextcloudTalkApplication -import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication -import com.nextcloud.talk.components.filebrowser.adapters.items.BrowserFileItem -import com.nextcloud.talk.components.filebrowser.interfaces.ListingInterface -import com.nextcloud.talk.components.filebrowser.models.BrowserFile -import com.nextcloud.talk.components.filebrowser.models.DavResponse -import com.nextcloud.talk.components.filebrowser.operations.DavListing -import com.nextcloud.talk.components.filebrowser.operations.ListingAbstractClass -import com.nextcloud.talk.controllers.base.NewBaseController -import com.nextcloud.talk.controllers.util.viewBinding -import com.nextcloud.talk.databinding.ControllerBrowserBinding -import com.nextcloud.talk.interfaces.SelectionInterface -import com.nextcloud.talk.models.database.UserEntity -import com.nextcloud.talk.ui.dialog.SortingOrderDialogFragment -import com.nextcloud.talk.utils.DisplayUtils -import com.nextcloud.talk.utils.FileSortOrder -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BROWSER_TYPE -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY -import com.nextcloud.talk.utils.database.user.UserUtils -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import kotlinx.android.parcel.Parcelize -import net.orange_box.storebox.listeners.OnPreferenceValueChangedListener -import okhttp3.OkHttpClient -import org.parceler.Parcels -import java.io.File -import java.util.ArrayList -import java.util.Collections -import java.util.TreeSet -import javax.inject.Inject - -@AutoInjector(NextcloudTalkApplication::class) -abstract class BrowserController(args: Bundle) : - NewBaseController( - R.layout.controller_browser, - args - ), - ListingInterface, - FlexibleAdapter.OnItemClickListener, - SwipeRefreshLayout.OnRefreshListener, - SelectionInterface { - - private val binding: ControllerBrowserBinding by viewBinding(ControllerBrowserBinding::bind) - - @JvmField - protected val selectedPaths: MutableSet - - @JvmField - @Inject - var userUtils: UserUtils? = null - - @JvmField - @Inject - var okHttpClient: OkHttpClient? = null - - @JvmField - protected var activeUser: UserEntity - - private var filesSelectionDoneMenuItem: MenuItem? = null - private var layoutManager: RecyclerView.LayoutManager? = null - private var adapter: FlexibleAdapter? = null - private var recyclerViewItems: List = ArrayList() - private var listingAbstractClass: ListingAbstractClass? = null - private val browserType: BrowserType - private var currentPath: String - - private var sortingChangeListener: OnPreferenceValueChangedListener? = null - - override fun onViewBound(view: View) { - super.onViewBound(view) - if (adapter == null) { - adapter = FlexibleAdapter(recyclerViewItems, context, false) - } - - appPreferences!!.registerSortingChangeListener( - SortingChangeListener(this).also { - sortingChangeListener = it - } - ) - - changeEnabledStatusForBarItems(true) - prepareViews() - } - - abstract fun onFileSelectionDone() - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.menu_share_files, menu) - filesSelectionDoneMenuItem = menu.findItem(R.id.files_selection_done) - filesSelectionDoneMenuItem?.isVisible = selectedPaths.size > 0 - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == R.id.files_selection_done) { - onFileSelectionDone() - return true - } - return super.onOptionsItemSelected(item) - } - - override fun onAttach(view: View) { - super.onAttach(view) - - binding.pathNavigation.menu.findItem(R.id.action_back)?.setOnMenuItemClickListener { goBack() } - binding.sortButton.setOnClickListener { changeSorting() } - - binding.sortButton.setText( - DisplayUtils.getSortOrderStringId(FileSortOrder.getFileSortOrder(appPreferences?.sorting)) - ) - - refreshCurrentPath() - } - - override fun onRefresh() { - refreshCurrentPath() - } - - fun changeSorting() { - val newFragment: DialogFragment = SortingOrderDialogFragment - .newInstance(FileSortOrder.getFileSortOrder(appPreferences?.sorting)) - newFragment.show( - (activity as MainActivity?)!!.supportFragmentManager, - SortingOrderDialogFragment.SORTING_ORDER_FRAGMENT - ) - } - - public override fun onDestroy() { - super.onDestroy() - listingAbstractClass!!.tearDown() - } - - override val title: String - get() = - currentPath - - fun goBack(): Boolean { - fetchPath(File(currentPath).parent) - return true - } - - fun refreshCurrentPath(): Boolean { - fetchPath(currentPath) - return true - } - - @SuppressLint("RestrictedApi") - private fun changeEnabledStatusForBarItems(shouldBeEnabled: Boolean) { - binding.pathNavigation.menu.findItem(R.id.action_back)?.isEnabled = shouldBeEnabled && currentPath != "/" - } - - private fun fetchPath(path: String) { - listingAbstractClass!!.cancelAllJobs() - changeEnabledStatusForBarItems(false) - listingAbstractClass!!.getFiles( - path, - activeUser, - if (BrowserType.DAV_BROWSER == browserType) okHttpClient else null - ) - } - - override fun listingResult(davResponse: DavResponse) { - recyclerViewItems = ArrayList() - if (davResponse.getData() != null) { - val objectList = davResponse.getData() as List - currentPath = objectList[0].path!! - if (activity != null) { - activity!!.runOnUiThread { setTitle() } - } - for (i in 1 until objectList.size) { - (recyclerViewItems as ArrayList).add(BrowserFileItem(objectList[i], activeUser, this)) - } - } - - FileSortOrder.getFileSortOrder(appPreferences?.sorting).sortCloudFiles(recyclerViewItems) - - if (activity != null) { - activity!!.runOnUiThread { - adapter!!.clear() - adapter!!.addItems(0, recyclerViewItems) - adapter!!.notifyDataSetChanged() - changeEnabledStatusForBarItems(true) - } - } - - binding.swipeRefreshList.isRefreshing = false - } - - private fun shouldPathBeSelectedDueToParent(currentPath: String): Boolean { - if (selectedPaths.size > 0) { - var file = File(currentPath) - if (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!!) - } - if (activity != null) { - activity!!.runOnUiThread { - adapter!!.notifyDataSetChanged() - } - } - } - - override fun onItemClick(view: View, position: Int): Boolean { - val browserFile = (adapter!!.getItem(position) as BrowserFileItem).model - if ("inode/directory" == browserFile.mimeType) { - fetchPath(browserFile.path!!) - return true - } - return false - } - - private fun prepareViews() { - if (activity != null) { - layoutManager = SmoothScrollLinearLayoutManager(activity) - binding.recyclerView.layoutManager = layoutManager - binding.recyclerView.setHasFixedSize(true) - binding.recyclerView.adapter = adapter - adapter!!.addListener(this) - - binding.swipeRefreshList.setOnRefreshListener(this) - binding.swipeRefreshList.setColorSchemeResources(R.color.colorPrimary) - binding.swipeRefreshList.setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background) - } - } - - @SuppressLint("RestrictedApi") - override fun toggleBrowserItemSelection(path: String) { - if (selectedPaths.contains(path) || shouldPathBeSelectedDueToParent(path)) { - checkAndRemoveAnySelectedParents(path) - } else { - // TOOD: 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) - } - - abstract override fun shouldOnlySelectOneImageFile(): Boolean - - @Parcelize - enum class BrowserType : Parcelable { - FILE_BROWSER, DAV_BROWSER - } - - init { - setHasOptionsMenu(true) - sharedApplication!!.componentApplication.inject(this) - browserType = Parcels.unwrap(args.getParcelable(KEY_BROWSER_TYPE)) - activeUser = Parcels.unwrap(args.getParcelable(KEY_USER_ENTITY)) - currentPath = "/" - if (BrowserType.DAV_BROWSER == browserType) { - listingAbstractClass = DavListing(this) - } // else { - // listingAbstractClass = new LocalListing(this); - // } - selectedPaths = Collections.synchronizedSet(TreeSet()) - } - - @Suppress("Detekt.TooGenericExceptionCaught") - private class SortingChangeListener(private val browserController: BrowserController) : - OnPreferenceValueChangedListener { - override fun onChanged(newValue: String) { - try { - val sortOrder = FileSortOrder.getFileSortOrder(newValue) - - browserController.binding.sortButton.setText(DisplayUtils.getSortOrderStringId(sortOrder)) - browserController.recyclerViewItems = sortOrder.sortCloudFiles(browserController.recyclerViewItems) - - if (browserController.activity != null) { - browserController.activity!!.runOnUiThread { - browserController.adapter!!.updateDataSet(browserController.recyclerViewItems) - browserController.changeEnabledStatusForBarItems(true) - } - } - } catch (npe: NullPointerException) { - // view binding can be null - // since this is called asynchronously and UI might have been destroyed in the meantime - Log.i(BrowserController.TAG, "UI destroyed - view binding already gone") - } - } - } - - companion object { - private const val TAG = "BrowserController" - } -} diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserForAvatarController.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserForAvatarController.java deleted file mode 100644 index 9075d3788..000000000 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserForAvatarController.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Tobias Kaminsky - * Copyright (C) 2021 Tobias Kaminsky - * - * 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.controllers; - -import android.content.Intent; -import android.os.Bundle; - -import com.nextcloud.talk.controllers.ProfileController; - -import androidx.annotation.Nullable; - -public class BrowserForAvatarController extends BrowserController { - private ProfileController controller; - - public BrowserForAvatarController(Bundle args) { - super(args); - } - - public BrowserForAvatarController(Bundle args, ProfileController controller) { - super(args); - - this.controller = controller; - } - - @Override - public void onFileSelectionDone() { - controller.handleAvatar(selectedPaths.iterator().next()); - - getRouter().popCurrentController(); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - } - - @Override - public boolean shouldOnlySelectOneImageFile() { - return true; - } -} diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserForSharingController.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserForSharingController.java deleted file mode 100644 index f0b0e640a..000000000 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserForSharingController.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Tobias Kaminsky - * Copyright (C) 2021 Tobias Kaminsky - * - * 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.controllers; - -import android.os.Bundle; - -import com.nextcloud.talk.jobs.ShareOperationWorker; -import com.nextcloud.talk.utils.bundle.BundleKeys; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import androidx.work.Data; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; - -public class BrowserForSharingController extends BrowserController { - private final String roomToken; - - public BrowserForSharingController(Bundle args) { - super(args); - - roomToken = args.getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN()); - } - - @Override - public void onFileSelectionDone() { - synchronized (selectedPaths) { - Iterator iterator = selectedPaths.iterator(); - - List paths = new ArrayList<>(); - Data data; - OneTimeWorkRequest shareWorker; - - while (iterator.hasNext()) { - String path = iterator.next(); - paths.add(path); - iterator.remove(); - if (paths.size() == 10 || !iterator.hasNext()) { - data = new Data.Builder() - .putLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), activeUser.getId()) - .putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), roomToken) - .putStringArray(BundleKeys.INSTANCE.getKEY_FILE_PATHS(), paths.toArray(new String[0])) - .build(); - shareWorker = new OneTimeWorkRequest.Builder(ShareOperationWorker.class) - .setInputData(data) - .build(); - WorkManager.getInstance().enqueue(shareWorker); - paths = new ArrayList<>(); - } - } - } - - getRouter().popCurrentController(); - } - - @Override - public boolean shouldOnlySelectOneImageFile() { - return false; - } -} 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 deleted file mode 100644 index dc69a1422..000000000 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * @author Andy Scherzinger - * Copyright (C) 2022 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.components.filebrowser.operations; - -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.models.database.UserEntity; - -import java.util.concurrent.Callable; - -import androidx.annotation.Nullable; -import io.reactivex.Single; -import io.reactivex.SingleObserver; -import io.reactivex.annotations.NonNull; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import okhttp3.OkHttpClient; - -public class DavListing extends ListingAbstractClass { - private static final String TAG = DavListing.class.getSimpleName(); - - private DavResponse davResponse = new DavResponse(); - - public DavListing(ListingInterface listingInterface) { - super(listingInterface); - } - - @Override - public void getFiles(String path, UserEntity currentUser, @Nullable OkHttpClient okHttpClient) { - Single.fromCallable(new Callable() { - @Override - public ReadFilesystemOperation call() { - return new ReadFilesystemOperation(okHttpClient, currentUser, path, 1); - } - }).subscribeOn(Schedulers.io()) - .subscribe(new SingleObserver() { - @Override - public void onSubscribe(@NonNull Disposable d) { - - } - - @Override - public void onSuccess(@NonNull ReadFilesystemOperation readFilesystemOperation) { - davResponse = readFilesystemOperation.readRemotePath(); - try { - listingInterface.listingResult(davResponse); - } catch (NullPointerException npe) { - Log.i(TAG, "Error loading remote folder - due to view already been terminated", npe); - } - } - - @Override - public void onError(@NonNull Throwable e) { - listingInterface.listingResult(davResponse); - } - }); - } -} diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/ListingAbstractClass.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/ListingAbstractClass.java deleted file mode 100644 index b01bc31cc..000000000 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/ListingAbstractClass.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * 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.components.filebrowser.operations; - -import android.os.Handler; -import androidx.annotation.Nullable; -import com.nextcloud.talk.components.filebrowser.interfaces.ListingInterface; -import com.nextcloud.talk.models.database.UserEntity; -import okhttp3.OkHttpClient; - -public abstract class ListingAbstractClass { - Handler handler; - ListingInterface listingInterface; - - ListingAbstractClass(ListingInterface listingInterface) { - handler = new Handler(); - this.listingInterface = listingInterface; - } - - public abstract void getFiles(String path, UserEntity currentUser, @Nullable OkHttpClient okHttpClient); - - public void cancelAllJobs() { - handler.removeCallbacksAndMessages(null); - } - - public void tearDown() { - cancelAllJobs(); - handler = null; - } -} 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/ReadFilesystemOperation.java index ed8e978cf..09c1fc2b7 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/ReadFilesystemOperation.java @@ -91,7 +91,7 @@ public class ReadFilesystemOperation { } }); } catch (IOException | DavException e) { - Log.w("", "Error reading remote path"); + Log.w(TAG, "Error reading remote path"); } remoteFiles.add(BrowserFile.Companion.getModelFromResponse(rootElement[0], diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFolderListingOperation.kt b/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFolderListingOperation.kt new file mode 100644 index 000000000..b14035ca2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFolderListingOperation.kt @@ -0,0 +1,179 @@ +/* + * 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 ReadFolderListingOperation(okHttpClient: OkHttpClient, currentUser: UserEntity, path: String, depth: Int) { + private val okHttpClient: OkHttpClient + private val url: String + private val depth: Int + private val basePath: String + + 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 + } + + 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") + } + for (memberElement in memberElements) { + remoteFiles.add( + getModelFromResponse( + memberElement, + memberElement + .href + .toString() + .substring(basePath.length) + ) + ) + } + davResponse.setData(remoteFiles) + return davResponse + } + + 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(READ_PERMISSION) + ) { + 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 + } + } + } + + companion object { + private const val TAG = "ReadFilesystemOperation" + private const val READ_PERMISSION = "R" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index 15fbbe98f..9c719f32e 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt @@ -91,7 +91,6 @@ import autodagger.AutoInjector import coil.load import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler -import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler import com.facebook.common.executors.UiThreadImmediateExecutorService import com.facebook.common.references.CloseableReference import com.facebook.datasource.DataSource @@ -121,14 +120,13 @@ import com.nextcloud.talk.adapters.messages.VoiceMessageInterface import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.callbacks.MentionAutocompleteCallback -import com.nextcloud.talk.components.filebrowser.controllers.BrowserController -import com.nextcloud.talk.components.filebrowser.controllers.BrowserForSharingController import com.nextcloud.talk.controllers.base.NewBaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.databinding.ControllerChatBinding import com.nextcloud.talk.events.UserMentionClickEvent import com.nextcloud.talk.events.WebSocketCommunicationEvent import com.nextcloud.talk.jobs.DownloadFileToCacheWorker +import com.nextcloud.talk.jobs.ShareOperationWorker import com.nextcloud.talk.jobs.UploadAndShareFilesWorker import com.nextcloud.talk.messagesearch.MessageSearchActivity import com.nextcloud.talk.models.database.CapabilitiesUtil @@ -143,6 +141,7 @@ import com.nextcloud.talk.models.json.conversations.RoomsOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.mention.Mention import com.nextcloud.talk.presenters.MentionAutocompletePresenter +import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity import com.nextcloud.talk.shareditems.activities.SharedItemsActivity import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet import com.nextcloud.talk.ui.dialog.AttachmentDialog @@ -164,6 +163,8 @@ import com.nextcloud.talk.utils.UriUtils import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY @@ -1352,6 +1353,24 @@ class ChatController(args: Bundle) : } when (requestCode) { + REQUEST_CODE_SELECT_REMOTE_FILES -> { + val pathList = intent?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS) + if (pathList?.size!! >= 1) { + pathList + .chunked(10) + .forEach { paths -> + val data = Data.Builder() + .putLong(KEY_INTERNAL_USER_ID, conversationUser!!.id) + .putString(KEY_ROOM_TOKEN, roomToken) + .putStringArray(KEY_FILE_PATHS, paths.toTypedArray()) + .build() + val worker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java) + .setInputData(data) + .build() + WorkManager.getInstance().enqueue(worker) + } + } + } REQUEST_CODE_CHOOSE_FILE -> { try { checkNotNull(intent) @@ -1606,16 +1625,9 @@ class ChatController(args: Bundle) : requestReadContacts() } - fun showBrowserScreen(browserType: BrowserController.BrowserType) { - val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap(browserType)) - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap(conversationUser)) - bundle.putString(KEY_ROOM_TOKEN, roomToken) - router.pushController( - RouterTransaction.with(BrowserForSharingController(bundle)) - .pushChangeHandler(VerticalChangeHandler()) - .popChangeHandler(VerticalChangeHandler()) - ) + fun showBrowserScreen() { + val sharingFileBrowserIntent = Intent(activity, RemoteFileBrowserActivity::class.java) + startActivityForResult(sharingFileBrowserIntent, REQUEST_CODE_SELECT_REMOTE_FILES) } fun showShareLocationScreen() { @@ -3142,6 +3154,7 @@ class ChatController(args: Bundle) : private const val REQUEST_READ_CONTACT_PERMISSION = 234 private const val REQUEST_CAMERA_PERMISSION = 223 private const val REQUEST_CODE_PICK_CAMERA: Int = 333 + private const val REQUEST_CODE_SELECT_REMOTE_FILES = 888 private const val OBJECT_MESSAGE: String = "{object}" private const val MINIMUM_VOICE_RECORD_DURATION: Int = 1000 private const val VOICE_RECORD_CANCEL_SLIDER_X: Int = -50 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..fa604250e 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 @@ -58,8 +55,6 @@ import com.nextcloud.talk.R 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,12 +66,12 @@ 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.bundle.BundleKeys.KEY_BROWSER_TYPE -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY +import com.nextcloud.talk.utils.FileUtils +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MIME_TYPE_FILTER import com.nextcloud.talk.utils.database.user.UserUtils import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers @@ -86,7 +81,6 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody import okhttp3.ResponseBody -import org.parceler.Parcels import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -199,7 +193,7 @@ class ProfileController : NewBaseController(R.layout.controller_profile) { currentUser = userUtils.currentUser val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token) binding.avatarUpload.setOnClickListener { sendSelectLocalFileIntent() } - binding.avatarChoose.setOnClickListener { showBrowserScreen(BrowserType.DAV_BROWSER) } + binding.avatarChoose.setOnClickListener { showBrowserScreen() } binding.avatarDelete.setOnClickListener { ncApi.deleteAvatar( credentials, @@ -486,22 +480,14 @@ class ProfileController : NewBaseController(R.layout.controller_profile) { startActivityForResult(intent, 1) } - private fun showBrowserScreen(browserType: BrowserType) { + private fun showBrowserScreen() { val bundle = Bundle() - bundle.putParcelable( - KEY_BROWSER_TYPE, - Parcels.wrap(BrowserType::class.java, browserType) - ) - bundle.putParcelable( - KEY_USER_ENTITY, - Parcels.wrap(UserEntity::class.java, currentUser) - ) - bundle.putString(KEY_ROOM_TOKEN, "123") - router.pushController( - RouterTransaction.with(BrowserForAvatarController(bundle, this)) - .pushChangeHandler(VerticalChangeHandler()) - .popChangeHandler(VerticalChangeHandler()) - ) + bundle.putString(KEY_MIME_TYPE_FILTER, "image/") + + val avatarIntent = Intent(activity, RemoteFileBrowserActivity::class.java) + avatarIntent.putExtras(bundle) + + startActivityForResult(avatarIntent, REQUEST_CODE_SELECT_REMOTE_FILES) } fun handleAvatar(remotePath: String?) { @@ -526,9 +512,13 @@ 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) + FileUtils.removeTempCacheFile( + this.context!!, + AVATAR_PATH + ) + file = FileUtils.getTempCacheFile( + this.context!!, + AVATAR_PATH ) try { FileOutputStream(file).use { out -> bitmap.compress(Bitmap.CompressFormat.PNG, FULL_QUALITY, out) } @@ -553,13 +543,22 @@ class ProfileController : NewBaseController(R.layout.controller_profile) { startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER) } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { if (resultCode == Activity.RESULT_OK) { - uploadAvatar(getFile(data)) + if (requestCode == REQUEST_CODE_IMAGE_PICKER) { + uploadAvatar(getFile(intent)) + } else if (requestCode == REQUEST_CODE_SELECT_REMOTE_FILES) { + val pathList = intent?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS) + if (pathList?.size!! >= 1) { + handleAvatar(pathList[0]) + } + } else { + Log.w(TAG, "Unknown intent request code") + } } else if (resultCode == ImagePicker.RESULT_ERROR) { - Toast.makeText(activity, getError(data), Toast.LENGTH_SHORT).show() + Toast.makeText(activity, getError(intent), Toast.LENGTH_SHORT).show() } else { - Toast.makeText(activity, "Task Cancelled", Toast.LENGTH_SHORT).show() + Log.i(TAG, "Task Cancelled") } } @@ -809,6 +808,8 @@ class ProfileController : NewBaseController(R.layout.controller_profile) { companion object { private const val TAG: String = "ProfileController" + private const val AVATAR_PATH = "photos/avatar.png" + private const val REQUEST_CODE_SELECT_REMOTE_FILES = 22 private const val DEFAULT_CACHE_SIZE: Int = 20 private const val DEFAULT_RETRIES: Long = 3 private const val MAX_SIZE: Int = 1024 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/interfaces/SelectionInterface.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/SelectionInterface.kt similarity index 85% rename from app/src/main/java/com/nextcloud/talk/interfaces/SelectionInterface.kt rename to app/src/main/java/com/nextcloud/talk/remotefilebrowser/SelectionInterface.kt index 23fda533b..0ddfe8ffd 100644 --- a/app/src/main/java/com/nextcloud/talk/interfaces/SelectionInterface.kt +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/SelectionInterface.kt @@ -2,6 +2,8 @@ * Nextcloud Talk application * * @author Mario Danic + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger * Copyright (C) 2017-2018 Mario Danic * * This program is free software: you can redistribute it and/or modify @@ -18,12 +20,8 @@ * along with this program. If not, see . */ -package com.nextcloud.talk.interfaces +package com.nextcloud.talk.remotefilebrowser interface SelectionInterface { - fun toggleBrowserItemSelection(path: String) - fun isPathSelected(path: String): Boolean - - fun shouldOnlySelectOneImageFile(): Boolean } 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..3e3caf29e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt @@ -0,0 +1,260 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * @author Álvaro Brey + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2022 Álvaro Brey + * + * 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.remotefilebrowser.SelectionInterface +import com.nextcloud.talk.remotefilebrowser.adapters.RemoteFileBrowserItemsAdapter +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.bundle.BundleKeys.KEY_MIME_TYPE_FILTER +import com.nextcloud.talk.utils.database.user.CurrentUserProvider +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class RemoteFileBrowserActivity : AppCompatActivity(), SelectionInterface, SwipeRefreshLayout.OnRefreshListener { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var currentUserProvider: CurrentUserProvider + + private lateinit var binding: ActivityRemoteFileBrowserBinding + private lateinit var viewModel: RemoteFileBrowserItemsViewModel + + private var filesSelectionDoneMenuItem: MenuItem? = 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?.setDisplayHomeAsUpEnabled(true) + + val extras = intent.extras + val mimeTypeSelectionFilter = extras?.getString(KEY_MIME_TYPE_FILTER, null) + + initViewModel(mimeTypeSelectionFilter) + + binding.swipeRefreshList.setOnRefreshListener(this) + binding.swipeRefreshList.setColorSchemeResources(R.color.colorPrimary) + binding.swipeRefreshList.setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background) + + binding.pathNavigationBackButton.setOnClickListener { viewModel.navigateUp() } + binding.sortButton.setOnClickListener { changeSorting() } + + viewModel.loadItems() + } + + private fun initViewModel(mimeTypeSelectionFilter: String?) { + 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 -> { + loadList(state, mimeTypeSelectionFilter) + } + is RemoteFileBrowserItemsViewModel.FinishState -> { + finishWithResult(state.selectedPaths) + } + } + } + + viewModel.fileSortOrder.observe(this) { sortOrder -> + if (sortOrder != null) { + binding.sortButton.setText(DisplayUtils.getSortOrderStringId(sortOrder)) + } + } + + viewModel.currentPath.observe(this) { path -> + if (path != null) { + supportActionBar?.title = path + } + } + + viewModel.selectedPaths.observe(this) { selectedPaths -> + filesSelectionDoneMenuItem?.isVisible = !selectedPaths.isNullOrEmpty() + binding.recyclerView.adapter?.notifyDataSetChanged() + } + } + + private fun loadList( + state: RemoteFileBrowserItemsViewModel.LoadedState, + mimeTypeSelectionFilter: String? + ) { + val remoteFileBrowserItems = state.items + Log.d(TAG, "Items received: $remoteFileBrowserItems") + + // TODO make showGrid based on preferences (when available) + val showGrid = false + val layoutManager = if (showGrid) { + GridLayoutManager(this, SPAN_COUNT) + } else { + LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + } + + // TODO do not needlessly recreate adapter if it can be reused + val adapter = RemoteFileBrowserItemsAdapter( + showGrid = showGrid, + mimeTypeSelectionFilter = mimeTypeSelectionFilter, + userEntity = currentUserProvider.currentUser!!, + selectionInterface = this, + onItemClicked = viewModel::onItemClicked + ) + adapter.items = remoteFileBrowserItems + + 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) + return true + } + + override fun onBackPressed() { + setResult(Activity.RESULT_CANCELED) + super.onBackPressed() + } + + override fun onResume() { + super.onResume() + refreshCurrentPath() + } + + private fun changeSorting() { + val newFragment: DialogFragment = SortingOrderDialogFragment + .newInstance(FileSortOrder.getFileSortOrder(viewModel.fileSortOrder.value!!.name)) + newFragment.show( + supportFragmentManager, + SortingOrderDialogFragment.SORTING_ORDER_FRAGMENT + ) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + onBackPressed() + true + } + R.id.files_selection_done -> { + viewModel.onSelectionDone() + true + } + else -> { + return super.onOptionsItemSelected(item) + } + } + } + + private fun finishWithResult(selectedPaths: Set) { + 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() + } + + override fun isPathSelected(path: String): Boolean { + return viewModel.isPathSelected(path) + } + + companion object { + private val TAG = RemoteFileBrowserActivity::class.simpleName + const val SPAN_COUNT: Int = 4 + const val EXTRA_SELECTED_PATHS = "EXTRA_SELECTED_PATH" + } +} 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..2d6020394 --- /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.models.database.UserEntity +import com.nextcloud.talk.remotefilebrowser.SelectionInterface +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..64f21207c --- /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) 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.text.format.Formatter +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.models.database.UserEntity +import com.nextcloud.talk.remotefilebrowser.SelectionInterface +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) && + (item.isAllowedToReShare && !item.isEncrypted) + } + + 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..71f343eb9 --- /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.models.database.UserEntity +import com.nextcloud.talk.remotefilebrowser.SelectionInterface +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..e4eb2c044 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/model/RemoteFileBrowserItem.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * @author Mario Danic + * Copyright (C) 2022 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 kotlinx.android.parcel.Parcelize + +@Parcelize +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 diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/interfaces/ListingInterface.java b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepository.kt similarity index 64% rename from app/src/main/java/com/nextcloud/talk/components/filebrowser/interfaces/ListingInterface.java rename to app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepository.kt index c953d8a59..6f2239d83 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/interfaces/ListingInterface.java +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepository.kt @@ -1,8 +1,8 @@ /* * Nextcloud Talk application * - * @author Mario Danic - * Copyright (C) 2017-2018 Mario Danic + * @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 @@ -18,10 +18,12 @@ * along with this program. If not, see . */ -package com.nextcloud.talk.components.filebrowser.interfaces; +package com.nextcloud.talk.remotefilebrowser.repositories -import com.nextcloud.talk.components.filebrowser.models.DavResponse; +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import io.reactivex.Observable -public interface ListingInterface { - void listingResult(DavResponse davResponse); +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..33b89c244 --- /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) 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.repositories + +import com.nextcloud.talk.components.filebrowser.webdav.ReadFolderListingOperation +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 = + ReadFolderListingOperation( + 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..3c0bb4cbe --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt @@ -0,0 +1,230 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * @author Álvaro Brey + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2022 Álvaro Brey + * + * 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 com.nextcloud.talk.utils.FileSortOrder +import com.nextcloud.talk.utils.preferences.AppPreferences +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import net.orange_box.storebox.listeners.OnPreferenceValueChangedListener +import java.io.File +import javax.inject.Inject + +/** + * @startuml + * hide empty description + * [*] --> InitialState + * InitialState --> LoadingItemsState + * LoadingItemsState --> NoRemoteFileItemsState + * NoRemoteFileItemsState --> LoadingItemsState + * LoadingItemsState --> LoadedState + * LoadedState --> LoadingItemsState + * LoadedState --> FinishState + * FinishState --> [*] + * @enduml + */ +class RemoteFileBrowserItemsViewModel @Inject constructor( + private val repository: RemoteFileBrowserItemsRepository, + private val appPreferences: AppPreferences +) : + ViewModel() { + + sealed interface ViewState + object InitialState : ViewState + object NoRemoteFileItemsState : ViewState + object LoadingItemsState : ViewState + class LoadedState(val items: List) : ViewState + class FinishState(val selectedPaths: Set) : ViewState + + private val initialSortOrder = FileSortOrder.getFileSortOrder(appPreferences.sorting) + private val sortingPrefListener: SortChangeListener = SortChangeListener() + + private val _viewState: MutableLiveData = MutableLiveData(InitialState) + val viewState: LiveData + get() = _viewState + + // TODO incorporate into view state object? + private val _fileSortOrder: MutableLiveData = MutableLiveData(initialSortOrder) + val fileSortOrder: LiveData + get() = _fileSortOrder + + private val _currentPath: MutableLiveData = MutableLiveData(ROOT_PATH) + val currentPath: LiveData + get() = _currentPath + + private val _selectedPaths: MutableLiveData> = MutableLiveData(emptySet()) + val selectedPaths: LiveData> + get() = _selectedPaths + + init { + appPreferences.registerSortingChangeListener(sortingPrefListener) + } + + inner class SortChangeListener : OnPreferenceValueChangedListener { + override fun onChanged(newValue: String) { + onSelectSortOrder(newValue) + } + } + + override fun onCleared() { + super.onCleared() + appPreferences.unregisterSortingChangeListener(sortingPrefListener) + } + + fun loadItems() { + _viewState.value = LoadingItemsState + repository.listFolder(currentPath.value!!).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 = fileSortOrder.value!!.sortCloudFiles(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 + } + } + } + + private fun onSelectSortOrder(newSortOrderString: String) { + val newSortOrder = FileSortOrder.getFileSortOrder(newSortOrderString) + if (newSortOrder.name != fileSortOrder.value?.name) { + _fileSortOrder.value = newSortOrder + val currentState = viewState.value + if (currentState is LoadedState) { + val sortedItems = newSortOrder.sortCloudFiles(currentState.items) + _viewState.value = LoadedState(sortedItems) + } + } + } + + private fun changePath(path: String) { + _currentPath.value = path + loadItems() + } + + fun navigateUp() { + val path = _currentPath.value + if (path!! != ROOT_PATH) { + _currentPath.value = File(path).parent!! + loadItems() + } + } + + fun onSelectionDone() { + val selection = selectedPaths.value + if (!selection.isNullOrEmpty()) { + _viewState.value = FinishState(selection) + } + } + + fun onItemClicked(remoteFileBrowserItem: RemoteFileBrowserItem) { + if (remoteFileBrowserItem.mimeType == MIME_DIRECTORY) { + changePath(remoteFileBrowserItem.path!!) + } else { + toggleBrowserItemSelection(remoteFileBrowserItem.path!!) + } + } + + private fun toggleBrowserItemSelection(path: String) { + val paths = selectedPaths.value!!.toMutableSet() + if (paths.contains(path) || shouldPathBeSelectedDueToParent(path)) { + checkAndRemoveAnySelectedParents(path) + } else { + // TODO if it's a folder, remove all the children we added manually + paths.add(path) + _selectedPaths.value = paths + } + } + + private fun checkAndRemoveAnySelectedParents(currentPath: String) { + var file = File(currentPath) + val paths = selectedPaths.value!!.toMutableSet() + paths.remove(currentPath) + while (file.parent != null) { + paths.remove(file.parent!! + File.pathSeparator) + file = File(file.parent!!) + } + _selectedPaths.value = paths + } + + private fun shouldPathBeSelectedDueToParent(currentPath: String): Boolean { + var file = File(currentPath) + val paths = selectedPaths.value!! + if (paths.isNotEmpty() && file.parent != ROOT_PATH) { + while (file.parent != null) { + var parent = file.parent!! + if (File(file.parent!!).parent != null) { + parent += File.pathSeparator + } + if (paths.contains(parent)) { + return true + } + file = File(file.parent!!) + } + } + return false + } + + fun isPathSelected(path: String): Boolean { + return selectedPaths.value?.contains(path) == true || shouldPathBeSelectedDueToParent(path) + } + + companion object { + private val TAG = RemoteFileBrowserItemsViewModel::class.simpleName + private const val ROOT_PATH = "/" + private const val MIME_DIRECTORY = "inode/directory" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt index 77c5acbfb..c88db9ca6 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt @@ -29,7 +29,6 @@ import android.view.ViewGroup import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.nextcloud.talk.R -import com.nextcloud.talk.components.filebrowser.controllers.BrowserController import com.nextcloud.talk.controllers.ChatController import com.nextcloud.talk.databinding.DialogAttachmentBinding import com.nextcloud.talk.models.database.CapabilitiesUtil @@ -76,7 +75,7 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle } dialogAttachmentBinding.menuAttachFileFromCloud.setOnClickListener { - chatController.showBrowserScreen(BrowserController.BrowserType.DAV_BROWSER) + chatController.showBrowserScreen() dismiss() } diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/SortingOrderDialogFragment.java b/app/src/main/java/com/nextcloud/talk/ui/dialog/SortingOrderDialogFragment.java index 715abb9a6..f2d5c8d66 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/SortingOrderDialogFragment.java +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/SortingOrderDialogFragment.java @@ -39,6 +39,8 @@ import com.nextcloud.talk.databinding.SortingOrderFragmentBinding; import com.nextcloud.talk.utils.FileSortOrder; import com.nextcloud.talk.utils.preferences.AppPreferences; +import org.jetbrains.annotations.NotNull; + import javax.inject.Inject; import androidx.annotation.NonNull; @@ -67,11 +69,11 @@ public class SortingOrderDialogFragment extends DialogFragment implements View.O private View[] taggedViews; private String currentSortOrderName; - public static SortingOrderDialogFragment newInstance(FileSortOrder sortOrder) { + public static SortingOrderDialogFragment newInstance(@NotNull FileSortOrder sortOrder) { SortingOrderDialogFragment dialogFragment = new SortingOrderDialogFragment(); Bundle args = new Bundle(); - args.putString(KEY_SORT_ORDER, sortOrder.name); + args.putString(KEY_SORT_ORDER, sortOrder.getName()); dialogFragment.setArguments(args); return dialogFragment; @@ -84,7 +86,8 @@ public class SortingOrderDialogFragment extends DialogFragment implements View.O setRetainInstance(true); if (getArguments() != null) { - currentSortOrderName = getArguments().getString(KEY_SORT_ORDER, FileSortOrder.sort_a_to_z.name); + currentSortOrderName = getArguments().getString(KEY_SORT_ORDER, + FileSortOrder.Companion.getSort_a_to_z().getName()); } } @@ -120,29 +123,29 @@ public class SortingOrderDialogFragment extends DialogFragment implements View.O taggedViews = new View[12]; taggedViews[0] = binding.sortByNameAscending; - taggedViews[0].setTag(FileSortOrder.sort_a_to_z); + taggedViews[0].setTag(FileSortOrder.Companion.getSort_a_to_z()); taggedViews[1] = binding.sortByNameAZText; - taggedViews[1].setTag(FileSortOrder.sort_a_to_z); + taggedViews[1].setTag(FileSortOrder.Companion.getSort_a_to_z()); taggedViews[2] = binding.sortByNameDescending; - taggedViews[2].setTag(FileSortOrder.sort_z_to_a); + taggedViews[2].setTag(FileSortOrder.Companion.getSort_z_to_a()); taggedViews[3] = binding.sortByNameZAText; - taggedViews[3].setTag(FileSortOrder.sort_z_to_a); + taggedViews[3].setTag(FileSortOrder.Companion.getSort_z_to_a()); taggedViews[4] = binding.sortByModificationDateAscending; - taggedViews[4].setTag(FileSortOrder.sort_old_to_new); + taggedViews[4].setTag(FileSortOrder.Companion.getSort_old_to_new()); taggedViews[5] = binding.sortByModificationDateOldestFirstText; - taggedViews[5].setTag(FileSortOrder.sort_old_to_new); + taggedViews[5].setTag(FileSortOrder.Companion.getSort_old_to_new()); taggedViews[6] = binding.sortByModificationDateDescending; - taggedViews[6].setTag(FileSortOrder.sort_new_to_old); + taggedViews[6].setTag(FileSortOrder.Companion.getSort_new_to_old()); taggedViews[7] = binding.sortByModificationDateNewestFirstText; - taggedViews[7].setTag(FileSortOrder.sort_new_to_old); + taggedViews[7].setTag(FileSortOrder.Companion.getSort_new_to_old()); taggedViews[8] = binding.sortBySizeAscending; - taggedViews[8].setTag(FileSortOrder.sort_small_to_big); + taggedViews[8].setTag(FileSortOrder.Companion.getSort_small_to_big()); taggedViews[9] = binding.sortBySizeSmallestFirstText; - taggedViews[9].setTag(FileSortOrder.sort_small_to_big); + taggedViews[9].setTag(FileSortOrder.Companion.getSort_small_to_big()); taggedViews[10] = binding.sortBySizeDescending; - taggedViews[10].setTag(FileSortOrder.sort_big_to_small); + taggedViews[10].setTag(FileSortOrder.Companion.getSort_big_to_small()); taggedViews[11] = binding.sortBySizeBiggestFirstText; - taggedViews[11].setTag(FileSortOrder.sort_big_to_small); + taggedViews[11].setTag(FileSortOrder.Companion.getSort_big_to_small()); setupActiveOrderSelection(); } @@ -154,8 +157,8 @@ public class SortingOrderDialogFragment extends DialogFragment implements View.O final int color = getResources().getColor(R.color.colorPrimary); Log.i("SortOrder", "currentSortOrderName="+currentSortOrderName); for (View view : taggedViews) { - Log.i("SortOrder", ((FileSortOrder) view.getTag()).name); - if (!((FileSortOrder) view.getTag()).name.equals(currentSortOrderName)) { + Log.i("SortOrder", ((FileSortOrder) view.getTag()).getName()); + if (!((FileSortOrder) view.getTag()).getName().equals(currentSortOrderName)) { continue; } if (view instanceof MaterialButton) { @@ -192,7 +195,7 @@ public class SortingOrderDialogFragment extends DialogFragment implements View.O @Override public void onClick(View v) { - appPreferences.setSorting(((FileSortOrder) v.getTag()).name); + appPreferences.setSorting(((FileSortOrder) v.getTag()).getName()); dismiss(); } } 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..526b09c04 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java @@ -619,7 +619,7 @@ public class DisplayUtils { public static @StringRes int getSortOrderStringId(FileSortOrder sortOrder) { - switch (sortOrder.name) { + switch (sortOrder.getName()) { case sort_z_to_a_id: return R.string.menu_item_sort_by_name_z_a; case sort_new_to_old_id: diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrder.java b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrder.java deleted file mode 100644 index ec88d0e4a..000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrder.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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 java.io.File; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import androidx.annotation.Nullable; - -/** - * Sort order - */ -public class FileSortOrder { - 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 FileSortOrder sort_a_to_z = new FileSortOrderByName(sort_a_to_z_id, true); - public static final FileSortOrder sort_z_to_a = new FileSortOrderByName(sort_z_to_a_id, false); - public static final FileSortOrder sort_old_to_new = new FileSortOrderByDate(sort_old_to_new_id, true); - public static final FileSortOrder sort_new_to_old = new FileSortOrderByDate(sort_new_to_old_id, false); - public static final FileSortOrder sort_small_to_big = new FileSortOrderBySize(sort_small_to_big_id, true); - public static final FileSortOrder sort_big_to_small = new FileSortOrderBySize(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 FileSortOrder(String name, boolean ascending) { - this.name = name; - isAscending = ascending; - } - - public static FileSortOrder 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.getModel().isFavorite() && o2.getModel().isFavorite()) { - return 0; - } else if (o1.getModel().isFavorite()) { - return -1; - } else if (o2.getModel().isFavorite()) { - return 1; - } - return 0; - }); - - return files; - } -} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrder.kt b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrder.kt new file mode 100644 index 000000000..53511b7a6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrder.kt @@ -0,0 +1,95 @@ +/* + * Nextcloud Talk application + * + * @author Sven R. Kunze + * @author Andy Scherzinger + * Copyright (C) 2021-2022 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.remotefilebrowser.model.RemoteFileBrowserItem +import java.util.Collections + +open class FileSortOrder(var name: String, var isAscending: Boolean) { + companion object { + const val sort_a_to_z_id = "sort_a_to_z" + const val sort_z_to_a_id = "sort_z_to_a" + const val sort_old_to_new_id = "sort_old_to_new" + const val sort_new_to_old_id = "sort_new_to_old" + const val sort_small_to_big_id = "sort_small_to_big" + const val sort_big_to_small_id = "sort_big_to_small" + + val sort_a_to_z: FileSortOrder = FileSortOrderByName(sort_a_to_z_id, true) + val sort_z_to_a: FileSortOrder = FileSortOrderByName(sort_z_to_a_id, false) + val sort_old_to_new: FileSortOrder = FileSortOrderByDate(sort_old_to_new_id, true) + val sort_new_to_old: FileSortOrder = FileSortOrderByDate(sort_new_to_old_id, false) + val sort_small_to_big: FileSortOrder = FileSortOrderBySize(sort_small_to_big_id, true) + val sort_big_to_small: FileSortOrder = FileSortOrderBySize(sort_big_to_small_id, false) + + val sortOrders: Map = mapOf( + sort_a_to_z.name to sort_a_to_z, + sort_z_to_a.name to sort_z_to_a, + sort_old_to_new.name to sort_old_to_new, + sort_new_to_old.name to sort_new_to_old, + sort_small_to_big.name to sort_small_to_big, + sort_big_to_small.name to sort_big_to_small, + ) + + fun getFileSortOrder(key: String?): FileSortOrder { + return if (TextUtils.isEmpty(key) || !sortOrders.containsKey(key)) { + sort_a_to_z + } else { + sortOrders[key]!! + } + } + + /** + * Sorts list by Favourites. + * + * @param files files to sort + */ + fun sortCloudFilesByFavourite(files: List): List { + Collections.sort(files, RemoteFileBrowserItemFavoriteComparator()) + return files + } + } + + val multiplier: Int + get() = if (isAscending) 1 else -1 + + open fun sortCloudFiles(files: List): List { + return sortCloudFilesByFavourite(files) + } + + /** + * Comparator for RemoteFileBrowserItems, sorts favorite state. + */ + class RemoteFileBrowserItemFavoriteComparator : Comparator { + override fun compare(left: RemoteFileBrowserItem, right: RemoteFileBrowserItem): Int { + return if (left.isFavorite && right.isFavorite) { + 0 + } else if (left.isFavorite) { + -1 + } else if (right.isFavorite) { + 1 + } else { + 0 + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDate.java b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDate.java deleted file mode 100644 index 2d24e88b0..000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDate.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.components.filebrowser.adapters.items.BrowserFileItem; - -import java.util.Collections; -import java.util.List; - -/** - * Created by srkunze on 28.08.17. - */ -public class FileSortOrderByDate extends FileSortOrder { - - FileSortOrderByDate(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.getModel().getModifiedTimestamp(), o2.getModel().getModifiedTimestamp())); - - return super.sortCloudFiles(files); - } -} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDate.kt b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDate.kt new file mode 100644 index 000000000..f7bcc5e05 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDate.kt @@ -0,0 +1,47 @@ +/* + * Nextcloud Talk application + * + * @author Sven R. Kunze + * @author Andy Scherzinger + * Copyright (C) 2021-2022 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 + +class FileSortOrderByDate internal constructor(name: String, ascending: Boolean) : FileSortOrder(name, ascending) { + /** + * Sorts list by Date. + * + * @param files list of files to sort + */ + override fun sortCloudFiles(files: List): List { + Collections.sort(files, RemoteFileBrowserItemDateComparator(multiplier)) + return super.sortCloudFiles(files) + } + + /** + * Comparator for RemoteFileBrowserItems, sorts by modified timestamp. + */ + class RemoteFileBrowserItemDateComparator(private val multiplier: Int) : Comparator { + + override fun compare(left: RemoteFileBrowserItem, right: RemoteFileBrowserItem): Int { + return multiplier * left.modifiedTimestamp.compareTo(right.modifiedTimestamp) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.java b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.java deleted file mode 100644 index c5035fd60..000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.components.filebrowser.adapters.items.BrowserFileItem; - -import java.util.Collections; -import java.util.List; - -import third_parties.daveKoeller.AlphanumComparator; - -/** - * Created by srkunze on 28.08.17. - */ -public class FileSortOrderByName extends FileSortOrder { - - FileSortOrderByName(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.getModel().isFile() && !o2.getModel().isFile()) { - return multiplier * new AlphanumComparator().compare(o1, o2); - } else if (!o1.getModel().isFile()) { - return -1; - } else if (!o2.getModel().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/FileSortOrderByName.kt b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.kt new file mode 100644 index 000000000..2b4f3524a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.kt @@ -0,0 +1,57 @@ +/* + * Nextcloud Talk application + * + * @author Sven R. Kunze + * @author Andy Scherzinger + * Copyright (C) 2021-2022 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 third_parties.daveKoeller.AlphanumComparator +import java.util.Collections + +class FileSortOrderByName internal constructor(name: String, ascending: Boolean) : FileSortOrder(name, ascending) { + /** + * Sorts list by Name. + * + * @param files files to sort + */ + override fun sortCloudFiles(files: List): List { + Collections.sort(files, RemoteFileBrowserItemNameComparator(multiplier)) + return super.sortCloudFiles(files) + } + + /** + * Comparator for RemoteFileBrowserItems, sorts by name. + */ + class RemoteFileBrowserItemNameComparator(private val multiplier: Int) : Comparator { + private val alphanumComparator = AlphanumComparator() + + override fun compare(left: RemoteFileBrowserItem, right: RemoteFileBrowserItem): Int { + return if (!left.isFile && !right.isFile) { + return multiplier * alphanumComparator.compare(left.path, right.path) + } else if (!left.isFile) { + -1 + } else if (!right.isFile) { + 1 + } else { + multiplier * alphanumComparator.compare(left.path, right.path) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySize.java b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySize.java deleted file mode 100644 index 4eab78668..000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySize.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.components.filebrowser.adapters.items.BrowserFileItem; - -import java.util.Collections; -import java.util.List; - -/** - * Sorts files by sizes - */ -public class FileSortOrderBySize extends FileSortOrder { - - FileSortOrderBySize(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.getModel().isFile() && !o2.getModel().isFile()) { - return multiplier * Long.compare(o1.getModel().getSize(), o2.getModel().getSize()); - } else if (!o1.getModel().isFile()) { - return -1; - } else if (!o2.getModel().isFile()) { - return 1; - } else { - return multiplier * Long.compare(o1.getModel().getSize(), o2.getModel().getSize()); - } - }); - - return super.sortCloudFiles(files); - } -} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySize.kt b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySize.kt new file mode 100644 index 000000000..9beab94a4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySize.kt @@ -0,0 +1,55 @@ +/* + * Nextcloud Talk application + * + * @author Sven R. Kunze + * @author Andy Scherzinger + * Copyright (C) 2021-2022 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 + +class FileSortOrderBySize internal constructor(name: String, ascending: Boolean) : FileSortOrder(name, ascending) { + /** + * Sorts list by Size. + * + * @param files list of files to sort + */ + override fun sortCloudFiles(files: List): List { + Collections.sort(files, RemoteFileBrowserItemSizeComparator(multiplier)) + return super.sortCloudFiles(files) + } + + /** + * Comparator for RemoteFileBrowserItems, sorts by name. + */ + class RemoteFileBrowserItemSizeComparator(private val multiplier: Int) : Comparator { + + override fun compare(left: RemoteFileBrowserItem, right: RemoteFileBrowserItem): Int { + return if (!left.isFile && !right.isFile) { + return multiplier * left.size.compareTo(right.size) + } else if (!left.isFile) { + -1 + } else if (!right.isFile) { + 1 + } else { + multiplier * left.size.compareTo(right.size) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileUtils.java b/app/src/main/java/com/nextcloud/talk/utils/FileUtils.java index 796337f83..ceb8f1bb9 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/FileUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/FileUtils.java @@ -65,4 +65,21 @@ public class FileUtils { return cacheFile; } + + /** + * Creates a new {@link File} + */ + public static void removeTempCacheFile(@NonNull Context context, String fileName) throws IOException { + File cacheFile = new File(context.getApplicationContext().getFilesDir().getAbsolutePath() + "/" + fileName); + + Log.v(TAG, "Full path for new cache file:" + cacheFile.getAbsolutePath()); + + if (cacheFile.exists()) { + if(cacheFile.delete()) { + Log.v(TAG, "Deletion successful"); + } else { + throw new IOException("Directory for temporary file does not exist and could not be created."); + } + } + } } 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..75ca6b9d3 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,5 @@ 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_MIME_TYPE_FILTER = "KEY_MIME_TYPE_FILTER" } diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java index 6ff15688c..fd141a4d1 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java @@ -25,7 +25,6 @@ package com.nextcloud.talk.utils.preferences; import com.nextcloud.talk.R; -import com.nextcloud.talk.utils.FileSortOrder; import net.orange_box.storebox.annotations.method.ClearMethod; import net.orange_box.storebox.annotations.method.DefaultValue; diff --git a/app/src/main/java/third_parties/daveKoeller/AlphanumComparator.java b/app/src/main/java/third_parties/daveKoeller/AlphanumComparator.java index af25c47b2..35cd76177 100644 --- a/app/src/main/java/third_parties/daveKoeller/AlphanumComparator.java +++ b/app/src/main/java/third_parties/daveKoeller/AlphanumComparator.java @@ -24,9 +24,6 @@ package third_parties.daveKoeller; -import com.nextcloud.talk.components.filebrowser.adapters.items.BrowserFileItem; - -import java.io.File; import java.io.Serializable; import java.math.BigInteger; import java.text.Collator; @@ -87,13 +84,6 @@ public class AlphanumComparator implements Comparator, Serializable { return chunk.toString(); } - public int compare(BrowserFileItem f1, BrowserFileItem f2) { - String s1 = f1.getModel().getPath(); - String s2 = f2.getModel().getPath(); - - return compare(s1, s2); - } - public int compare(T t1, T t2) { return compare(t1.toString(), t2.toString()); } diff --git a/app/src/main/res/drawable/round_corner.xml b/app/src/main/res/drawable/round_corner.xml index ecc82cfd1..6888a97bf 100644 --- a/app/src/main/res/drawable/round_corner.xml +++ b/app/src/main/res/drawable/round_corner.xml @@ -1,5 +1,5 @@ - + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/controller_browser.xml b/app/src/main/res/layout/controller_browser.xml deleted file mode 100644 index 294136883..000000000 --- a/app/src/main/res/layout/controller_browser.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/controller_profile.xml b/app/src/main/res/layout/controller_profile.xml index 9b6e7de19..9cf360582 100644 --- a/app/src/main/res/layout/controller_profile.xml +++ b/app/src/main/res/layout/controller_profile.xml @@ -78,8 +78,8 @@ diff --git a/app/src/main/res/menu/file_browser_path.xml b/app/src/main/res/menu/file_browser_path.xml deleted file mode 100644 index 67c6dfd52..000000000 --- a/app/src/main/res/menu/file_browser_path.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index deecee935..fc3942e1a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -358,7 +358,6 @@ Back %1$s | Last modified: %2$s - You are not allowed to re-share this file Sort by file_browser_sort_by sort_a_to_z diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index d16b13034..02455e83e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -63,11 +63,7 @@ @color/fontAppbar - - - +