From 74927837cf9ac5d29cc7b9747aba65d22b27013d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 11 Mar 2026 13:35:47 +0100 Subject: [PATCH 1/7] Rename .java to .kt Signed-off-by: alperozturk96 --- .../activities/{ActivitiesActivity.java => ActivitiesActivity.kt} | 0 .../activities/{ActivitiesContract.java => ActivitiesContract.kt} | 0 .../{ActivitiesPresenter.java => ActivitiesPresenter.kt} | 0 ...ckyHeaderItemDecoration.java => StickyHeaderItemDecoration.kt} | 0 .../{ActivitiesRepository.java => ActivitiesRepository.kt} | 0 .../{ActivitiesServiceApi.java => ActivitiesServiceApi.kt} | 0 ...{ActivitiesServiceApiImpl.java => ActivitiesServiceApiImpl.kt} | 0 ...oteActivitiesRepository.java => RemoteActivitiesRepository.kt} | 0 .../data/files/{FilesRepository.java => FilesRepository.kt} | 0 .../data/files/{FilesServiceApi.java => FilesServiceApi.kt} | 0 .../files/{FilesServiceApiImpl.java => FilesServiceApiImpl.kt} | 0 .../{RemoteFilesRepository.java => RemoteFilesRepository.kt} | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/owncloud/android/ui/activities/{ActivitiesActivity.java => ActivitiesActivity.kt} (100%) rename app/src/main/java/com/owncloud/android/ui/activities/{ActivitiesContract.java => ActivitiesContract.kt} (100%) rename app/src/main/java/com/owncloud/android/ui/activities/{ActivitiesPresenter.java => ActivitiesPresenter.kt} (100%) rename app/src/main/java/com/owncloud/android/ui/activities/{StickyHeaderItemDecoration.java => StickyHeaderItemDecoration.kt} (100%) rename app/src/main/java/com/owncloud/android/ui/activities/data/activities/{ActivitiesRepository.java => ActivitiesRepository.kt} (100%) rename app/src/main/java/com/owncloud/android/ui/activities/data/activities/{ActivitiesServiceApi.java => ActivitiesServiceApi.kt} (100%) rename app/src/main/java/com/owncloud/android/ui/activities/data/activities/{ActivitiesServiceApiImpl.java => ActivitiesServiceApiImpl.kt} (100%) rename app/src/main/java/com/owncloud/android/ui/activities/data/activities/{RemoteActivitiesRepository.java => RemoteActivitiesRepository.kt} (100%) rename app/src/main/java/com/owncloud/android/ui/activities/data/files/{FilesRepository.java => FilesRepository.kt} (100%) rename app/src/main/java/com/owncloud/android/ui/activities/data/files/{FilesServiceApi.java => FilesServiceApi.kt} (100%) rename app/src/main/java/com/owncloud/android/ui/activities/data/files/{FilesServiceApiImpl.java => FilesServiceApiImpl.kt} (100%) rename app/src/main/java/com/owncloud/android/ui/activities/data/files/{RemoteFilesRepository.java => RemoteFilesRepository.kt} (100%) diff --git a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java rename to app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.kt diff --git a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesContract.java b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesContract.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activities/ActivitiesContract.java rename to app/src/main/java/com/owncloud/android/ui/activities/ActivitiesContract.kt diff --git a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesPresenter.java b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesPresenter.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activities/ActivitiesPresenter.java rename to app/src/main/java/com/owncloud/android/ui/activities/ActivitiesPresenter.kt diff --git a/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.java b/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.java rename to app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.kt diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesRepository.java b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesRepository.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesRepository.java rename to app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesRepository.kt diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApi.java b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApi.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApi.java rename to app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApi.kt diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApiImpl.java b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApiImpl.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApiImpl.java rename to app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApiImpl.kt diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/RemoteActivitiesRepository.java b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/RemoteActivitiesRepository.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activities/data/activities/RemoteActivitiesRepository.java rename to app/src/main/java/com/owncloud/android/ui/activities/data/activities/RemoteActivitiesRepository.kt diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesRepository.java b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesRepository.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesRepository.java rename to app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesRepository.kt diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApi.java b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApi.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApi.java rename to app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApi.kt diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApiImpl.java b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApiImpl.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApiImpl.java rename to app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApiImpl.kt diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/files/RemoteFilesRepository.java b/app/src/main/java/com/owncloud/android/ui/activities/data/files/RemoteFilesRepository.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activities/data/files/RemoteFilesRepository.java rename to app/src/main/java/com/owncloud/android/ui/activities/data/files/RemoteFilesRepository.kt From 9142730334c907913bc1b59353dd4a2a014916c1 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 11 Mar 2026 13:35:48 +0100 Subject: [PATCH 2/7] refactor: convert activities Java classes to Kotlin Signed-off-by: alperozturk96 --- .../nextcloud/client/ActivitiesActivityIT.kt | 6 +- .../ui/activities/ActivitiesActivity.kt | 367 +++++++++--------- .../ui/activities/ActivitiesContract.kt | 43 +- .../ui/activities/ActivitiesPresenter.kt | 125 +++--- .../activities/StickyHeaderItemDecoration.kt | 141 ++++--- .../data/activities/ActivitiesRepository.kt | 17 +- .../data/activities/ActivitiesServiceApi.kt | 21 +- .../activities/ActivitiesServiceApiImpl.kt | 165 +++----- .../data/activities/ActivityRepositories.java | 20 - .../activities/RemoteActivitiesRepository.kt | 52 +-- .../data/files/FileRepositories.java | 20 - .../activities/data/files/FilesRepository.kt | 21 +- .../activities/data/files/FilesServiceApi.kt | 18 +- .../data/files/FilesServiceApiImpl.kt | 180 ++++----- .../data/files/RemoteFilesRepository.kt | 50 ++- .../android/ui/activity/DrawerActivity.java | 2 +- 16 files changed, 530 insertions(+), 718 deletions(-) delete mode 100644 app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivityRepositories.java delete mode 100644 app/src/main/java/com/owncloud/android/ui/activities/data/files/FileRepositories.java diff --git a/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt index 3e403d2203c5..1a8292f5a846 100644 --- a/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt @@ -50,9 +50,9 @@ class ActivitiesActivityIT : AbstractIT() { fun loading() { launchActivity().use { scenario -> scenario.onActivity { sut -> - sut.binding.emptyList.root.visibility = View.GONE - sut.binding.swipeContainingList.visibility = View.GONE - sut.binding.loadingContent.visibility = View.VISIBLE + sut.binding?.emptyList?.root?.visibility = View.GONE + sut.binding?.swipeContainingList?.visibility = View.GONE + sut.binding?.loadingContent?.visibility = View.VISIBLE } val screenShotName = createName(testClassName + "_" + "loading", "") diff --git a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.kt b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.kt index 5327d157a9f7..b8b10f13b455 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.kt @@ -1,256 +1,239 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2018 Edvard Holst * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.activities; - -import android.content.Intent; -import android.os.Bundle; -import android.view.MenuItem; -import android.view.View; - -import com.nextcloud.client.network.ClientFactory; -import com.nextcloud.client.network.ConnectivityService; -import com.nextcloud.common.NextcloudClient; -import com.owncloud.android.R; -import com.owncloud.android.databinding.ActivityListLayoutBinding; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.activities.model.RichObject; -import com.owncloud.android.lib.resources.files.FileUtils; -import com.owncloud.android.ui.activities.data.activities.ActivitiesRepository; -import com.owncloud.android.ui.activities.data.files.FilesRepository; -import com.owncloud.android.ui.activity.DrawerActivity; -import com.owncloud.android.ui.activity.FileDisplayActivity; -import com.owncloud.android.ui.adapter.ActivityListAdapter; -import com.owncloud.android.ui.interfaces.ActivityListInterface; -import com.owncloud.android.ui.preview.PreviewImageActivity; -import com.owncloud.android.ui.preview.PreviewImageFragment; -import com.owncloud.android.utils.DisplayUtils; - -import java.util.List; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import static com.owncloud.android.ui.activity.FileActivity.EXTRA_FILE; -import static com.owncloud.android.ui.activity.FileActivity.EXTRA_USER; - -/** - * This Activity presents activities feed. - */ -public class ActivitiesActivity extends DrawerActivity implements ActivityListInterface, ActivitiesContract.View { - private static final String TAG = ActivitiesActivity.class.getSimpleName(); - - ActivityListLayoutBinding binding; - private ActivityListAdapter adapter; - private long lastGiven; - private boolean isLoadingActivities; - private ActivitiesContract.ActionListener actionListener; - - @Inject ActivitiesRepository activitiesRepository; - @Inject FilesRepository filesRepository; - @Inject ClientFactory clientFactory; - @Inject ConnectivityService connectivityService; - - @Override - protected void onCreate(Bundle savedInstanceState) { - Log_OC.v(TAG, "onCreate() start"); - super.onCreate(savedInstanceState); - - actionListener = new ActivitiesPresenter(activitiesRepository, filesRepository, this); - - binding = ActivityListLayoutBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - // setup toolbar - setupToolbar(); - - viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingList); - - // setup drawer - setupDrawer(getMenuItemId()); - updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_activities)); - - binding.swipeContainingList.setOnRefreshListener(() -> { +package com.owncloud.android.ui.activities + +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import androidx.core.view.size +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.R +import com.owncloud.android.databinding.ActivityListLayoutBinding +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.activities.model.RichObject +import com.owncloud.android.lib.resources.files.FileUtils +import com.owncloud.android.ui.activities.data.activities.ActivitiesRepository +import com.owncloud.android.ui.activities.data.files.FilesRepository +import com.owncloud.android.ui.activity.DrawerActivity +import com.owncloud.android.ui.activity.FileActivity +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.adapter.ActivityListAdapter +import com.owncloud.android.ui.interfaces.ActivityListInterface +import com.owncloud.android.ui.preview.PreviewImageActivity +import com.owncloud.android.ui.preview.PreviewImageFragment.Companion.canBePreviewed +import com.owncloud.android.utils.DisplayUtils +import java.util.function.Supplier +import javax.inject.Inject + +class ActivitiesActivity : + DrawerActivity(), + ActivityListInterface, + ActivitiesContract.View { + var binding: ActivityListLayoutBinding? = null + private var adapter: ActivityListAdapter? = null + private var lastGiven: Long = 0 + private var isLoadingActivities = false + private var actionListener: ActivitiesContract.ActionListener? = null + + @Inject + lateinit var activitiesRepository: ActivitiesRepository + + @Inject + lateinit var filesRepository: FilesRepository + + @Inject + lateinit var connectivityService: ConnectivityService + + override fun onCreate(savedInstanceState: Bundle?) { + Log_OC.v(TAG, "onCreate() start") + super.onCreate(savedInstanceState) + + actionListener = ActivitiesPresenter(activitiesRepository, filesRepository, this) + + binding = ActivityListLayoutBinding.inflate(layoutInflater) + setContentView(binding?.getRoot()) + + setupToolbar() + + binding?.swipeContainingList?.let { viewThemeUtils.androidx.themeSwipeRefreshLayout(it) } + + setupDrawer(menuItemId) + updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_activities)) + + binding?.swipeContainingList?.setOnRefreshListener { // We set lastGiven variable to undefined here since when manually refreshing // activities data we want to clear the list and reset the pagination. - lastGiven = ActivitiesContract.ActionListener.UNDEFINED; - actionListener.loadActivities(lastGiven); - }); - } - - @Override - protected int getMenuItemId() { - return R.id.nav_activity; - } - - @VisibleForTesting - public ActivityListLayoutBinding getBinding() { - return binding; + lastGiven = ActivitiesContract.ActionListener.UNDEFINED.toLong() + actionListener?.loadActivities(lastGiven) + } } - /** - * sets up the UI elements and loads all activity items. - */ - private void setupContent() { - binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_activity); + override fun getMenuItemId(): Int = R.id.nav_activity - adapter = new ActivityListAdapter(this, - getUserAccountManager(), - this, - clientFactory, - false, - viewThemeUtils); - binding.list.setAdapter(adapter); + @Suppress("MagicNumber") + private fun setupContent() { + binding?.emptyList?.emptyListIcon?.setImageResource(R.drawable.ic_activity) - LinearLayoutManager layoutManager = new LinearLayoutManager(this); + adapter = ActivityListAdapter( + this, + userAccountManager, + this, + clientFactory, + false, + viewThemeUtils + ) + binding?.list?.setAdapter(adapter) - binding.list.setLayoutManager(layoutManager); - binding.list.addOnScrollListener(new RecyclerView.OnScrollListener() { + val layoutManager = LinearLayoutManager(this) - @Override - public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - super.onScrolled(recyclerView, dx, dy); + binding?.list?.setLayoutManager(layoutManager) + binding?.list?.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) - int visibleItemCount = recyclerView.getChildCount(); - int totalItemCount = layoutManager.getItemCount(); - int firstVisibleItemIndex = layoutManager.findFirstVisibleItemPosition(); + val visibleItemCount = recyclerView.size + val totalItemCount = layoutManager.getItemCount() + val firstVisibleItemIndex = layoutManager.findFirstVisibleItemPosition() // synchronize loading state when item count changes - if (!isLoadingActivities && (totalItemCount - visibleItemCount) <= (firstVisibleItemIndex + 5) - && lastGiven > 0) { + if (!isLoadingActivities && (totalItemCount - visibleItemCount) <= (firstVisibleItemIndex + 5) && + lastGiven > 0 + ) { // Almost reached the end, continue to load new activities - actionListener.loadActivities(lastGiven); + actionListener?.loadActivities(lastGiven) } } - }); + }) - actionListener.loadActivities(ActivitiesContract.ActionListener.UNDEFINED); + actionListener?.loadActivities(ActivitiesContract.ActionListener.UNDEFINED.toLong()) } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - boolean retval = true; + override fun onOptionsItemSelected(item: MenuItem): Boolean { + var retval = true - if (item.getItemId() == android.R.id.home) { - if (isDrawerOpen()) { - closeDrawer(); + if (item.itemId == android.R.id.home) { + if (isDrawerOpen) { + closeDrawer() } else { - openDrawer(); + openDrawer() } } else { - Log_OC.w(TAG, "Unknown menu item triggered"); - retval = super.onOptionsItemSelected(item); + Log_OC.w(TAG, "Unknown menu item triggered") + retval = super.onOptionsItemSelected(item) } - return retval; + return retval } - @Override - protected void onResume() { - super.onResume(); - highlightNavigationViewItem(getMenuItemId()); - actionListener.onResume(); - setupContent(); + override fun onResume() { + super.onResume() + highlightNavigationViewItem(menuItemId) + actionListener?.onResume() + setupContent() } - @Override - public void onActivityClicked(RichObject richObject) { - String path = FileUtils.PATH_SEPARATOR + richObject.getPath(); - actionListener.openActivity(path, this); + override fun onActivityClicked(richObject: RichObject) { + val path = FileUtils.PATH_SEPARATOR + richObject.path + actionListener?.openActivity(path, this) } - @Override - public void showActivities(List activities, NextcloudClient client, long lastGiven) { - boolean clear = this.lastGiven == ActivitiesContract.ActionListener.UNDEFINED; - adapter.setActivityItems(activities, client, clear); - this.lastGiven = lastGiven; + override fun showActivities(activities: List, client: NextcloudClient, lastGiven: Long) { + val clear = this.lastGiven == ActivitiesContract.ActionListener.UNDEFINED.toLong() + adapter?.setActivityItems(activities, client, clear) + this.lastGiven = lastGiven // Hide the recyclerView if list is empty - if (adapter.isEmpty()) { - showEmptyContent(getString(R.string.activities_no_results_headline), getString(R.string.activities_no_results_message)); - binding.loadingContent.setVisibility(View.GONE); - binding.list.setVisibility(View.GONE); + if (adapter?.isEmpty == true) { + showEmptyContent( + getString(R.string.activities_no_results_headline), + getString(R.string.activities_no_results_message) + ) + binding?.loadingContent?.visibility = View.GONE + binding?.list?.visibility = View.GONE } else { - binding.emptyList.emptyListView.setVisibility(View.GONE); - binding.loadingContent.setVisibility(View.GONE); - binding.list.setVisibility(View.VISIBLE); + binding?.emptyList?.emptyListView?.visibility = View.GONE + binding?.loadingContent?.visibility = View.GONE + binding?.list?.visibility = View.VISIBLE } } - @Override - public void showActivitiesLoadError(String error) { - connectivityService.isNetworkAndServerAvailable(result -> { - if (result) { - DisplayUtils.showSnackMessage(this, error); + override fun showActivitiesLoadError(error: String) { + connectivityService.isNetworkAndServerAvailable { + if (it) { + DisplayUtils.showSnackMessage(this, error) } else { - showEmptyContent(getString(R.string.server_not_reachable), - getString(R.string.server_not_reachable_content)); - binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_sync_off); + showEmptyContent( + getString(R.string.server_not_reachable), + getString(R.string.server_not_reachable_content) + ) + binding?.emptyList?.emptyListIcon?.setImageResource(R.drawable.ic_sync_off) } - }); - + } } - @Override - public void showActivityDetailUI(OCFile ocFile) { - Intent showDetailsIntent; - if (PreviewImageFragment.canBePreviewed(ocFile)) { - showDetailsIntent = new Intent(getBaseContext(), PreviewImageActivity.class); + override fun showActivityDetailUI(ocFile: OCFile) { + val intent = if (canBePreviewed(ocFile)) { + Intent(baseContext, PreviewImageActivity::class.java) } else { - showDetailsIntent = new Intent(getBaseContext(), FileDisplayActivity.class); + Intent(baseContext, FileDisplayActivity::class.java) + }.apply { + putExtra(FileActivity.EXTRA_FILE, ocFile) + putExtra( + FileActivity.EXTRA_USER, + user.orElseThrow(Supplier { RuntimeException() }) + ) } - showDetailsIntent.putExtra(EXTRA_FILE, ocFile); - showDetailsIntent.putExtra(EXTRA_USER, getUser().orElseThrow(RuntimeException::new)); - startActivity(showDetailsIntent); + startActivity(intent) } - @Override - public void showActivityDetailUIIsNull() { - DisplayUtils.showSnackMessage(this, R.string.file_not_found); + override fun showActivityDetailUIIsNull() { + DisplayUtils.showSnackMessage(this, R.string.file_not_found) } - @Override - public void showActivityDetailError(String error) { - DisplayUtils.showSnackMessage(this, error); + override fun showActivityDetailError(error: String) { + DisplayUtils.showSnackMessage(this, error) } - @Override - public void showLoadingMessage() { - binding.emptyList.emptyListView.setVisibility(View.GONE); + override fun showLoadingMessage() { + binding?.emptyList?.emptyListView?.visibility = View.GONE } - @Override - public void showEmptyContent(String headline, String message) { - binding.emptyList.emptyListViewHeadline.setText(headline); - binding.emptyList.emptyListViewText.setText(message); - binding.loadingContent.setVisibility(View.GONE); - binding.emptyList.emptyListIcon.setVisibility(View.VISIBLE); - binding.emptyList.emptyListViewHeadline.setVisibility(View.VISIBLE); - binding.emptyList.emptyListViewText.setVisibility(View.VISIBLE); - binding.emptyList.emptyListView.setVisibility(View.VISIBLE); + override fun showEmptyContent(headline: String, message: String) { + binding?.run { + emptyList.emptyListViewHeadline.text = headline + emptyList.emptyListViewText.text = message + loadingContent.visibility = View.GONE + emptyList.emptyListIcon.setVisibility(View.VISIBLE) + emptyList.emptyListViewHeadline.visibility = View.VISIBLE + emptyList.emptyListViewText.visibility = View.VISIBLE + emptyList.emptyListView.visibility = View.VISIBLE + } } - @Override - public void setProgressIndicatorState(boolean isActive) { - isLoadingActivities = isActive; - if (!adapter.isEmpty()) { - binding.swipeContainingList.post(() -> binding.swipeContainingList.setRefreshing(isActive)); + override fun setProgressIndicatorState(isActive: Boolean) { + isLoadingActivities = isActive + if (adapter?.isEmpty == false) { + binding?.swipeContainingList?.post { binding?.swipeContainingList?.isRefreshing = isActive } } } - @Override - protected void onStop() { - super.onStop(); + override fun onStop() { + super.onStop() + actionListener?.onStop() + } - actionListener.onStop(); + companion object { + private val TAG: String = ActivitiesActivity::class.java.getSimpleName() } } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesContract.kt b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesContract.kt index b5396d47ef81..664cd0ceb6fe 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesContract.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesContract.kt @@ -1,39 +1,40 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2018 Edvard Holst * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.activities; +package com.owncloud.android.ui.activities -import com.nextcloud.common.NextcloudClient; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.ui.activity.BaseActivity; - -import java.util.List; - -public interface ActivitiesContract { +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.BaseActivity +interface ActivitiesContract { interface View { - void showActivities(List activities, NextcloudClient client, long lastGiven); - void showActivitiesLoadError(String error); - void showActivityDetailUI(OCFile ocFile); - void showActivityDetailUIIsNull(); - void showActivityDetailError(String error); - void showLoadingMessage(); - void showEmptyContent(String headline, String message); - void setProgressIndicatorState(boolean isActive); + fun showActivities(activities: List, client: NextcloudClient, lastGiven: Long) + fun showActivitiesLoadError(error: String) + fun showActivityDetailUI(ocFile: OCFile) + fun showActivityDetailUIIsNull() + fun showActivityDetailError(error: String) + fun showLoadingMessage() + fun showEmptyContent(headline: String, message: String) + fun setProgressIndicatorState(isActive: Boolean) } interface ActionListener { - int UNDEFINED = -1; + fun loadActivities(lastGiven: Long) - void loadActivities(long lastGiven); + fun openActivity(fileUrl: String, baseActivity: BaseActivity) - void openActivity(String fileUrl, BaseActivity baseActivity); + fun onStop() - void onStop(); + fun onResume() - void onResume(); + companion object { + @JvmField + val UNDEFINED: Int = -1 + } } } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesPresenter.kt b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesPresenter.kt index 1c4ff09cbab5..cf35391ed1f4 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesPresenter.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesPresenter.kt @@ -1,94 +1,81 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2018 Edvard Holst * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.activities; +package com.owncloud.android.ui.activities -import com.nextcloud.common.NextcloudClient; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.ui.activities.data.activities.ActivitiesRepository; -import com.owncloud.android.ui.activities.data.files.FilesRepository; -import com.owncloud.android.ui.activity.BaseActivity; +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activities.data.activities.ActivitiesRepository +import com.owncloud.android.ui.activities.data.activities.ActivitiesRepository.LoadActivitiesCallback +import com.owncloud.android.ui.activities.data.files.FilesRepository +import com.owncloud.android.ui.activities.data.files.FilesRepository.ReadRemoteFileCallback +import com.owncloud.android.ui.activity.BaseActivity -import java.util.List; +class ActivitiesPresenter internal constructor( + private val activitiesRepository: ActivitiesRepository, + private val filesRepository: FilesRepository, + private val activitiesView: ActivitiesContract.View +) : ActivitiesContract.ActionListener { + private var activityStopped = false -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public class ActivitiesPresenter implements ActivitiesContract.ActionListener { - - private final ActivitiesContract.View activitiesView; - private final ActivitiesRepository activitiesRepository; - private final FilesRepository filesRepository; - private boolean activityStopped; - - - ActivitiesPresenter(@NonNull ActivitiesRepository activitiesRepository, - @NonNull FilesRepository filesRepository, - @NonNull ActivitiesContract.View activitiesView) { - this.activitiesRepository = activitiesRepository; - this.activitiesView = activitiesView; - this.filesRepository = filesRepository; - } - - @Override - public void loadActivities(long lastGiven) { - if (UNDEFINED == lastGiven) { - activitiesView.showLoadingMessage(); + override fun loadActivities(lastGiven: Long) { + if (ActivitiesContract.ActionListener.UNDEFINED.toLong() == lastGiven) { + activitiesView.showLoadingMessage() } else { - activitiesView.setProgressIndicatorState(true); + activitiesView.setProgressIndicatorState(true) } - activitiesRepository.getActivities(lastGiven, new ActivitiesRepository.LoadActivitiesCallback() { - @Override - public void onActivitiesLoaded(List activities, NextcloudClient client, long lastGiven) { - - if (!activityStopped) { - activitiesView.setProgressIndicatorState(false); - activitiesView.showActivities(activities, client, lastGiven); + activitiesRepository.getActivities( + lastGiven, + object : LoadActivitiesCallback { + override fun onActivitiesLoaded(activities: List, client: NextcloudClient, lastGiven: Long) { + if (!activityStopped) { + activitiesView.setProgressIndicatorState(false) + activitiesView.showActivities(activities, client, lastGiven) + } } - } - @Override - public void onActivitiesLoadedError(String error) { - if (!activityStopped) { - activitiesView.setProgressIndicatorState(false); - activitiesView.showActivitiesLoadError(error); + override fun onActivitiesLoadedError(error: String) { + if (!activityStopped) { + activitiesView.setProgressIndicatorState(false) + activitiesView.showActivitiesLoadError(error) + } } } - }); + ) } - @Override - public void openActivity(String fileUrl, BaseActivity baseActivity) { - activitiesView.setProgressIndicatorState(true); - filesRepository.readRemoteFile(fileUrl, baseActivity, new FilesRepository.ReadRemoteFileCallback() { - @Override - public void onFileLoaded(@Nullable OCFile ocFile) { - activitiesView.setProgressIndicatorState(false); - if (ocFile != null) { - activitiesView.showActivityDetailUI(ocFile); - } else { - activitiesView.showActivityDetailUIIsNull(); - } + override fun openActivity(fileUrl: String, baseActivity: BaseActivity) { + activitiesView.setProgressIndicatorState(true) + filesRepository.readRemoteFile( + fileUrl, + baseActivity, + object : ReadRemoteFileCallback { + override fun onFileLoaded(ocFile: OCFile?) { + activitiesView.setProgressIndicatorState(false) + if (ocFile != null) { + activitiesView.showActivityDetailUI(ocFile) + } else { + activitiesView.showActivityDetailUIIsNull() } + } - @Override - public void onFileLoadError(String error) { - activitiesView.setProgressIndicatorState(false); - activitiesView.showActivityDetailError(error); - } - }); + override fun onFileLoadError(error: String) { + activitiesView.setProgressIndicatorState(false) + activitiesView.showActivityDetailError(error) + } + } + ) } - @Override - public void onStop() { - activityStopped = true; + override fun onStop() { + activityStopped = true } - @Override - public void onResume() { - activityStopped = false; + override fun onResume() { + activityStopped = false } } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.kt b/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.kt index f862db555221..00d374d6a03b 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.kt @@ -1,107 +1,98 @@ /* * Nextcloud Android client application * - * Copyright (C) 2019 Sevastyan Savanyuk * Copyright (C) 2019 Nextcloud GmbH * * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.activities; - -import android.graphics.Canvas; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.owncloud.android.ui.adapter.StickyHeaderAdapter; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -public class StickyHeaderItemDecoration extends RecyclerView.ItemDecoration { - private final StickyHeaderAdapter adapter; - - - public StickyHeaderItemDecoration(StickyHeaderAdapter stickyHeaderAdapter) { - this.adapter = stickyHeaderAdapter; - } - - @Override - public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { - super.onDrawOver(canvas, parent, state); - - View topChild = parent.getChildAt(0); - if (topChild == null) { - return; - } - int topChildPosition = parent.getChildAdapterPosition(topChild); +package com.owncloud.android.ui.activities + +import android.graphics.Canvas +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration +import com.owncloud.android.ui.adapter.StickyHeaderAdapter +import androidx.core.graphics.withTranslation +import androidx.core.view.size + +@Suppress("ReturnCount") +class StickyHeaderItemDecoration(private val adapter: StickyHeaderAdapter) : ItemDecoration() { + override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + super.onDrawOver(canvas, parent, state) + + val topChild = parent.getChildAt(0) ?: return + val topChildPosition = parent.getChildAdapterPosition(topChild) if (topChildPosition == RecyclerView.NO_POSITION) { - return; - } - View currentHeader = getHeaderViewForItem(topChildPosition, parent); - fixLayoutSize(parent, currentHeader); - int contactPoint = currentHeader.getBottom(); - View childInContact = getChildInContact(parent, contactPoint); - - if (childInContact == null) { - return; + return } + val currentHeader = getHeaderViewForItem(topChildPosition, parent) + fixLayoutSize(parent, currentHeader) + val contactPoint = currentHeader.bottom + val childInContact = getChildInContact(parent, contactPoint) ?: return if (adapter.isHeader(parent.getChildAdapterPosition(childInContact))) { - moveHeader(canvas, currentHeader, childInContact); - return; + moveHeader(canvas, currentHeader, childInContact) + return } - drawHeader(canvas, currentHeader); + drawHeader(canvas, currentHeader) } - private void drawHeader(Canvas canvas, View header) { - canvas.save(); - canvas.translate(0, 0); - header.draw(canvas); - canvas.restore(); + private fun drawHeader(canvas: Canvas, header: View) { + canvas.withTranslation(0f, 0f) { + header.draw(this) + } } - private void moveHeader(Canvas canvas, View currentHeader, View nextHeader) { - canvas.save(); - canvas.translate(0, nextHeader.getTop() - currentHeader.getHeight()); - currentHeader.draw(canvas); - canvas.restore(); + private fun moveHeader(canvas: Canvas, currentHeader: View, nextHeader: View) { + canvas.withTranslation(0f, (nextHeader.top - currentHeader.height).toFloat()) { + currentHeader.draw(this) + } } - private View getChildInContact(RecyclerView parent, int contactPoint) { - View childInContact = null; - for (int i = 0; i < parent.getChildCount(); i++) { - View currentChild = parent.getChildAt(i); - if (currentChild.getBottom() > contactPoint && currentChild.getTop() <= contactPoint) { - childInContact = currentChild; - break; + private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? { + var childInContact: View? = null + for (i in 0.. contactPoint && currentChild.top <= contactPoint) { + childInContact = currentChild + break } } - return childInContact; + return childInContact } - private View getHeaderViewForItem(int itemPosition, RecyclerView parent) { - int headerPosition = adapter.getHeaderPositionForItem(itemPosition); - int layoutId = adapter.getHeaderLayout(itemPosition); - View header = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); - adapter.bindHeaderData(header, headerPosition); - return header; + private fun getHeaderViewForItem(itemPosition: Int, parent: RecyclerView): View { + val headerPosition = adapter.getHeaderPositionForItem(itemPosition) + val layoutId = adapter.getHeaderLayout(itemPosition) + val header = LayoutInflater.from(parent.context).inflate(layoutId, parent, false) + adapter.bindHeaderData(header, headerPosition) + return header } - private void fixLayoutSize(ViewGroup parent, View view) { - + private fun fixLayoutSize(parent: ViewGroup, view: View) { // Specs for parent (RecyclerView) - int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY); - int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); - // Specs for children (headers) - int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), view.getLayoutParams().width); - int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), view.getLayoutParams().height); + val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY) + val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED) - view.measure(childWidthSpec, childHeightSpec); - view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); + // Specs for children (headers) + val childWidthSpec = ViewGroup.getChildMeasureSpec( + widthSpec, + parent.getPaddingLeft() + parent.getPaddingRight(), + view.layoutParams.width + ) + val childHeightSpec = ViewGroup.getChildMeasureSpec( + heightSpec, + parent.paddingTop + parent.paddingBottom, + view.layoutParams.height + ) + + view.measure(childWidthSpec, childHeightSpec) + view.layout(0, 0, view.measuredWidth, view.measuredHeight) } } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesRepository.kt b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesRepository.kt index e23c3a391cdb..a5130fd8fd31 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesRepository.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesRepository.kt @@ -1,25 +1,22 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2018 Edvard Holst * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.activities.data.activities; +package com.owncloud.android.ui.activities.data.activities -import com.nextcloud.common.NextcloudClient; - -import java.util.List; - -import androidx.annotation.NonNull; +import com.nextcloud.common.NextcloudClient /** * Main entry point for accessing activities data. */ -public interface ActivitiesRepository { +interface ActivitiesRepository { interface LoadActivitiesCallback { - void onActivitiesLoaded(List activities, NextcloudClient client, long lastGiven); - void onActivitiesLoadedError(String error); + fun onActivitiesLoaded(activities: List, client: NextcloudClient, lastGiven: Long) + fun onActivitiesLoadedError(error: String) } - void getActivities(long lastGiven, @NonNull LoadActivitiesCallback callback); + fun getActivities(lastGiven: Long, callback: LoadActivitiesCallback) } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApi.kt b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApi.kt index 870416c81132..2cb054d5ab8f 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApi.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApi.kt @@ -1,28 +1,23 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2018 Edvard Holst * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.activities.data.activities; +package com.owncloud.android.ui.activities.data.activities -import com.nextcloud.common.NextcloudClient; -import com.owncloud.android.lib.resources.activities.model.Activity; - -import java.util.List; +import com.nextcloud.common.NextcloudClient /** - * Defines an interface to the Activities service API. All ({@link Activity}) data requests should + * Defines an interface to the Activities service API. All ([Activity]) data requests should * be piped through this interface. */ - -public interface ActivitiesServiceApi { - +interface ActivitiesServiceApi { interface ActivitiesServiceCallback { - void onLoaded(T activities, NextcloudClient client, long lastGiven); - void onError (String error); + fun onLoaded(activities: T, client: NextcloudClient, lastGiven: Long) + fun onError(error: String) } - void getAllActivities(long lastGiven, ActivitiesServiceApi.ActivitiesServiceCallback> callback); - + fun getAllActivities(lastGiven: Long, callback: ActivitiesServiceCallback>) } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApiImpl.kt b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApiImpl.kt index 0534b6f5dff8..4b6ced59c0cc 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApiImpl.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApiImpl.kt @@ -1,130 +1,87 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2019 Chris Narkiewicz * SPDX-FileCopyrightText: 2018 Edvard Holst * SPDX-FileCopyrightText: 2025 TSI-mc * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.activities.data.activities; - -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.content.Context; -import android.os.AsyncTask; - -import com.nextcloud.client.account.User; -import com.nextcloud.client.account.UserAccountManager; -import com.nextcloud.common.NextcloudClient; -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.lib.common.OwnCloudAccount; -import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.activities.GetActivitiesRemoteOperation; - -import org.apache.commons.httpclient.HttpStatus; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +package com.owncloud.android.ui.activities.data.activities + +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.activities.GetActivitiesRemoteOperation +import com.owncloud.android.ui.activities.data.activities.ActivitiesServiceApi.ActivitiesServiceCallback +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.apache.commons.httpclient.HttpStatus /** * Implementation of the Activities Service API that communicates with the NextCloud remote server */ -public class ActivitiesServiceApiImpl implements ActivitiesServiceApi { - - private static final String TAG = ActivitiesServiceApiImpl.class.getSimpleName(); - private final UserAccountManager accountManager; +@Suppress("TooGenericExceptionThrown") +class ActivitiesServiceApiImpl(private val accountManager: UserAccountManager) : ActivitiesServiceApi { - public ActivitiesServiceApiImpl(UserAccountManager accountManager) { - this.accountManager = accountManager; - } + override fun getAllActivities(lastGiven: Long, callback: ActivitiesServiceCallback>) { + CoroutineScope(Dispatchers.Main).launch { + val result = runCatching { + withContext(Dispatchers.IO) { fetchActivities(lastGiven) } + } - @Override - public void getAllActivities(long lastGiven, ActivitiesServiceCallback> callback) { - GetActivityListTask getActivityListTask = new GetActivityListTask(accountManager.getUser(), - lastGiven, - callback); - getActivityListTask.execute(); + result.fold( + onSuccess = { (activities, client, updatedLastGiven) -> + callback.onLoaded(activities, client, updatedLastGiven) + }, + onFailure = { error -> + Log_OC.e(TAG, "Failed to fetch activities", error) + callback.onError(error.message ?: "") + } + ) + } } - private static class GetActivityListTask extends AsyncTask { + private data class ActivitiesResult(val activities: List, val client: NextcloudClient, val lastGiven: Long) - private final ActivitiesServiceCallback> callback; - private List activities; - private final User user; - private long lastGiven; - private String errorMessage; - private NextcloudClient client; + private fun fetchActivities(lastGiven: Long): ActivitiesResult { + val context = MainApp.getAppContext() + val ocAccount = accountManager.user.toOwnCloudAccount() + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(ocAccount, context) - private GetActivityListTask(User user, - long lastGiven, - ActivitiesServiceCallback> callback) { - this.user = user; - this.lastGiven = lastGiven; - this.callback = callback; - activities = new ArrayList<>(); + val operation = if (lastGiven > 0) { + GetActivitiesRemoteOperation(lastGiven) + } else { + GetActivitiesRemoteOperation() } + val result = operation.execute(client) - @Override - protected Boolean doInBackground(Void... voids) { - final Context context = MainApp.getAppContext(); - try { - final OwnCloudAccount ocAccount = user.toOwnCloudAccount(); - client = OwnCloudClientManagerFactory.getDefaultSingleton(). - getNextcloudClientFor(ocAccount, MainApp.getAppContext()); - - GetActivitiesRemoteOperation getRemoteActivitiesOperation; - if (lastGiven > 0) { - getRemoteActivitiesOperation = new GetActivitiesRemoteOperation(lastGiven); - } else { - getRemoteActivitiesOperation = new GetActivitiesRemoteOperation(); - } - - final RemoteOperationResult result = getRemoteActivitiesOperation.execute(client); - - if (result.isSuccess() && result.getData() != null) { - final ArrayList data = result.getData(); - activities = (ArrayList) data.get(0); - - lastGiven = (long) data.get(1); - return Boolean.TRUE; - } else { - Log_OC.d(TAG, result.getLogMessage()); - // show error - errorMessage = result.getLogMessage(MainApp.getAppContext()); - if (result.getHttpCode() == HttpStatus.SC_NOT_MODIFIED) { - errorMessage = context.getString(R.string.file_list_empty_headline_server_search); - } - return Boolean.FALSE; - - } - } catch (IOException e) { - Log_OC.e(TAG, "IO error", e); - errorMessage = "IO error"; - } catch (OperationCanceledException e) { - Log_OC.e(TAG, "Operation has been canceled", e); - errorMessage = "Operation has been canceled"; - } catch (AuthenticatorException e) { - Log_OC.e(TAG, "Authentication Exception", e); - errorMessage = "Authentication Exception"; - } - - return Boolean.FALSE; + if (result.isSuccess && result.getData() != null) { + val data = result.getData() + val activities = data[0] as List + val updatedLastGiven = data[1] as Long + val items = activities.filterNotNull() + return ActivitiesResult(items, client, updatedLastGiven) } - @Override - protected void onPostExecute(Boolean success) { - super.onPostExecute(success); - if (success) { - callback.onLoaded(activities, client, lastGiven); - } else { - callback.onError(errorMessage); - } + val errorMessage = if (result.httpCode == HttpStatus.SC_NOT_MODIFIED) { + context.getString(R.string.file_list_empty_headline_server_search) + } else { + result.getLogMessage(context) } + + Log_OC.d(TAG, result.logMessage) + throw RuntimeException(errorMessage) + } + + companion object { + private val TAG: String = ActivitiesServiceApiImpl::class.java.simpleName } } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivityRepositories.java b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivityRepositories.java deleted file mode 100644 index b4bfeb520bae..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivityRepositories.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.activities.data.activities; - -import androidx.annotation.NonNull; - -public final class ActivityRepositories { - - private ActivityRepositories() { - // No instance - } - - public static synchronized ActivitiesRepository getRepository(@NonNull ActivitiesServiceApi activitiesServiceApi) { - return new RemoteActivitiesRepository(activitiesServiceApi); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/RemoteActivitiesRepository.kt b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/RemoteActivitiesRepository.kt index 82adde65dc1d..1ab57f80fd83 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/RemoteActivitiesRepository.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/RemoteActivitiesRepository.kt @@ -1,39 +1,29 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2018 Edvard Holst * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.activities.data.activities; - -import com.nextcloud.common.NextcloudClient; - -import java.util.List; - -import androidx.annotation.NonNull; - -public class RemoteActivitiesRepository implements ActivitiesRepository { - - private final ActivitiesServiceApi activitiesServiceApi; - - public RemoteActivitiesRepository(@NonNull ActivitiesServiceApi activitiesServiceApi) { - this.activitiesServiceApi = activitiesServiceApi; - } - - - @Override - public void getActivities(long lastGiven, @NonNull LoadActivitiesCallback callback) { - activitiesServiceApi.getAllActivities(lastGiven, - new ActivitiesServiceApi.ActivitiesServiceCallback<>() { - @Override - public void onLoaded(List activities, NextcloudClient client, long lastGiven) { - callback.onActivitiesLoaded(activities, client, lastGiven); - } - - @Override - public void onError(String error) { - callback.onActivitiesLoadedError(error); - } - }); +package com.owncloud.android.ui.activities.data.activities + +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.ui.activities.data.activities.ActivitiesRepository.LoadActivitiesCallback +import com.owncloud.android.ui.activities.data.activities.ActivitiesServiceApi.ActivitiesServiceCallback + +class RemoteActivitiesRepository(private val activitiesServiceApi: ActivitiesServiceApi) : ActivitiesRepository { + override fun getActivities(lastGiven: Long, callback: LoadActivitiesCallback) { + activitiesServiceApi.getAllActivities( + lastGiven, + object : ActivitiesServiceCallback> { + override fun onLoaded(activities: List, client: NextcloudClient, lastGiven: Long) { + callback.onActivitiesLoaded(activities, client, lastGiven) + } + + override fun onError(error: String) { + callback.onActivitiesLoadedError(error) + } + } + ) } } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FileRepositories.java b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FileRepositories.java deleted file mode 100644 index 37da9e94ea0f..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FileRepositories.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.activities.data.files; - -import androidx.annotation.NonNull; - -public final class FileRepositories { - - private FileRepositories() { - // No instance - } - - public static synchronized FilesRepository getRepository(@NonNull FilesServiceApi filesServiceApi) { - return new RemoteFilesRepository(filesServiceApi); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesRepository.kt b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesRepository.kt index 49300a48c555..30b851d26e99 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesRepository.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesRepository.kt @@ -1,25 +1,20 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2018 Edvard Holst * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.activities.data.files; +package com.owncloud.android.ui.activities.data.files -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.ui.activity.BaseActivity; +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.BaseActivity -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * Main entry point for accessing remote files - */ -public interface FilesRepository { +interface FilesRepository { interface ReadRemoteFileCallback { - void onFileLoaded(@Nullable OCFile ocFile); - void onFileLoadError(String error); + fun onFileLoaded(ocFile: OCFile?) + fun onFileLoadError(error: String) } - void readRemoteFile(String path, BaseActivity activity, @NonNull ReadRemoteFileCallback callback); + fun readRemoteFile(path: String, activity: BaseActivity, callback: ReadRemoteFileCallback) } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApi.kt b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApi.kt index 5d08e5632909..8e11ccd965bb 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApi.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApi.kt @@ -1,24 +1,24 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2018 Edvard Holst * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.activities.data.files; +package com.owncloud.android.ui.activities.data.files -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.ui.activity.BaseActivity; +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.BaseActivity /** - * Defines an interface to the Files service API. All {{@link OCFile}} remote data requests + * Defines an interface to the Files service API. All {[OCFile]} remote data requests * should be piped through this interface. */ -public interface FilesServiceApi { - +interface FilesServiceApi { interface FilesServiceCallback { - void onLoaded(OCFile ocFile); - void onError(String error); + fun onLoaded(ocFile: OCFile) + fun onError(error: String) } - void readRemoteFile(String fileUrl, BaseActivity activity, FilesServiceApi.FilesServiceCallback callback); + fun readRemoteFile(fileUrl: String, activity: BaseActivity, callback: FilesServiceCallback) } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApiImpl.kt b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApiImpl.kt index 8ad140822c95..a699f172583a 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApiImpl.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApiImpl.kt @@ -1,130 +1,92 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2019 Chris Narkiewicz * SPDX-FileCopyrightText: 2018 Edvard Holst * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.activities.data.files; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.AsyncTask; - -import com.nextcloud.client.account.User; -import com.nextcloud.client.account.UserAccountManager; -import com.nextcloud.client.network.ClientFactory; -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.operations.RemoteOperation; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation; -import com.owncloud.android.lib.resources.files.model.RemoteFile; -import com.owncloud.android.operations.RefreshFolderOperation; -import com.owncloud.android.ui.activity.BaseActivity; -import com.owncloud.android.utils.FileStorageUtils; +package com.owncloud.android.ui.activities.data.files + +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.network.ClientFactory +import com.nextcloud.client.network.ClientFactory.CreationException +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation +import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.RefreshFolderOperation +import com.owncloud.android.ui.activities.data.files.FilesServiceApi.FilesServiceCallback +import com.owncloud.android.ui.activity.BaseActivity +import com.owncloud.android.utils.FileStorageUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Implementation of the Files service API that communicates with the NextCloud remote server. */ -public class FilesServiceApiImpl implements FilesServiceApi { - - private static final String TAG = FilesServiceApiImpl.class.getSimpleName(); - - private final UserAccountManager accountManager; - private final ClientFactory clientFactory; - - public FilesServiceApiImpl(UserAccountManager accountManager, ClientFactory clientFactory) { - this.accountManager = accountManager; - this.clientFactory = clientFactory; - } - - @Override - public void readRemoteFile(String fileUrl, BaseActivity activity, FilesServiceCallback callback) { - ReadRemoteFileTask readRemoteFileTask = new ReadRemoteFileTask( - accountManager, - clientFactory, - fileUrl, - activity, - callback - ); - readRemoteFileTask.execute(); - } - - private static class ReadRemoteFileTask extends AsyncTask { - private final FilesServiceCallback callback; - private OCFile remoteOcFile; - private String errorMessage; - // TODO: Figure out a better way to do this than passing a BaseActivity reference. - @SuppressLint("StaticFieldLeak") private final BaseActivity baseActivity; - private final String fileUrl; - private final User user; - private final UserAccountManager accountManager; - private final ClientFactory clientFactory; - - private ReadRemoteFileTask(UserAccountManager accountManager, - ClientFactory clientFactory, - String fileUrl, - BaseActivity baseActivity, - FilesServiceCallback callback) { - this.callback = callback; - this.baseActivity = baseActivity; - this.fileUrl = fileUrl; - this.user = accountManager.getUser(); - this.accountManager = accountManager; - this.clientFactory = clientFactory; - } +class FilesServiceApiImpl(private val accountManager: UserAccountManager, private val clientFactory: ClientFactory) : + FilesServiceApi { - @Override - protected Boolean doInBackground(Void... voids) { - final Context context = MainApp.getAppContext(); - try { - OwnCloudClient ownCloudClient = clientFactory.create(user); - // always update file as it could be an old state saved in database - RemoteOperationResult resultRemoteFileOp = new ReadFileRemoteOperation(fileUrl).execute(ownCloudClient); - - if (resultRemoteFileOp.isSuccess()) { - OCFile temp = FileStorageUtils.fillOCFile((RemoteFile) resultRemoteFileOp.getData().get(0)); - remoteOcFile = baseActivity.getStorageManager().saveFileWithParent(temp, context); + override fun readRemoteFile(fileUrl: String, activity: BaseActivity, callback: FilesServiceCallback) { + CoroutineScope(Dispatchers.Main).launch { + val result = runCatching { + withContext(Dispatchers.IO) { fetchRemoteFile(fileUrl, activity) } + } - if (remoteOcFile.isFolder()) { - // perform folder synchronization - RemoteOperation synchFolderOp = new RefreshFolderOperation(remoteOcFile, - System.currentTimeMillis(), - false, - true, - baseActivity.getStorageManager(), - baseActivity.getUser().orElseThrow(RuntimeException::new), - context); - synchFolderOp.execute(ownCloudClient); + result.fold( + onSuccess = { ocFile -> + if (ocFile != null) { + callback.onLoaded(ocFile) + } else { + callback.onError(activity.getString(R.string.file_not_found)) + } + }, + onFailure = { error -> + val message = when (error) { + is CreationException -> activity.getString(R.string.account_not_found) + else -> error.message } + Log_OC.e(TAG, "Failed to read remote file", error) + callback.onError(message ?: "") } - return Boolean.TRUE; - } catch (ClientFactory.CreationException e) { - Log_OC.e(TAG, "Account not found", e); - errorMessage = baseActivity.getString(R.string.account_not_found); - } - - return Boolean.FALSE; + ) } + } - @Override - protected void onPostExecute(Boolean success) { - super.onPostExecute(success); + private fun fetchRemoteFile(fileUrl: String?, activity: BaseActivity): OCFile? { + val context = MainApp.getAppContext() + val client = clientFactory.create(accountManager.user) + val result = ReadFileRemoteOperation(fileUrl).execute(client) + + if (!result.isSuccess) return null + + val remoteFile = result.getData()[0] as RemoteFile + val ocFile = activity.storageManager.saveFileWithParent( + FileStorageUtils.fillOCFile(remoteFile), + context + ) + + if (ocFile.isFolder) { + RefreshFolderOperation( + ocFile, + System.currentTimeMillis(), + false, + true, + activity.storageManager, + activity.user.orElseThrow { RuntimeException() }, + context + ).execute(client) + } - if (success) { - if (remoteOcFile != null) { - callback.onLoaded(remoteOcFile); - return; - } else { - errorMessage = baseActivity.getString(R.string.file_not_found); - } - } + return ocFile + } - callback.onError(errorMessage); - } + companion object { + private val TAG: String = FilesServiceApiImpl::class.java.simpleName } } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/files/RemoteFilesRepository.kt b/app/src/main/java/com/owncloud/android/ui/activities/data/files/RemoteFilesRepository.kt index 759e0b499b7f..4a9aed32e1ab 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/files/RemoteFilesRepository.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/files/RemoteFilesRepository.kt @@ -1,37 +1,31 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2018 Edvard Holst * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.activities.data.files; - -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.ui.activity.BaseActivity; - -import androidx.annotation.NonNull; - -public class RemoteFilesRepository implements FilesRepository { - - private final FilesServiceApi filesServiceApi; - - public RemoteFilesRepository(@NonNull FilesServiceApi filesServiceApi) { - this.filesServiceApi = filesServiceApi; - } - - - @Override - public void readRemoteFile(String path, BaseActivity activity, @NonNull ReadRemoteFileCallback callback) { - filesServiceApi.readRemoteFile(path, activity, new FilesServiceApi.FilesServiceCallback<>() { - @Override - public void onLoaded(OCFile ocFile) { - callback.onFileLoaded(ocFile); - } - - @Override - public void onError(String error) { - callback.onFileLoadError(error); +package com.owncloud.android.ui.activities.data.files + +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activities.data.files.FilesRepository.ReadRemoteFileCallback +import com.owncloud.android.ui.activities.data.files.FilesServiceApi.FilesServiceCallback +import com.owncloud.android.ui.activity.BaseActivity + +class RemoteFilesRepository(private val filesServiceApi: FilesServiceApi) : FilesRepository { + override fun readRemoteFile(path: String, activity: BaseActivity, callback: ReadRemoteFileCallback) { + filesServiceApi.readRemoteFile( + path, + activity, + object : FilesServiceCallback { + override fun onLoaded(ocFile: OCFile) { + callback.onFileLoaded(ocFile) + } + + override fun onError(error: String) { + callback.onFileLoadError(error) + } } - }); + ) } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java index 9c517765acf4..3e9b88cc4fb6 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java @@ -226,7 +226,7 @@ protected int getMenuItemId() { AppPreferences preferences; @Inject - ClientFactory clientFactory; + protected ClientFactory clientFactory; @Override public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { From f860c5a5252711a59cc9441baef1cb30501c2a43 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 11 Mar 2026 13:36:08 +0100 Subject: [PATCH 3/7] refactor: convert activities Java classes to Kotlin Signed-off-by: alperozturk96 --- .../java/com/nextcloud/client/ActivitiesActivityIT.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt index 1a8292f5a846..cbe58b420ae8 100644 --- a/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt @@ -162,7 +162,7 @@ class ActivitiesActivityIT : AbstractIT() { launchActivity().use { scenario -> scenario.onActivity { sut -> - sut.showActivities(activities as List?, nextcloudClient, -1) + sut.showActivities(activities as List, nextcloudClient, -1) sut.setProgressIndicatorState(false) } From 4cb1daf589a64b78bd0a18179cbc8438d3d60e24 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 11 Mar 2026 14:47:21 +0100 Subject: [PATCH 4/7] Rename .java to .kt Signed-off-by: alperozturk96 --- .../{ActivityListAdapterTest.java => ActivityListAdapterTest.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/test/java/com/owncloud/android/ui/adapter/{ActivityListAdapterTest.java => ActivityListAdapterTest.kt} (100%) diff --git a/app/src/test/java/com/owncloud/android/ui/adapter/ActivityListAdapterTest.java b/app/src/test/java/com/owncloud/android/ui/adapter/ActivityListAdapterTest.kt similarity index 100% rename from app/src/test/java/com/owncloud/android/ui/adapter/ActivityListAdapterTest.java rename to app/src/test/java/com/owncloud/android/ui/adapter/ActivityListAdapterTest.kt From edd1e1dc97e9f0d71777adca6b2812171ac6d8c1 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 11 Mar 2026 14:47:21 +0100 Subject: [PATCH 5/7] convert remaining files Signed-off-by: alperozturk96 --- .../ui/activities/ActivitiesActivity.kt | 65 ++- .../ui/activities/StickyHeaderAdapter.kt | 42 ++ .../activities/StickyHeaderItemDecoration.kt | 76 ++-- .../adapter/ActivityAndVersionListAdapter.kt | 93 ++++ .../activities/adapter/ActivityListAdapter.kt | 322 ++++++++++++++ .../ActivityAndVersionListAdapter.java | 157 ------- .../ui/adapter/ActivityListAdapter.java | 418 ------------------ .../ui/adapter/StickyHeaderAdapter.java | 45 -- .../FileDetailActivitiesFragment.java | 5 +- .../ui/adapter/ActivityListAdapterTest.kt | 131 ++---- 10 files changed, 555 insertions(+), 799 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderAdapter.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityAndVersionListAdapter.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityListAdapter.kt delete mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/ActivityAndVersionListAdapter.java delete mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java delete mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/StickyHeaderAdapter.java diff --git a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.kt b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.kt index b8b10f13b455..446201e5fc58 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.kt @@ -27,7 +27,7 @@ import com.owncloud.android.ui.activities.data.files.FilesRepository import com.owncloud.android.ui.activity.DrawerActivity import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.ui.activity.FileDisplayActivity -import com.owncloud.android.ui.adapter.ActivityListAdapter +import com.owncloud.android.ui.activities.adapter.ActivityListAdapter import com.owncloud.android.ui.interfaces.ActivityListInterface import com.owncloud.android.ui.preview.PreviewImageActivity import com.owncloud.android.ui.preview.PreviewImageFragment.Companion.canBePreviewed @@ -35,6 +35,7 @@ import com.owncloud.android.utils.DisplayUtils import java.util.function.Supplier import javax.inject.Inject +@Suppress("MagicNumber") class ActivitiesActivity : DrawerActivity(), ActivityListInterface, @@ -88,7 +89,6 @@ class ActivitiesActivity : this, userAccountManager, this, - clientFactory, false, viewThemeUtils ) @@ -96,43 +96,42 @@ class ActivitiesActivity : val layoutManager = LinearLayoutManager(this) - binding?.list?.setLayoutManager(layoutManager) - binding?.list?.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(recyclerView, dx, dy) - - val visibleItemCount = recyclerView.size - val totalItemCount = layoutManager.getItemCount() - val firstVisibleItemIndex = layoutManager.findFirstVisibleItemPosition() - - // synchronize loading state when item count changes - if (!isLoadingActivities && (totalItemCount - visibleItemCount) <= (firstVisibleItemIndex + 5) && - lastGiven > 0 - ) { - // Almost reached the end, continue to load new activities - actionListener?.loadActivities(lastGiven) - } - } - }) + binding?.list?.run { + setLayoutManager(layoutManager) + addOnScrollListener(getOnScrollListener(layoutManager)) + } actionListener?.loadActivities(ActivitiesContract.ActionListener.UNDEFINED.toLong()) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - var retval = true + private fun getOnScrollListener(layoutManager: LinearLayoutManager) = object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) - if (item.itemId == android.R.id.home) { - if (isDrawerOpen) { - closeDrawer() - } else { - openDrawer() + val visibleItemCount = recyclerView.size + val totalItemCount = layoutManager.getItemCount() + val firstVisibleItemIndex = layoutManager.findFirstVisibleItemPosition() + + // synchronize loading state when item count changes + if (!isLoadingActivities && (totalItemCount - visibleItemCount) <= (firstVisibleItemIndex + 5) && + lastGiven > 0 + ) { + // Almost reached the end, continue to load new activities + actionListener?.loadActivities(lastGiven) } - } else { - Log_OC.w(TAG, "Unknown menu item triggered") - retval = super.onOptionsItemSelected(item) } + } - return retval + override fun onOptionsItemSelected(item: MenuItem): Boolean = if (item.itemId == android.R.id.home) { + if (isDrawerOpen) { + closeDrawer() + } else { + openDrawer() + } + true + } else { + Log_OC.w(TAG, "Unknown menu item triggered") + super.onOptionsItemSelected(item) } override fun onResume() { @@ -153,7 +152,7 @@ class ActivitiesActivity : this.lastGiven = lastGiven // Hide the recyclerView if list is empty - if (adapter?.isEmpty == true) { + if (adapter?.isEmpty() == true) { showEmptyContent( getString(R.string.activities_no_results_headline), getString(R.string.activities_no_results_message) @@ -223,7 +222,7 @@ class ActivitiesActivity : override fun setProgressIndicatorState(isActive: Boolean) { isLoadingActivities = isActive - if (adapter?.isEmpty == false) { + if (adapter?.isEmpty() == false) { binding?.swipeContainingList?.post { binding?.swipeContainingList?.isRefreshing = isActive } } } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderAdapter.kt b/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderAdapter.kt new file mode 100644 index 000000000000..02daa619d8d9 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderAdapter.kt @@ -0,0 +1,42 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activities + +import android.view.View + +interface StickyHeaderAdapter { + /** + * This method gets called by [StickyHeaderItemDecoration] to fetch the position of the header item in the adapter + * that is used for (represents) item at specified position. + * @param itemPosition int. Adapter's position of the item for which to do the search of the position of the header + * item. + * @return int. Position of the header item in the adapter. + */ + fun getHeaderPositionForItem(itemPosition: Int): Int + + /** + * This method gets called by [StickyHeaderItemDecoration] to get layout resource id for the header item at + * specified adapter's position. + * @param headerPosition int. Position of the header item in the adapter. + * @return int. Layout resource id. + */ + fun getHeaderLayout(headerPosition: Int): Int + + /** + * This method gets called by [StickyHeaderItemDecoration] to setup the header View. + * @param header View. Header to set the data on. + * @param headerPosition int. Position of the header item in the adapter. + */ + fun bindHeaderData(header: View?, headerPosition: Int) + + /** + * This method gets called by [StickyHeaderItemDecoration] to verify whether the item represents a header. + * @param itemPosition int. + * @return true, if item at the specified adapter's position represents a header. + */ + fun isHeader(itemPosition: Int): Boolean +} diff --git a/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.kt b/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.kt index 00d374d6a03b..17616607889d 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.kt @@ -1,10 +1,8 @@ /* - * Nextcloud Android client application + * Nextcloud - Android Client * - * Copyright (C) 2019 Sevastyan Savanyuk - * Copyright (C) 2019 Nextcloud GmbH - * - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later */ package com.owncloud.android.ui.activities @@ -13,79 +11,61 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.ItemDecoration -import com.owncloud.android.ui.adapter.StickyHeaderAdapter import androidx.core.graphics.withTranslation -import androidx.core.view.size @Suppress("ReturnCount") -class StickyHeaderItemDecoration(private val adapter: StickyHeaderAdapter) : ItemDecoration() { +class StickyHeaderItemDecoration(private val adapter: StickyHeaderAdapter) : RecyclerView.ItemDecoration() { + override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { super.onDrawOver(canvas, parent, state) val topChild = parent.getChildAt(0) ?: return - val topChildPosition = parent.getChildAdapterPosition(topChild) + val position = parent.getChildAdapterPosition(topChild) + if (position == RecyclerView.NO_POSITION) return - if (topChildPosition == RecyclerView.NO_POSITION) { - return - } - val currentHeader = getHeaderViewForItem(topChildPosition, parent) - fixLayoutSize(parent, currentHeader) - val contactPoint = currentHeader.bottom + val header = getHeaderViewForItem(position, parent) + fixLayoutSize(parent, header) + + val contactPoint = header.bottom val childInContact = getChildInContact(parent, contactPoint) ?: return if (adapter.isHeader(parent.getChildAdapterPosition(childInContact))) { - moveHeader(canvas, currentHeader, childInContact) - return + moveHeader(canvas, header, childInContact) + } else { + drawHeader(canvas, header) } - - drawHeader(canvas, currentHeader) } - private fun drawHeader(canvas: Canvas, header: View) { - canvas.withTranslation(0f, 0f) { - header.draw(this) - } - } + private fun drawHeader(canvas: Canvas, header: View) = canvas.withTranslation(0f, 0f) { header.draw(this) } - private fun moveHeader(canvas: Canvas, currentHeader: View, nextHeader: View) { + private fun moveHeader(canvas: Canvas, currentHeader: View, nextHeader: View) = canvas.withTranslation(0f, (nextHeader.top - currentHeader.height).toFloat()) { currentHeader.draw(this) } - } - private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? { - var childInContact: View? = null - for (i in 0.. contactPoint && currentChild.top <= contactPoint) { - childInContact = currentChild - break - } - } - return childInContact - } + private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? = (0 until parent.childCount) + .map { parent.getChildAt(it) } + .firstOrNull { it.bottom > contactPoint && it.top <= contactPoint } - private fun getHeaderViewForItem(itemPosition: Int, parent: RecyclerView): View { - val headerPosition = adapter.getHeaderPositionForItem(itemPosition) - val layoutId = adapter.getHeaderLayout(itemPosition) - val header = LayoutInflater.from(parent.context).inflate(layoutId, parent, false) - adapter.bindHeaderData(header, headerPosition) - return header + private fun getHeaderViewForItem(position: Int, parent: RecyclerView): View { + val headerPos = adapter.getHeaderPositionForItem(position) + val layoutId = adapter.getHeaderLayout(position) + + return LayoutInflater.from(parent.context) + .inflate(layoutId, parent, false) + .apply { adapter.bindHeaderData(this, headerPos) } } private fun fixLayoutSize(parent: ViewGroup, view: View) { - // Specs for parent (RecyclerView) - val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY) val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED) - // Specs for children (headers) val childWidthSpec = ViewGroup.getChildMeasureSpec( widthSpec, - parent.getPaddingLeft() + parent.getPaddingRight(), + parent.paddingLeft + parent.paddingRight, view.layoutParams.width ) + val childHeightSpec = ViewGroup.getChildMeasureSpec( heightSpec, parent.paddingTop + parent.paddingBottom, diff --git a/app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityAndVersionListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityAndVersionListAdapter.kt new file mode 100644 index 000000000000..cd2c24c65ce3 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityAndVersionListAdapter.kt @@ -0,0 +1,93 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activities.adapter + +import android.annotation.SuppressLint +import android.text.format.DateFormat +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.fragment.app.FragmentActivity +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.client.account.CurrentAccountProvider +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.databinding.VersionListItemBinding +import com.owncloud.android.lib.resources.activities.model.Activity +import com.owncloud.android.lib.resources.files.model.FileVersion +import com.owncloud.android.ui.interfaces.ActivityListInterface +import com.owncloud.android.ui.interfaces.VersionListInterface +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import java.util.Date + +class ActivityAndVersionListAdapter( + context: FragmentActivity, + currentAccountProvider: CurrentAccountProvider, + activityListInterface: ActivityListInterface, + private val versionListInterface: VersionListInterface.View, + viewThemeUtils: ViewThemeUtils +) : ActivityListAdapter(context, currentAccountProvider, activityListInterface, true, viewThemeUtils) { + + @SuppressLint("NotifyDataSetChanged") + fun setActivityAndVersionItems(items: MutableList, newClient: NextcloudClient?, clear: Boolean) { + if (client == null) client = newClient + + if (clear) { + values.clear() + items.sortByDescending { it.timestamp() } + } + + var sTime = "" + for (item in items) { + val time = getHeaderDateString(context, item.timestamp() ?: continue).toString() + if (!sTime.equals(time, ignoreCase = true)) { + sTime = time + values.add(sTime) + } + if (item != null) { + values.add(item) + } + } + + notifyDataSetChanged() + } + + private fun Any?.timestamp(): Long? = when (this) { + is Activity -> datetime.time + is FileVersion -> modifiedTimestamp + else -> null + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + if (viewType == VERSION_TYPE) { + return VersionViewHolder(VersionListItemBinding.inflate(LayoutInflater.from(parent.context))) + } + return super.onCreateViewHolder(parent, viewType) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is VersionViewHolder) { + val fileVersion = values[position] as FileVersion + holder.binding.size.text = DisplayUtils.bytesToHumanReadable(fileVersion.fileLength) + holder.binding.time.text = DateFormat.format("HH:mm", Date(fileVersion.modifiedTimestamp).time) + holder.binding.restore.setOnClickListener { versionListInterface.onRestoreClicked(fileVersion) } + } else { + super.onBindViewHolder(holder, position) + } + } + + override fun getItemViewType(position: Int) = when (values[position]) { + is Activity -> ACTIVITY_TYPE + is FileVersion -> VERSION_TYPE + else -> HEADER_TYPE + } + + open class VersionViewHolder(val binding: VersionListItemBinding) : RecyclerView.ViewHolder(binding.root) + + companion object { + private const val VERSION_TYPE = 102 + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityListAdapter.kt new file mode 100644 index 000000000000..eeccbc46c965 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityListAdapter.kt @@ -0,0 +1,322 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activities.adapter + +import android.content.Context +import android.content.res.Configuration +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.Typeface +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.TextPaint +import android.text.format.DateFormat +import android.text.format.DateUtils +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.client.account.CurrentAccountProvider +import com.nextcloud.common.NextcloudClient +import com.nextcloud.utils.GlideHelper +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.databinding.ActivityListItemBinding +import com.owncloud.android.databinding.ActivityListItemHeaderBinding +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.activities.model.Activity +import com.owncloud.android.lib.resources.activities.model.RichElement +import com.owncloud.android.lib.resources.activities.model.RichObject +import com.owncloud.android.lib.resources.activities.models.PreviewObject +import com.owncloud.android.ui.activities.StickyHeaderAdapter +import com.owncloud.android.ui.interfaces.ActivityListInterface +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.Locale +import kotlin.math.floor +import kotlin.math.log +import kotlin.math.pow + +@Suppress("MagicNumber") +open class ActivityListAdapter( + protected val context: FragmentActivity, + private val currentAccountProvider: CurrentAccountProvider, + private val activityListInterface: ActivityListInterface, + private val isDetailView: Boolean, + private val viewThemeUtils: ViewThemeUtils +) : RecyclerView.Adapter(), + StickyHeaderAdapter { + + protected var client: NextcloudClient? = null + val values: MutableList = mutableListOf() + private val px = getThumbnailDimension() + + @Suppress("NotifyDataSetChanged") + fun setActivityItems(activityItems: List, client: NextcloudClient, clear: Boolean) { + this.client = client + if (clear) values.clear() + + var sTime = "" + for (o in activityItems) { + val activity = o as Activity + val time = getHeaderDateString(context, activity.datetime.time).toString() + if (!sTime.equals(time, ignoreCase = true)) { + sTime = time + values.add(sTime) + } + values.add(activity) + } + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return if (viewType == ACTIVITY_TYPE) { + ActivityViewHolder(ActivityListItemBinding.inflate(inflater, parent, false)) + } else { + ActivityViewHeaderHolder(ActivityListItemHeaderBinding.inflate(inflater, parent, false)) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is ActivityViewHolder -> bindActivityViewHolder(holder, position) + is ActivityViewHeaderHolder -> holder.binding.header.text = values[position] as String + } + } + + @Suppress("LongMethod") + private fun bindActivityViewHolder(holder: ActivityViewHolder, position: Int) { + val activity = values[position] as Activity + + holder.binding.datetime.apply { + visibility = View.VISIBLE + text = DateFormat.format("HH:mm", activity.datetime.time) + } + + when { + activity.richSubjectElement.richSubject.isNotEmpty() -> holder.binding.subject.apply { + visibility = View.VISIBLE + movementMethod = LinkMovementMethod.getInstance() + setText(addClickablePart(activity.richSubjectElement), TextView.BufferType.SPANNABLE) + } + + activity.subject.isNotEmpty() -> holder.binding.subject.apply { + visibility = View.VISIBLE + text = activity.subject + } + + else -> holder.binding.subject.visibility = View.GONE + } + + holder.binding.message.apply { + text = activity.message + visibility = if (activity.message.isNotEmpty()) View.VISIBLE else View.GONE + } + + if (activity.icon.isNotEmpty()) { + loadImageAsync(activity.icon, holder.binding.icon, R.drawable.ic_activity) + } + + val isNightMode = + (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == + Configuration.UI_MODE_NIGHT_YES + val isFileCreatedOrDeleted = activity.type.equals("file_created", ignoreCase = true) || + activity.type.equals("file_deleted", ignoreCase = true) + + if (!isFileCreatedOrDeleted) { + holder.binding.icon.setColorFilter( + if (isNightMode) Color.WHITE else Color.BLACK, + PorterDuff.Mode.SRC_IN + ) + } + + val richObjectList = activity.richSubjectElement.richObjectList + if (richObjectList.isNotEmpty()) { + holder.binding.list.apply { + visibility = View.VISIBLE + removeAllViews() + post { + val totalColumnCount = measuredWidth / (px + 20) + try { + columnCount = totalColumnCount + } catch (e: IllegalArgumentException) { + Log_OC.e(TAG, "error setting column count to $totalColumnCount") + } + } + activity.previews + .filter { + !isDetailView || MimeTypeUtil.isImageOrVideo(it.mimeType) || + MimeTypeUtil.isVideo(it.mimeType) + } + .forEach { addView(createThumbnail(it, richObjectList)) } + } + } else { + holder.binding.list.apply { + removeAllViews() + visibility = View.GONE + } + } + } + + private fun loadImageAsync(url: String, imageView: ImageView, @DrawableRes placeholder: Int) { + context.lifecycleScope.launch { + runCatching { + val client = withContext(Dispatchers.IO) { + OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(currentAccountProvider.user.toOwnCloudAccount(), context) + } + GlideHelper.loadIntoImageView(context, client, url, imageView, placeholder, false) + }.onFailure { + Log_OC.e(TAG, "Exception loading image: $it") + } + } + } + + private fun createThumbnail(previewObject: PreviewObject, richObjectList: List): ImageView { + val imageView = ImageView(context).apply { + layoutParams = LinearLayout.LayoutParams(px, px).apply { setMargins(10, 10, 10, 10) } + } + + richObjectList.firstOrNull { it.id?.toIntOrNull() == previewObject.fileId }?.let { richObject -> + imageView.setOnClickListener { activityListInterface.onActivityClicked(richObject) } + } + + when { + MimeTypeUtil.isImageOrVideo(previewObject.mimeType) -> { + val placeholder = + if (MimeTypeUtil.isImage(previewObject.mimeType)) R.drawable.file_image else R.drawable.file_movie + previewObject.source?.let { loadImageAsync(it, imageView, placeholder) } + } + + MimeTypeUtil.isFolder(previewObject.mimeType) -> + imageView.setImageDrawable(MimeTypeUtil.getDefaultFolderIcon(context, viewThemeUtils)) + + else -> + imageView.setImageDrawable( + MimeTypeUtil.getFileTypeIcon( + previewObject.mimeType, + "", + context, + viewThemeUtils + ) + ) + } + + return imageView + } + + private fun addClickablePart(richElement: RichElement): SpannableStringBuilder { + var text = richElement.richSubject + val ssb = SpannableStringBuilder(text) + + var idx1 = text.indexOf('{') + while (idx1 != -1) { + var idx2 = text.indexOf('}', idx1) + 1 + val richObject = richElement.richObjectList.firstOrNull { + it.tag.equals(text.substring(idx1 + 1, idx2 - 1), ignoreCase = true) + } + + if (richObject != null) { + val name = richObject.name.orEmpty() + ssb.replace(idx1, idx2, name) + text = ssb.toString() + idx2 = idx1 + name.length + + ssb.setSpan( + object : ClickableSpan() { + override fun onClick(widget: View) = activityListInterface.onActivityClicked(richObject) + override fun updateDrawState(ds: TextPaint) { + ds.isUnderlineText = false + } + }, + idx1, + idx2, + 0 + ) + ssb.setSpan(StyleSpan(Typeface.BOLD), idx1, idx2, 0) + ssb.setSpan( + ForegroundColorSpan(context.resources.getColor(R.color.text_color)), + idx1, + idx2, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + idx1 = text.indexOf('{', idx2) + } + + return ssb + } + + override fun getItemViewType(position: Int) = if (values[position] is Activity) ACTIVITY_TYPE else HEADER_TYPE + + override fun getItemCount() = values.size + + fun isEmpty() = values.isEmpty() + + private fun getThumbnailDimension(): Int { + val dimension = MainApp.getAppContext().resources.getDimension(R.dimen.file_icon_size_grid) + return (2.0.pow(floor(log(dimension.toDouble(), 2.0))) / 2).toInt() + } + + fun getHeaderDateString(context: Context, modificationTimestamp: Long): CharSequence = + if ((System.currentTimeMillis() - modificationTimestamp) < DateUtils.WEEK_IN_MILLIS) { + DisplayUtils.getRelativeDateTimeString( + context, + modificationTimestamp, + DateUtils.DAY_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + 0 + ) + } else { + DateFormat.format( + DateFormat.getBestDateTimePattern(Locale.getDefault(), "EEEE, MMMM d"), + modificationTimestamp + ) + } + + override fun getHeaderPositionForItem(itemPosition: Int): Int { + var pos = itemPosition + while (pos >= 0 && !isHeader(pos)) pos-- + return pos + } + + override fun getHeaderLayout(headerPosition: Int) = R.layout.activity_list_item_header + + override fun bindHeaderData(header: View?, headerPosition: Int) { + header?.findViewById(R.id.header)?.text = values[headerPosition] as String + } + + override fun isHeader(itemPosition: Int) = getItemViewType(itemPosition) == HEADER_TYPE + + protected class ActivityViewHolder(val binding: ActivityListItemBinding) : + RecyclerView.ViewHolder(binding.root) + + protected class ActivityViewHeaderHolder(val binding: ActivityListItemHeaderBinding) : + RecyclerView.ViewHolder(binding.root) + + companion object { + const val HEADER_TYPE = 100 + const val ACTIVITY_TYPE = 101 + private val TAG: String = ActivityListAdapter::class.java.simpleName + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityAndVersionListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityAndVersionListAdapter.java deleted file mode 100644 index 51c00db8126c..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityAndVersionListAdapter.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Andy Scherzinger - * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-FileCopyrightText: 2019 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.adapter; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.text.format.DateFormat; -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import com.nextcloud.client.account.CurrentAccountProvider; -import com.nextcloud.client.network.ClientFactory; -import com.nextcloud.common.NextcloudClient; -import com.owncloud.android.databinding.VersionListItemBinding; -import com.owncloud.android.lib.resources.activities.model.Activity; -import com.owncloud.android.lib.resources.files.model.FileVersion; -import com.owncloud.android.ui.interfaces.ActivityListInterface; -import com.owncloud.android.ui.interfaces.VersionListInterface; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -public class ActivityAndVersionListAdapter extends ActivityListAdapter { - private static final int VERSION_TYPE = 102; - - private final VersionListInterface.View versionListInterface; - - public ActivityAndVersionListAdapter( - Context context, - CurrentAccountProvider currentAccountProvider, - ActivityListInterface activityListInterface, - VersionListInterface.View versionListInterface, - ClientFactory clientFactory, - ViewThemeUtils viewThemeUtils - ) { - super(context, - currentAccountProvider, - activityListInterface, - clientFactory, - true, - viewThemeUtils); - - this.versionListInterface = versionListInterface; - } - - @SuppressLint("NotifyDataSetChanged") - public void setActivityAndVersionItems(List items, NextcloudClient newClient, boolean clear) { - if (client == null) { - client = newClient; - } - if (clear) { - values.clear(); - items.sort((o1, o2) -> { - long o1Date; - long o2Date; - if (o1 instanceof Activity) { - o1Date = ((Activity) o1).getDatetime().getTime(); - } else { - o1Date = ((FileVersion) o1).getModifiedTimestamp(); - } - - if (o2 instanceof Activity) { - o2Date = ((Activity) o2).getDatetime().getTime(); - } else { - o2Date = ((FileVersion) o2).getModifiedTimestamp(); - } - - return -1 * Long.compare(o1Date, o2Date); - }); - } - - String sTime = ""; - for (Object item : items) { - String time; - - if (item instanceof Activity activity) { - time = getHeaderDateString(context, activity.getDatetime().getTime()).toString(); - } else { - FileVersion version = (FileVersion) item; - time = getHeaderDateString(context, version.getModifiedTimestamp()).toString(); - } - - if (sTime.equalsIgnoreCase(time)) { - values.add(item); - } else { - sTime = time; - values.add(sTime); - values.add(item); - } - } - - notifyDataSetChanged(); - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - if (viewType == VERSION_TYPE) { - return new VersionViewHolder(VersionListItemBinding.inflate(LayoutInflater.from(parent.getContext()))); - } - - return super.onCreateViewHolder(parent, viewType); - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - if (holder instanceof VersionViewHolder versionViewHolder) { - FileVersion fileVersion = (FileVersion) values.get(position); - - versionViewHolder.binding.size.setText(DisplayUtils.bytesToHumanReadable(fileVersion.getFileLength())); - versionViewHolder.binding.time.setText( - DateFormat.format("HH:mm", new Date(fileVersion.getModifiedTimestamp()).getTime()) - ); - - versionViewHolder.binding.restore.setOnClickListener( - v -> versionListInterface.onRestoreClicked(fileVersion) - ); - } else { - super.onBindViewHolder(holder, position); - } - } - - @Override - public int getItemViewType(int position) { - Object value = values.get(position); - - if (value instanceof Activity) { - return ACTIVITY_TYPE; - } else if (value instanceof FileVersion) { - return VERSION_TYPE; - } else { - return HEADER_TYPE; - } - } - - protected class VersionViewHolder extends RecyclerView.ViewHolder { - - VersionListItemBinding binding; - - VersionViewHolder(VersionListItemBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java deleted file mode 100644 index d8246088ae81..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java +++ /dev/null @@ -1,418 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Andy Scherzinger - * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-FileCopyrightText: 2019 Alex Plutta - * SPDX-FileCopyrightText: 2018 Tobias Kaminsky - * SPDX-FileCopyrightText: 2017 Alejandro Morales - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.adapter; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.os.Handler; -import android.os.Looper; -import android.text.Spannable; -import android.text.SpannableStringBuilder; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.format.DateFormat; -import android.text.format.DateUtils; -import android.text.method.LinkMovementMethod; -import android.text.style.ClickableSpan; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.nextcloud.client.account.CurrentAccountProvider; -import com.nextcloud.client.network.ClientFactory; -import com.nextcloud.common.NextcloudClient; -import com.nextcloud.utils.GlideHelper; -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.databinding.ActivityListItemBinding; -import com.owncloud.android.databinding.ActivityListItemHeaderBinding; -import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.activities.model.Activity; -import com.owncloud.android.lib.resources.activities.model.RichElement; -import com.owncloud.android.lib.resources.activities.model.RichObject; -import com.owncloud.android.lib.resources.activities.models.PreviewObject; -import com.owncloud.android.ui.interfaces.ActivityListInterface; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.MimeTypeUtil; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Optional; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -/** - * Adapter for the activity view. - */ -public class ActivityListAdapter extends RecyclerView.Adapter implements StickyHeaderAdapter { - - static final int HEADER_TYPE = 100; - static final int ACTIVITY_TYPE = 101; - private final ActivityListInterface activityListInterface; - private final int px; - private static final String TAG = ActivityListAdapter.class.getSimpleName(); - protected NextcloudClient client; - - protected Context context; - private CurrentAccountProvider currentAccountProvider; - private ClientFactory clientFactory; - protected List values; - private boolean isDetailView; - private ViewThemeUtils viewThemeUtils; - private final Handler handler = new Handler(Looper.getMainLooper()); - - public ActivityListAdapter( - Context context, - CurrentAccountProvider currentAccountProvider, - ActivityListInterface activityListInterface, - ClientFactory clientFactory, - boolean isDetailView, - ViewThemeUtils viewThemeUtils) { - this.values = new ArrayList<>(); - this.context = context; - this.currentAccountProvider = currentAccountProvider; - this.activityListInterface = activityListInterface; - this.clientFactory = clientFactory; - px = getThumbnailDimension(); - this.isDetailView = isDetailView; - this.viewThemeUtils = viewThemeUtils; - } - - @SuppressLint("NotifyDataSetChanged") - public void setActivityItems(List activityItems, NextcloudClient client, boolean clear) { - this.client = client; - String sTime = ""; - - if (clear) { - values.clear(); - } - - for (Object o : activityItems) { - Activity activity = (Activity) o; - String time; - time = getHeaderDateString(context, activity.getDatetime().getTime()).toString(); - - if (sTime.equalsIgnoreCase(time)) { - values.add(activity); - } else { - sTime = time; - values.add(sTime); - values.add(activity); - } - } - notifyDataSetChanged(); - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - if (viewType == ACTIVITY_TYPE) { - return new ActivityViewHolder( - ActivityListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) - ); - } else { - return new ActivityViewHeaderHolder( - ActivityListItemHeaderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) - ); - } - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - if (holder instanceof ActivityViewHolder activityViewHolder) { - Activity activity = (Activity) values.get(position); - activityViewHolder.binding.datetime.setVisibility(View.VISIBLE); - activityViewHolder.binding.datetime.setText(DateFormat.format("HH:mm", activity.getDatetime().getTime())); - - if (!TextUtils.isEmpty(activity.getRichSubjectElement().getRichSubject())) { - activityViewHolder.binding.subject.setVisibility(View.VISIBLE); - activityViewHolder.binding.subject.setMovementMethod(LinkMovementMethod.getInstance()); - activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()), - TextView.BufferType.SPANNABLE); - activityViewHolder.binding.subject.setVisibility(View.VISIBLE); - } else if (!TextUtils.isEmpty(activity.getSubject())) { - activityViewHolder.binding.subject.setVisibility(View.VISIBLE); - activityViewHolder.binding.subject.setText(activity.getSubject()); - } else { - activityViewHolder.binding.subject.setVisibility(View.GONE); - } - - if (!TextUtils.isEmpty(activity.getMessage())) { - activityViewHolder.binding.message.setText(activity.getMessage()); - activityViewHolder.binding.message.setVisibility(View.VISIBLE); - } else { - activityViewHolder.binding.message.setVisibility(View.GONE); - } - - if (!TextUtils.isEmpty(activity.getIcon())) { - new Thread(() -> {{ - try { - final var client = OwnCloudClientManagerFactory.getDefaultSingleton().getNextcloudClientFor(Optional.of(currentAccountProvider.getUser().toOwnCloudAccount()).get(), context); - handler.post(() -> GlideHelper.INSTANCE.loadIntoImageView(context, client, activity.getIcon(), activityViewHolder.binding.icon, R.drawable.ic_activity, false)); - } catch (Exception e) { - Log_OC.e("TemplateAdapter", "Exception setData: " + e); - } - }}).start(); - } - - int nightModeFlag = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - - if (!"file_created".equalsIgnoreCase(activity.getType()) && - !"file_deleted".equalsIgnoreCase(activity.getType())) { - if (Configuration.UI_MODE_NIGHT_YES == nightModeFlag) { - activityViewHolder.binding.icon.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN); - } else { - activityViewHolder.binding.icon.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN); - } - } - - - if (!activity.getRichSubjectElement().getRichObjectList().isEmpty()) { - activityViewHolder.binding.list.setVisibility(View.VISIBLE); - activityViewHolder.binding.list.removeAllViews(); - - activityViewHolder.binding.list.post(() -> { - int w = activityViewHolder.binding.list.getMeasuredWidth(); - int elPxSize = px + 20; - int totalColumnCount = w / elPxSize; - - try { - activityViewHolder.binding.list.setColumnCount(totalColumnCount); - } catch (IllegalArgumentException e) { - Log_OC.e(TAG, "error setting column count to " + totalColumnCount); - } - }); - - for (PreviewObject previewObject : activity.getPreviews()) { - if (!isDetailView || MimeTypeUtil.isImageOrVideo(previewObject.getMimeType()) || - MimeTypeUtil.isVideo(previewObject.getMimeType())) { - ImageView imageView = createThumbnailNew(previewObject, - activity - .getRichSubjectElement() - .getRichObjectList()); - activityViewHolder.binding.list.addView(imageView); - } - } - } else { - activityViewHolder.binding.list.removeAllViews(); - activityViewHolder.binding.list.setVisibility(View.GONE); - } - } else { - ActivityViewHeaderHolder activityViewHeaderHolder = (ActivityViewHeaderHolder) holder; - activityViewHeaderHolder.binding.header.setText((String) values.get(position)); - } - } - - private ImageView createThumbnailNew(PreviewObject previewObject, List richObjectList) { - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(px, px); - params.setMargins(10, 10, 10, 10); - ImageView imageView = new ImageView(context); - imageView.setLayoutParams(params); - - for (RichObject object : richObjectList) { - int objectId = -1; - try { - objectId = Integer.parseInt(object.getId()); - } catch (NumberFormatException e) { - // object.getId() can also be a string if RichObjects refers to an user - } - if (objectId == previewObject.getFileId()) { - imageView.setOnClickListener(v -> activityListInterface.onActivityClicked(object)); - break; - } - } - - if (MimeTypeUtil.isImageOrVideo(previewObject.getMimeType())) { - int placeholder; - if (MimeTypeUtil.isImage(previewObject.getMimeType())) { - placeholder = R.drawable.file_image; - } else { - placeholder = R.drawable.file_movie; - } - - if (previewObject.getSource() != null) { - new Thread(() -> {{ - try { - final var client = OwnCloudClientManagerFactory.getDefaultSingleton().getNextcloudClientFor(Optional.of(currentAccountProvider.getUser().toOwnCloudAccount()).get(), context); - handler.post(() -> GlideHelper.INSTANCE.loadIntoImageView(context, client, previewObject.getSource(), imageView, placeholder, false)); - } catch (Exception e) { - Log_OC.e("TemplateAdapter", "Exception setData: " + e); - } - }}).start(); - } - } else { - if (MimeTypeUtil.isFolder(previewObject.getMimeType())) { - imageView.setImageDrawable(MimeTypeUtil.getDefaultFolderIcon(context, viewThemeUtils)); - } else { - imageView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(previewObject.getMimeType(), - "", - context, - viewThemeUtils)); - } - } - - return imageView; - } - - private SpannableStringBuilder addClickablePart(RichElement richElement) { - String text = richElement.getRichSubject(); - SpannableStringBuilder ssb = new SpannableStringBuilder(text); - - int idx1 = text.indexOf('{'); - int idx2; - while (idx1 != -1) { - idx2 = text.indexOf('}', idx1) + 1; - final String clickString = text.substring(idx1 + 1, idx2 - 1); - final RichObject richObject = searchObjectByName(richElement.getRichObjectList(), clickString); - if (richObject != null) { - String name = richObject.getName(); - ssb.replace(idx1, idx2, name); - text = ssb.toString(); - idx2 = idx1 + name.length(); - ssb.setSpan(new ClickableSpan() { - @Override - public void onClick(@NonNull View widget) { - activityListInterface.onActivityClicked(richObject); - } - - @Override - public void updateDrawState(@NonNull TextPaint ds) { - ds.setUnderlineText(false); - } - }, idx1, idx2, 0); - ssb.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), idx1, idx2, 0); - ssb.setSpan( - new ForegroundColorSpan(context.getResources().getColor(R.color.text_color)), - idx1, - idx2, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ); - } - idx1 = text.indexOf('{', idx2); - } - - return ssb; - } - - private RichObject searchObjectByName(List richObjectList, String name) { - for (RichObject richObject : richObjectList) { - if (richObject.getTag().equalsIgnoreCase(name)) { - return richObject; - } - } - return null; - } - - - @Override - public int getItemViewType(int position) { - if (values.get(position) instanceof Activity) { - return ACTIVITY_TYPE; - } else { - return HEADER_TYPE; - } - } - - @Override - public int getItemCount() { - return values.size(); - } - - public boolean isEmpty() { - return values.isEmpty(); - } - - /** - * Converts size of file icon from dp to pixel - * - * @return int - */ - private int getThumbnailDimension() { - // Converts dp to pixel - Resources r = MainApp.getAppContext().getResources(); - double d = Math.pow(2, Math.floor(Math.log(r.getDimension(R.dimen.file_icon_size_grid)) / Math.log(2))) / 2; - return (int) d; - } - - CharSequence getHeaderDateString(Context context, long modificationTimestamp) { - if ((System.currentTimeMillis() - modificationTimestamp) < DateUtils.WEEK_IN_MILLIS) { - return DisplayUtils.getRelativeDateTimeString(context, modificationTimestamp, DateUtils.DAY_IN_MILLIS, - DateUtils.WEEK_IN_MILLIS, 0); - } else { - return DateFormat.format(DateFormat.getBestDateTimePattern( - Locale.getDefault(), "EEEE, MMMM d"), modificationTimestamp); - } - } - - - @Override - public int getHeaderPositionForItem(int itemPosition) { - int headerPosition = itemPosition; - while (headerPosition >= 0) { - if (this.isHeader(headerPosition)) { - break; - } - headerPosition -= 1; - } - return headerPosition; - } - - - @Override - public int getHeaderLayout(int headerPosition) { - return R.layout.activity_list_item_header; - } - - @Override - public void bindHeaderData(View header, int headerPosition) { - TextView textView = header.findViewById(R.id.header); - String headline = (String) values.get(headerPosition); - textView.setText(headline); - } - - @Override - public boolean isHeader(int itemPosition) { - return this.getItemViewType(itemPosition) == HEADER_TYPE; - } - - protected class ActivityViewHolder extends RecyclerView.ViewHolder { - - ActivityListItemBinding binding; - - ActivityViewHolder(ActivityListItemBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } - - protected class ActivityViewHeaderHolder extends RecyclerView.ViewHolder { - - ActivityListItemHeaderBinding binding; - - ActivityViewHeaderHolder(ActivityListItemHeaderBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/StickyHeaderAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/StickyHeaderAdapter.java deleted file mode 100644 index bd02cbcf8c82..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/adapter/StickyHeaderAdapter.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Nextcloud Android client application - * - - * Copyright (C) 2019 Sevastyan Savanyuk - * Copyright (C) 2019 Nextcloud GmbH - * - *SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.adapter; - -import android.view.View; - -import com.owncloud.android.ui.activities.StickyHeaderItemDecoration; - -public interface StickyHeaderAdapter { - /** - * This method gets called by {@link StickyHeaderItemDecoration} to fetch the position of the header item in the adapter - * that is used for (represents) item at specified position. - * @param itemPosition int. Adapter's position of the item for which to do the search of the position of the header item. - * @return int. Position of the header item in the adapter. - */ - int getHeaderPositionForItem(int itemPosition); - - /** - * This method gets called by {@link StickyHeaderItemDecoration} to get layout resource id for the header item at specified adapter's position. - * @param headerPosition int. Position of the header item in the adapter. - * @return int. Layout resource id. - */ - int getHeaderLayout(int headerPosition); - - /** - * This method gets called by {@link StickyHeaderItemDecoration} to setup the header View. - * @param header View. Header to set the data on. - * @param headerPosition int. Position of the header item in the adapter. - */ - void bindHeaderData(View header, int headerPosition); - - /** - * This method gets called by {@link StickyHeaderItemDecoration} to verify whether the item represents a header. - * @param itemPosition int. - * @return true, if item at the specified adapter's position represents a header. - */ - boolean isHeader(int itemPosition); -} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java index 8e1f035d8396..4807307a250b 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java @@ -40,7 +40,7 @@ import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.operations.CommentFileOperation; import com.owncloud.android.ui.activity.ComponentsGetter; -import com.owncloud.android.ui.adapter.ActivityAndVersionListAdapter; +import com.owncloud.android.ui.activities.adapter.ActivityAndVersionListAdapter; import com.owncloud.android.ui.events.CommentsEvent; import com.owncloud.android.ui.helpers.FileOperationsHelper; import com.owncloud.android.ui.interfaces.ActivityListInterface; @@ -253,11 +253,10 @@ private void setupView() { binding.emptyList.emptyListIcon.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_activity, null)); binding.emptyList.emptyListView.setVisibility(View.GONE); - adapter = new ActivityAndVersionListAdapter(getContext(), + adapter = new ActivityAndVersionListAdapter(requireActivity(), accountManager, this, this, - clientFactory, viewThemeUtils ); binding.list.setAdapter(adapter); diff --git a/app/src/test/java/com/owncloud/android/ui/adapter/ActivityListAdapterTest.kt b/app/src/test/java/com/owncloud/android/ui/adapter/ActivityListAdapterTest.kt index e480fafeff5a..7983eadf1cc0 100644 --- a/app/src/test/java/com/owncloud/android/ui/adapter/ActivityListAdapterTest.kt +++ b/app/src/test/java/com/owncloud/android/ui/adapter/ActivityListAdapterTest.kt @@ -1,122 +1,63 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2019 Alex Plutta - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.ui.adapter; +package com.owncloud.android.ui.adapter -import com.owncloud.android.lib.resources.activities.model.Activity; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; - -public final class ActivityListAdapterTest { +import com.owncloud.android.lib.resources.activities.model.Activity +import com.owncloud.android.ui.activities.adapter.ActivityListAdapter +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations +class ActivityListAdapterTest { @Mock - private ActivityListAdapter activityListAdapter; + private lateinit var adapter: ActivityListAdapter + + private val header: Any = "Hello" + private lateinit var activity: Activity @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - MockitoAnnotations.initMocks(activityListAdapter); - activityListAdapter.values = new ArrayList<>(); + fun setUp() { + MockitoAnnotations.openMocks(this) + activity = mock(Activity::class.java) + adapter.values.clear() } @Test - public void isHeader__ObjectIsHeader_ReturnTrue() { - Object header = "Hello"; - Object activity = Mockito.mock(Activity.class); - - Mockito.when(activityListAdapter.isHeader(0)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getItemViewType(0)).thenCallRealMethod(); - - activityListAdapter.values.add(header); - activityListAdapter.values.add(activity); - - final boolean result = activityListAdapter.isHeader(0); - Assert.assertTrue(result); + fun `isHeader returns true when item is a String header`() { + adapter.values.addAll(listOf(header, activity)) + assertTrue(adapter.isHeader(0)) } @Test - public void isHeader__ObjectIsActivity_ReturnFalse() { - Object header = "Hello"; - Object activity = Mockito.mock(Activity.class); - - Mockito.when(activityListAdapter.isHeader(1)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getItemViewType(1)).thenCallRealMethod(); - - activityListAdapter.values.add(header); - activityListAdapter.values.add(activity); - Assert.assertFalse(activityListAdapter.isHeader(1)); + fun `isHeader returns false when item is an Activity`() { + adapter.values.addAll(listOf(header, activity)) + assertFalse(adapter.isHeader(1)) } @Test - public void getHeaderPositionForItem__AdapterIsEmpty_ReturnZero(){ - Mockito.when(activityListAdapter.isHeader(0)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getItemViewType(0)).thenCallRealMethod(); - - Assert.assertEquals(0,activityListAdapter.getHeaderPositionForItem(0)); + fun `getHeaderPositionForItem returns 0 when adapter is empty`() { + assertEquals(0, adapter.getHeaderPositionForItem(0)) } @Test - public void getHeaderPositionForItem__ItemIsHeader_ReturnCurrentItem() { - Object header = "Hello"; - Object activity = Mockito.mock(Activity.class); - - Mockito.when(activityListAdapter.isHeader(0)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getItemViewType(0)).thenCallRealMethod(); - Mockito.when(activityListAdapter.isHeader(1)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getItemViewType(1)).thenCallRealMethod(); - Mockito.when(activityListAdapter.isHeader(2)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getItemViewType(2)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getHeaderPositionForItem(2)).thenCallRealMethod(); - Mockito.when(activityListAdapter.isHeader(3)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getItemViewType(3)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getHeaderPositionForItem(3)).thenCallRealMethod(); - - - activityListAdapter.values.add(header); - activityListAdapter.values.add(activity); - activityListAdapter.values.add(header); - activityListAdapter.values.add(activity); - - - Assert.assertEquals(2, activityListAdapter.getHeaderPositionForItem(2)); - + fun `getHeaderPositionForItem returns current position when item is a header`() { + adapter.values.addAll(listOf(header, activity, header, activity)) + assertEquals(2, adapter.getHeaderPositionForItem(2)) } @Test - public void getHeaderPositionForItem__ItemIsActivity_ReturnNextHeader() { - Object header = "Hello"; - Object activity = Mockito.mock(Activity.class); - - Mockito.when(activityListAdapter.isHeader(0)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getItemViewType(0)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getHeaderPositionForItem(0)).thenCallRealMethod(); - Mockito.when(activityListAdapter.isHeader(1)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getItemViewType(1)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getHeaderPositionForItem(1)).thenCallRealMethod(); - Mockito.when(activityListAdapter.isHeader(2)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getItemViewType(2)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getHeaderPositionForItem(2)).thenCallRealMethod(); - Mockito.when(activityListAdapter.isHeader(3)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getItemViewType(3)).thenCallRealMethod(); - Mockito.when(activityListAdapter.getHeaderPositionForItem(3)).thenCallRealMethod(); - - activityListAdapter.values.add(header); - activityListAdapter.values.add(activity); - activityListAdapter.values.add(header); - activityListAdapter.values.add(activity); - - Assert.assertEquals(2, activityListAdapter.getHeaderPositionForItem(2)); + fun `getHeaderPositionForItem returns preceding header position when item is an Activity`() { + adapter.values.addAll(listOf(header, activity, header, activity)) + assertEquals(2, adapter.getHeaderPositionForItem(3)) } - } From 476d1e81e688c3b347aa5e7de35b4b0b11da013f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 11 Mar 2026 14:57:32 +0100 Subject: [PATCH 6/7] fix first load from file list details Signed-off-by: alperozturk96 --- .../android/ui/fragment/FileDetailActivitiesFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java index 4807307a250b..c2b49e229d3c 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java @@ -355,7 +355,7 @@ private void fetchAndSetData(long lastGiven) { } activity.runOnUiThread(() -> { - if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { + if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) { populateList(activitiesAndVersions, lastGiven == -1); } }); From 2b2cd2fe896f419fae47c655a95443bb876d98c5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 11 Mar 2026 15:10:43 +0100 Subject: [PATCH 7/7] fix tests Signed-off-by: alperozturk96 --- .../ui/activities/adapter/ActivityListAdapter.kt | 3 ++- .../android/ui/adapter/ActivityListAdapterTest.kt | 15 ++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityListAdapter.kt index eeccbc46c965..6311715cb1a6 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/activities/adapter/ActivityListAdapter.kt @@ -306,7 +306,8 @@ open class ActivityListAdapter( header?.findViewById(R.id.header)?.text = values[headerPosition] as String } - override fun isHeader(itemPosition: Int) = getItemViewType(itemPosition) == HEADER_TYPE + override fun isHeader(itemPosition: Int) = + itemPosition in values.indices && getItemViewType(itemPosition) == HEADER_TYPE protected class ActivityViewHolder(val binding: ActivityListItemBinding) : RecyclerView.ViewHolder(binding.root) diff --git a/app/src/test/java/com/owncloud/android/ui/adapter/ActivityListAdapterTest.kt b/app/src/test/java/com/owncloud/android/ui/adapter/ActivityListAdapterTest.kt index 7983eadf1cc0..f81ff04aef82 100644 --- a/app/src/test/java/com/owncloud/android/ui/adapter/ActivityListAdapterTest.kt +++ b/app/src/test/java/com/owncloud/android/ui/adapter/ActivityListAdapterTest.kt @@ -8,18 +8,19 @@ package com.owncloud.android.ui.adapter import com.owncloud.android.lib.resources.activities.model.Activity import com.owncloud.android.ui.activities.adapter.ActivityListAdapter +import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue -import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +import org.mockito.Answers import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations class ActivityListAdapterTest { - @Mock + @Mock(answer = Answers.CALLS_REAL_METHODS) private lateinit var adapter: ActivityListAdapter private val header: Any = "Hello" @@ -29,7 +30,11 @@ class ActivityListAdapterTest { fun setUp() { MockitoAnnotations.openMocks(this) activity = mock(Activity::class.java) - adapter.values.clear() + + ActivityListAdapter::class.java + .getDeclaredField("values") + .apply { isAccessible = true } + .set(adapter, mutableListOf()) } @Test @@ -45,8 +50,8 @@ class ActivityListAdapterTest { } @Test - fun `getHeaderPositionForItem returns 0 when adapter is empty`() { - assertEquals(0, adapter.getHeaderPositionForItem(0)) + fun `getHeaderPositionForItem returns -1 when adapter is empty`() { + assertEquals(-1, adapter.getHeaderPositionForItem(0)) } @Test