From bebb1c96e4279942de22093c2c53bb93962f76de Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 30 Apr 2026 16:13:53 +0200 Subject: [PATCH 01/35] wip Signed-off-by: alperozturk96 --- .../com/nextcloud/ui/ImageDetailFragment.kt | 48 ++++++++++++++++++- .../ui/fragment/FileDetailFragment.java | 5 +- .../res/layout/item_dropdown_with_icon.xml | 9 ++++ .../layout/preview_image_details_fragment.xml | 16 +++++++ 4 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 app/src/main/res/layout/item_dropdown_with_icon.xml diff --git a/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt b/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt index 766809e8247c..c59590ee5c63 100644 --- a/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt @@ -16,6 +16,7 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ArrayAdapter import androidx.annotation.VisibleForTesting import androidx.core.content.ContextCompat import androidx.core.net.toUri @@ -72,6 +73,51 @@ class ImageDetailFragment : override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = PreviewImageDetailsFragmentBinding.inflate(layoutInflater, container, false) + // TODO Bottom sheet or dropdown? + data class DropdownItem(val text: String, val iconRes: Int) + val items = listOf( + DropdownItem("Option 1", R.drawable.outline_camera_24), + DropdownItem("Option 2", R.drawable.outline_image_24), + DropdownItem("Option 3", R.drawable.ic_information_outline) + ) + + val adapter = object : ArrayAdapter(requireContext(), R.layout.item_dropdown_with_icon, items) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = super.getView(position, convertView, parent) as android.widget.TextView + val item = getItem(position) + if (item != null) { + view.text = item.text + val drawable = ContextCompat.getDrawable(context, item.iconRes)?.mutate() + drawable?.let { + viewThemeUtils.platform.tintDrawable(requireContext(),it, ColorRole.ON_SURFACE) + } + view.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) + } + return view + } + } + binding.dropdownMenuAutoComplete.setAdapter(adapter) + + val defaultSelectedItem = items.firstOrNull() + if (defaultSelectedItem != null) { + binding.dropdownMenuAutoComplete.setText(defaultSelectedItem.text, false) + val drawable = ContextCompat.getDrawable(requireContext(), defaultSelectedItem.iconRes)?.mutate() + drawable?.let { + viewThemeUtils.platform.tintDrawable(requireContext(),it, ColorRole.ON_SURFACE) + } + binding.dropdownMenuAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) + binding.dropdownMenuAutoComplete.compoundDrawablePadding = resources.getDimensionPixelSize(R.dimen.standard_padding) + } + + binding.dropdownMenuAutoComplete.setOnItemClickListener { _, _, position, _ -> + val selected = items[position] + binding.dropdownMenuAutoComplete.setText(selected.text, false) + val drawable = ContextCompat.getDrawable(requireContext(), selected.iconRes)?.mutate() + binding.dropdownMenuAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) + binding.dropdownMenuAutoComplete.compoundDrawablePadding = resources.getDimensionPixelSize(R.dimen.standard_padding) + } + + binding.fileDetailsIcon.setImageDrawable( viewThemeUtils.platform.tintDrawable( requireContext(), @@ -395,4 +441,4 @@ class ImageDetailFragment : } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 829e1b4dcc31..319a2ad20c39 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -316,9 +316,8 @@ private void setupViewPager() { binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.share_dialog_title).setIcon(R.drawable.selector_tab_share)); } - if (MimeTypeUtil.isImage(getFile())) { - binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.filedetails_details).setIcon(R.drawable.selector_media)); - } + // TODO - Change detail tab icon? + binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.filedetails_details).setIcon(R.drawable.selector_media)); viewThemeUtils.material.themeTabLayout(binding.tabLayout); diff --git a/app/src/main/res/layout/item_dropdown_with_icon.xml b/app/src/main/res/layout/item_dropdown_with_icon.xml new file mode 100644 index 000000000000..e9324acddd27 --- /dev/null +++ b/app/src/main/res/layout/item_dropdown_with_icon.xml @@ -0,0 +1,9 @@ + + diff --git a/app/src/main/res/layout/preview_image_details_fragment.xml b/app/src/main/res/layout/preview_image_details_fragment.xml index 72f0017e2ac5..d49741a3f741 100644 --- a/app/src/main/res/layout/preview_image_details_fragment.xml +++ b/app/src/main/res/layout/preview_image_details_fragment.xml @@ -17,6 +17,22 @@ android:orientation="vertical" android:padding="16dp"> + + + + + + Date: Tue, 5 May 2026 09:01:51 +0200 Subject: [PATCH 02/35] Rename .java to .kt Signed-off-by: alperozturk96 --- .../{FileDetailTabAdapter.java => FileDetailTabAdapter.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/owncloud/android/ui/adapter/{FileDetailTabAdapter.java => FileDetailTabAdapter.kt} (100%) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.java rename to app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt From 505305fe63b3e0863eae21cd141ccfe2e709aafe Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 09:01:52 +0200 Subject: [PATCH 03/35] introduce file info fragment, separate image detail logic Signed-off-by: alperozturk96 --- .../FileDetailFragmentStaticServerIT.kt | 4 +- .../nextcloud/client/di/ComponentsModule.java | 4 +- .../com/nextcloud/ui/ImageDetailFragment.kt | 444 ------------------ .../nextcloud/ui/fileInfo/FileInfoFragment.kt | 168 +++++++ .../nextcloud/ui/fileInfo/ImageDetailInfo.kt | 294 ++++++++++++ .../ui/fileInfo/model/ImageMetadata.kt | 26 + .../ui/adapter/FileDetailTabAdapter.kt | 113 ++--- .../ui/fragment/FileDetailFragment.java | 4 +- .../main/res/drawable/ic_dashboard_filled.xml | 24 + .../res/drawable/ic_dashboard_outlined.xml | 24 + .../main/res/drawable/selector_file_info.xml | 11 + ...ls_fragment.xml => file_info_fragment.xml} | 0 12 files changed, 592 insertions(+), 524 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt create mode 100644 app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt create mode 100644 app/src/main/java/com/nextcloud/ui/fileInfo/ImageDetailInfo.kt create mode 100644 app/src/main/java/com/nextcloud/ui/fileInfo/model/ImageMetadata.kt create mode 100644 app/src/main/res/drawable/ic_dashboard_filled.xml create mode 100644 app/src/main/res/drawable/ic_dashboard_outlined.xml create mode 100644 app/src/main/res/drawable/selector_file_info.xml rename app/src/main/res/layout/{preview_image_details_fragment.xml => file_info_fragment.xml} (100%) diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailFragmentStaticServerIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailFragmentStaticServerIT.kt index da634d4d62c6..94fe57651bc5 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailFragmentStaticServerIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailFragmentStaticServerIT.kt @@ -15,7 +15,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.nextcloud.test.TestActivity -import com.nextcloud.ui.ImageDetailFragment +import com.nextcloud.ui.fileInfo.FileInfoFragment import com.owncloud.android.AbstractIT import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile @@ -79,7 +79,7 @@ class FileDetailFragmentStaticServerIT : AbstractIT() { var activity: TestActivity? = null scenario.onActivity { sut -> activity = sut - val fragment = ImageDetailFragment.newInstance(oCFile, user).apply { + val fragment = FileInfoFragment.newInstance(oCFile, user).apply { hideMap() } sut.addFragment(fragment) diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java index d6f19299750b..438fa1768cf5 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -28,7 +28,7 @@ import com.nextcloud.client.widget.DashboardWidgetService; import com.nextcloud.ui.ChooseAccountDialogFragment; import com.nextcloud.ui.ChooseStorageLocationDialogFragment; -import com.nextcloud.ui.ImageDetailFragment; +import com.nextcloud.ui.fileInfo.FileInfoFragment; import com.nextcloud.ui.SetOnlineStatusBottomSheet; import com.nextcloud.ui.SetStatusMessageBottomSheet; import com.nextcloud.ui.composeActivity.ComposeActivity; @@ -483,7 +483,7 @@ abstract class ComponentsModule { abstract EditImageActivity editImageActivity(); @ContributesAndroidInjector - abstract ImageDetailFragment imageDetailFragment(); + abstract FileInfoFragment fileInfoFragment(); @ContributesAndroidInjector abstract EtmBackgroundJobsFragment etmBackgroundJobsFragment(); diff --git a/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt b/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt deleted file mode 100644 index c59590ee5c63..000000000000 --- a/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt +++ /dev/null @@ -1,444 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2023 ZetaTom - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.ui - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.graphics.drawable.LayerDrawable -import android.os.Bundle -import android.os.Parcelable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import androidx.annotation.VisibleForTesting -import androidx.core.content.ContextCompat -import androidx.core.net.toUri -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import com.nextcloud.android.common.ui.theme.utils.ColorRole -import com.nextcloud.client.NominatimClient -import com.nextcloud.client.account.User -import com.nextcloud.client.di.Injectable -import com.nextcloud.utils.extensions.getParcelableArgument -import com.nextcloud.utils.extensions.logFileSize -import com.owncloud.android.MainApp -import com.owncloud.android.R -import com.owncloud.android.databinding.PreviewImageDetailsFragmentBinding -import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.datamodel.ThumbnailsCacheManager -import com.owncloud.android.utils.BitmapUtils -import com.owncloud.android.utils.DisplayUtils -import com.owncloud.android.utils.theme.ViewThemeUtils -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.parcelize.Parcelize -import org.osmdroid.config.Configuration -import org.osmdroid.tileprovider.tilesource.TileSourceFactory -import org.osmdroid.util.GeoPoint -import org.osmdroid.views.CustomZoomButtonsController -import org.osmdroid.views.overlay.ItemizedIconOverlay -import org.osmdroid.views.overlay.ItemizedIconOverlay.OnItemGestureListener -import org.osmdroid.views.overlay.OverlayItem -import java.lang.Long.max -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.Locale -import javax.inject.Inject -import kotlin.math.pow -import kotlin.math.roundToInt - -class ImageDetailFragment : - Fragment(), - Injectable { - private lateinit var binding: PreviewImageDetailsFragmentBinding - private lateinit var file: OCFile - private lateinit var user: User - private lateinit var metadata: ImageMetadata - private lateinit var nominatimClient: NominatimClient - - @Inject - lateinit var viewThemeUtils: ViewThemeUtils - - private val tag = "ImageDetailFragment" - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = PreviewImageDetailsFragmentBinding.inflate(layoutInflater, container, false) - - // TODO Bottom sheet or dropdown? - data class DropdownItem(val text: String, val iconRes: Int) - val items = listOf( - DropdownItem("Option 1", R.drawable.outline_camera_24), - DropdownItem("Option 2", R.drawable.outline_image_24), - DropdownItem("Option 3", R.drawable.ic_information_outline) - ) - - val adapter = object : ArrayAdapter(requireContext(), R.layout.item_dropdown_with_icon, items) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = super.getView(position, convertView, parent) as android.widget.TextView - val item = getItem(position) - if (item != null) { - view.text = item.text - val drawable = ContextCompat.getDrawable(context, item.iconRes)?.mutate() - drawable?.let { - viewThemeUtils.platform.tintDrawable(requireContext(),it, ColorRole.ON_SURFACE) - } - view.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) - } - return view - } - } - binding.dropdownMenuAutoComplete.setAdapter(adapter) - - val defaultSelectedItem = items.firstOrNull() - if (defaultSelectedItem != null) { - binding.dropdownMenuAutoComplete.setText(defaultSelectedItem.text, false) - val drawable = ContextCompat.getDrawable(requireContext(), defaultSelectedItem.iconRes)?.mutate() - drawable?.let { - viewThemeUtils.platform.tintDrawable(requireContext(),it, ColorRole.ON_SURFACE) - } - binding.dropdownMenuAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) - binding.dropdownMenuAutoComplete.compoundDrawablePadding = resources.getDimensionPixelSize(R.dimen.standard_padding) - } - - binding.dropdownMenuAutoComplete.setOnItemClickListener { _, _, position, _ -> - val selected = items[position] - binding.dropdownMenuAutoComplete.setText(selected.text, false) - val drawable = ContextCompat.getDrawable(requireContext(), selected.iconRes)?.mutate() - binding.dropdownMenuAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) - binding.dropdownMenuAutoComplete.compoundDrawablePadding = resources.getDimensionPixelSize(R.dimen.standard_padding) - } - - - binding.fileDetailsIcon.setImageDrawable( - viewThemeUtils.platform.tintDrawable( - requireContext(), - R.drawable.outline_image_24, - ColorRole.ON_BACKGROUND - ) - ) - - binding.cameraInformationIcon.setImageDrawable( - viewThemeUtils.platform.tintDrawable( - requireContext(), - R.drawable.outline_camera_24, - ColorRole.ON_BACKGROUND - ) - ) - - val arguments = arguments ?: throw IllegalStateException("arguments are mandatory") - file = arguments.getParcelableArgument(ARG_FILE, OCFile::class.java)!! - user = arguments.getParcelableArgument(ARG_USER, User::class.java)!! - - if (savedInstanceState != null) { - file = savedInstanceState.getParcelableArgument(ARG_FILE, OCFile::class.java)!! - user = savedInstanceState.getParcelableArgument(ARG_USER, User::class.java)!! - metadata = savedInstanceState.getParcelableArgument(ARG_METADATA, ImageMetadata::class.java)!! - } - - nominatimClient = NominatimClient( - getString(R.string.osm_geocoder_url), - getString(R.string.osm_geocoder_contact) - ) - - return binding.root - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - file.logFileSize(tag) - outState.putParcelable(ARG_FILE, file) - outState.putParcelable(ARG_USER, user) - outState.putParcelable(ARG_METADATA, metadata) - } - - override fun onStart() { - super.onStart() - gatherMetadata() - setupFragment() - } - - @SuppressLint("LongMethod") - private fun setupFragment() { - binding.fileInformationTime.text = metadata.date - - // detailed file information - val fileInformation = mutableListOf() - if ((metadata.length ?: 0) > 0 && (metadata.width ?: 0) > 0) { - try { - @Suppress("MagicNumber") - val pxlCount = when (val res = metadata.length!! * metadata.width!!.toLong()) { - in 0..999999 -> "%.2f".format(res / 1000000f) - in 1000000..9999999 -> "%.1f".format(res / 1000000f) - else -> (res / 1000000).toString() - } - - fileInformation.add(String.format(getString(R.string.image_preview_unit_megapixel), pxlCount)) - fileInformation.add("${metadata.width!!} × ${metadata.length!!}") - } catch (_: NumberFormatException) { - } - } - metadata.fileSize?.let { fileInformation.add(it) } - - if (fileInformation.isNotEmpty()) { - binding.fileInformationDetails.text = fileInformation.joinToString(separator = TEXT_SEP) - binding.fileInformation.visibility = View.VISIBLE - } - - setImageTakenConditions() - - // initialise map and address views - metadata.location?.let { location -> - initMap(location.first, location.second) - binding.imageLocation.visibility = View.VISIBLE - - // launch reverse geocoding request - lifecycleScope.launch(Dispatchers.IO) { - val geocodingResult = nominatimClient.reverseGeocode(location.first, location.second) - if (geocodingResult != null) { - withContext(Dispatchers.Main) { - binding.imageLocationText.visibility = View.VISIBLE - binding.imageLocationText.text = geocodingResult.displayName - } - } - } - } - } - - private fun setImageTakenConditions() { - // camera make and model - val makeModel = if (metadata.make?.let { metadata.model?.contains(it) } == false) { - "${metadata.make} ${metadata.model}" - } else { - metadata.model ?: metadata.make - } - - if (metadata.make == null || metadata.model?.contains(metadata.make!!) == true) { - binding.imgTCMakeModel.text = metadata.model - } else { - binding.imgTCMakeModel.text = String.format( - getString(R.string.make_model), - metadata.make, - metadata.model - ) - } - - // image taking conditions - val imageTakingConditions = mutableListOf() - metadata.aperture?.let { - imageTakingConditions.add(String.format(getString(R.string.image_preview_unit_fnumber), it)) - } - metadata.exposure?.let { - imageTakingConditions.add(String.format(getString(R.string.image_preview_unit_seconds), it)) - } - metadata.focalLen?.let { - imageTakingConditions.add(String.format(getString(R.string.image_preview_unit_millimetres), it)) - } - metadata.iso?.let { - imageTakingConditions.add(String.format(getString(R.string.image_preview_unit_iso), it)) - } - - if (imageTakingConditions.isNotEmpty() && makeModel != null) { - binding.imgTCMakeModel.text = makeModel - binding.imgTCConditions.text = imageTakingConditions.joinToString(separator = TEXT_SEP) - binding.imgTC.visibility = View.VISIBLE - } - } - - @SuppressLint("ClickableViewAccessibility") - private fun initMap(latitude: Double, longitude: Double, zoom: Double = 13.0) { - // required for OpenStreetMap - Configuration.getInstance().userAgentValue = MainApp.getUserAgent() - - val location = GeoPoint(latitude, longitude) - - binding.imageLocationMap.apply { - setTileSource(TileSourceFactory.MAPNIK) - - // set expected boundaries - setScrollableAreaLimitLatitude(SCROLL_LIMIT, -SCROLL_LIMIT, 0) - isVerticalMapRepetitionEnabled = false - minZoomLevel = 2.0 - maxZoomLevel = NominatimClient.Companion.ZoomLevel.MAX.int.toDouble() - - // initial location - controller.setCenter(location) - controller.setZoom(zoom) - - // scale labels to be legible - isTilesScaledToDpi = true - setZoomRounding(true) - - // hide zoom buttons - zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER) - - // enable multi-touch zoom - setMultiTouchControls(true) - setOnTouchListener { v, _ -> - v.parent.requestDisallowInterceptTouchEvent(true) - false - } - - val markerOverlay = ItemizedIconOverlay( - mutableListOf(OverlayItem(null, null, location)), - imagePinDrawable(context), - markerOnGestureListener(latitude, longitude), - context - ) - - overlays.add(markerOverlay) - - onResume() - } - - // add copyright notice - binding.imageLocationMapCopyright.text = binding.imageLocationMap.tileProvider.tileSource.copyrightNotice - } - - @VisibleForTesting - fun hideMap() { - binding.imageLocationMap.visibility = View.GONE - } - - @SuppressLint("SimpleDateFormat") - private fun gatherMetadata() { - val fileSize = DisplayUtils.bytesToHumanReadable(file.fileLength) - var timestamp = max(file.modificationTimestamp, file.creationTimestamp) - if (file.isDown) { - val exif = androidx.exifinterface.media.ExifInterface(file.storagePath) - var length = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_IMAGE_LENGTH)?.toInt() - var width = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_IMAGE_WIDTH)?.toInt() - var exposure = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_SHUTTER_SPEED_VALUE) - - // get timestamp from date string - exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_DATETIME)?.let { - timestamp = SimpleDateFormat("y:M:d H:m:s", Locale.ROOT).parse(it)?.time ?: timestamp - } - - // format exposure string - if (exposure == null) { - exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_TIME)?.let { - exposure = "1/" + (1 / it.toDouble()).toInt() - } - } else if ("/" in exposure!!) { - try { - exposure!!.split("/").also { - exposure = "1/" + 2f.pow(it[0].toFloat() / it[1].toFloat()).roundToInt() - } - } catch (_: NumberFormatException) { - } - } - - // determine size if not contained in exif data - if ((width ?: 0) <= 0 || (length ?: 0) <= 0) { - val res = BitmapUtils.getImageResolution(file.storagePath) - width = res[0] - length = res[1] - } - - metadata = ImageMetadata( - fileSize = fileSize, - length = length, - width = width, - exposure = exposure, - date = formatDate(timestamp), - location = exif.latLong?.let { Pair(it[0], it[1]) }, - aperture = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_F_NUMBER), - focalLen = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM), - make = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_MAKE), - model = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_MODEL), - iso = exif.getAttribute(androidx.exifinterface.media.ExifInterface.TAG_ISO_SPEED) ?: exif.getAttribute( - androidx.exifinterface.media.ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY - ) - ) - } else { - // get metadata from server - val location = if (file.geoLocation == null) { - null - } else { - Pair(file.geoLocation!!.latitude, file.geoLocation!!.longitude) - } - metadata = ImageMetadata( - fileSize = fileSize, - date = formatDate(timestamp), - location = location, - width = file.imageDimension?.width?.toInt(), - length = file.imageDimension?.height?.toInt() - ) - } - } - - @SuppressLint("SimpleDateFormat") - private fun formatDate(timestamp: Long): String = buildString { - append(SimpleDateFormat("EEEE").format(timestamp)) - append(TEXT_SEP) - append(DateFormat.getDateInstance(DateFormat.MEDIUM).format(timestamp)) - append(TEXT_SEP) - append(DateFormat.getTimeInstance(DateFormat.SHORT).format(timestamp)) - } - - private fun imagePinDrawable(context: Context): LayerDrawable { - val drawable = ContextCompat.getDrawable(context, R.drawable.photo_pin) as LayerDrawable - - val bitmap = - ThumbnailsCacheManager.getBitmapFromDiskCache(ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId) - BitmapUtils.bitmapToCircularBitmapDrawable(resources, bitmap)?.let { - drawable.setDrawable(1, it) - } - - return drawable - } - - /** - * OnItemGestureListener for marker in MapView. - */ - private fun markerOnGestureListener(latitude: Double, longitude: Double) = - object : OnItemGestureListener { - override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean { - val intent = Intent(Intent.ACTION_VIEW, "geo:0,0?q=$latitude,$longitude".toUri()) - DisplayUtils.startIntentIfAppAvailable(intent, activity, R.string.no_map_app_availble) - return true - } - - override fun onItemLongPress(index: Int, item: OverlayItem): Boolean = false - } - - @Parcelize - private data class ImageMetadata( - val fileSize: String? = null, - val date: String? = null, - val length: Int? = null, - val width: Int? = null, - val exposure: String? = null, - val aperture: String? = null, - val focalLen: String? = null, - val iso: String? = null, - val make: String? = null, - val model: String? = null, - val location: Pair? = null - ) : Parcelable - - companion object { - private const val ARG_FILE = "FILE" - private const val ARG_USER = "USER" - private const val ARG_METADATA = "METADATA" - private const val TEXT_SEP = " • " - private const val SCROLL_LIMIT = 80.0 - - @JvmStatic - fun newInstance(file: OCFile, user: User): ImageDetailFragment = ImageDetailFragment().apply { - arguments = Bundle().apply { - putParcelable(ARG_FILE, file) - putParcelable(ARG_USER, user) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt new file mode 100644 index 000000000000..2c45a2cf8d5f --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt @@ -0,0 +1,168 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2023 ZetaTom + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ + +package com.nextcloud.ui.fileInfo + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.client.account.User +import com.nextcloud.client.di.Injectable +import com.nextcloud.ui.fileInfo.model.ImageMetadata +import com.nextcloud.utils.extensions.getParcelableArgument +import com.owncloud.android.R +import com.owncloud.android.databinding.FileInfoFragmentBinding +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.theme.ViewThemeUtils +import javax.inject.Inject + +class FileInfoFragment : + Fragment(), + Injectable { + private lateinit var binding: FileInfoFragmentBinding + private lateinit var file: OCFile + private lateinit var user: User + private var metadata: ImageMetadata? = null + private var imageDetailInfo: ImageDetailInfo? = null + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FileInfoFragmentBinding.inflate(layoutInflater, container, false) + + val arguments = arguments ?: throw IllegalStateException("arguments are mandatory") + file = arguments.getParcelableArgument(ARG_FILE, OCFile::class.java)!! + user = arguments.getParcelableArgument(ARG_USER, User::class.java)!! + + if (savedInstanceState != null) { + file = savedInstanceState.getParcelableArgument(ARG_FILE, OCFile::class.java)!! + user = savedInstanceState.getParcelableArgument(ARG_USER, User::class.java)!! + metadata = savedInstanceState.getParcelableArgument(ARG_METADATA, ImageMetadata::class.java)!! + } + + imageDetailInfo = ImageDetailInfo(this) + + + // TODO Bottom sheet or dropdown? + data class DropdownItem(val text: String, val iconRes: Int) + val items = listOf( + DropdownItem("Option 1", R.drawable.outline_camera_24), + DropdownItem("Option 2", R.drawable.outline_image_24), + DropdownItem("Option 3", R.drawable.ic_information_outline) + ) + + val adapter = object : ArrayAdapter(requireContext(), R.layout.item_dropdown_with_icon, items) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = super.getView(position, convertView, parent) as TextView + val item = getItem(position) + if (item != null) { + view.text = item.text + val drawable = ContextCompat.getDrawable(context, item.iconRes)?.mutate() + drawable?.let { + viewThemeUtils.platform.tintDrawable(requireContext(),it, ColorRole.ON_SURFACE) + } + view.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) + } + return view + } + } + binding.dropdownMenuAutoComplete.setAdapter(adapter) + + val defaultSelectedItem = items.firstOrNull() + if (defaultSelectedItem != null) { + binding.dropdownMenuAutoComplete.setText(defaultSelectedItem.text, false) + val drawable = ContextCompat.getDrawable(requireContext(), defaultSelectedItem.iconRes)?.mutate() + drawable?.let { + viewThemeUtils.platform.tintDrawable(requireContext(),it, ColorRole.ON_SURFACE) + } + binding.dropdownMenuAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) + binding.dropdownMenuAutoComplete.compoundDrawablePadding = resources.getDimensionPixelSize(R.dimen.standard_padding) + } + + binding.dropdownMenuAutoComplete.setOnItemClickListener { _, _, position, _ -> + val selected = items[position] + binding.dropdownMenuAutoComplete.setText(selected.text, false) + val drawable = ContextCompat.getDrawable(requireContext(), selected.iconRes)?.mutate() + binding.dropdownMenuAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) + binding.dropdownMenuAutoComplete.compoundDrawablePadding = resources.getDimensionPixelSize(R.dimen.standard_padding) + } + + // + + binding.fileDetailsIcon.setImageDrawable( + viewThemeUtils.platform.tintDrawable( + requireContext(), + R.drawable.outline_image_24, + ColorRole.ON_BACKGROUND + ) + ) + + binding.cameraInformationIcon.setImageDrawable( + viewThemeUtils.platform.tintDrawable( + requireContext(), + R.drawable.outline_camera_24, + ColorRole.ON_BACKGROUND + ) + ) + + return binding.root + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.run { + putParcelable(ARG_FILE, file) + putParcelable(ARG_USER, user) + putParcelable(ARG_METADATA, metadata) + } + } + + override fun onStart() { + super.onStart() + + if (MimeTypeUtil.isImage(file)) { + metadata = imageDetailInfo?.gatherMetadata(file) + } + + setupFragment() + } + + @VisibleForTesting + fun hideMap() { + binding.imageLocationMap.visibility = View.GONE + } + + private fun setupFragment() { + if (MimeTypeUtil.isImage(file)) { + metadata?.let { imageDetailInfo?.init(file, it, binding) } + } + } + + companion object { + private const val ARG_FILE = "FILE" + private const val ARG_USER = "USER" + private const val ARG_METADATA = "METADATA" + + @JvmStatic + fun newInstance(file: OCFile, user: User): FileInfoFragment = FileInfoFragment().apply { + arguments = Bundle().apply { + putParcelable(ARG_FILE, file) + putParcelable(ARG_USER, user) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/ImageDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/ImageDetailInfo.kt new file mode 100644 index 000000000000..861223f3eda6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/ImageDetailInfo.kt @@ -0,0 +1,294 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.fileInfo + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.graphics.drawable.LayerDrawable +import android.view.View +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import androidx.exifinterface.media.ExifInterface +import androidx.lifecycle.lifecycleScope +import com.nextcloud.client.NominatimClient +import com.nextcloud.ui.fileInfo.model.ImageMetadata +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.databinding.FileInfoFragmentBinding +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.ThumbnailsCacheManager +import com.owncloud.android.utils.BitmapUtils +import com.owncloud.android.utils.DisplayUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.osmdroid.config.Configuration +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.CustomZoomButtonsController +import org.osmdroid.views.overlay.ItemizedIconOverlay +import org.osmdroid.views.overlay.OverlayItem +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Locale +import kotlin.math.pow +import kotlin.math.roundToInt +import kotlin.ranges.contains +import kotlin.text.contains +import kotlin.text.split + +class ImageDetailInfo(private val fragment: FileInfoFragment) { + companion object { + private const val TEXT_SEP = " • " + private const val SCROLL_LIMIT = 80.0 + } + + fun init(file: OCFile, metadata: ImageMetadata, binding: FileInfoFragmentBinding) { + binding.fileInformationTime.text = metadata.date + + // detailed file information + val fileInformation = mutableListOf() + if ((metadata.length ?: 0) > 0 && (metadata.width ?: 0) > 0) { + try { + @Suppress("MagicNumber") + val pxlCount = when (val res = metadata.length!! * metadata.width!!.toLong()) { + in 0..999999 -> "%.2f".format(res / 1000000f) + in 1000000..9999999 -> "%.1f".format(res / 1000000f) + else -> (res / 1000000).toString() + } + + fileInformation.add(String.format(fragment.getString(R.string.image_preview_unit_megapixel), pxlCount)) + fileInformation.add("${metadata.width} × ${metadata.length}") + } catch (_: NumberFormatException) { + } + } + metadata.fileSize?.let { fileInformation.add(it) } + + if (fileInformation.isNotEmpty()) { + binding.fileInformationDetails.text = fileInformation.joinToString(separator = TEXT_SEP) + binding.fileInformation.visibility = View.VISIBLE + } + + setImageTakenConditions(metadata, binding) + + // initialise map and address views + metadata.location?.let { location -> + initMap(binding, file,location.first,location.second) + binding.imageLocation.visibility = View.VISIBLE + + // launch reverse geocoding request + fragment.lifecycleScope.launch(Dispatchers.IO) { + val nominatimClient = NominatimClient( + fragment.getString(R.string.osm_geocoder_url), + fragment.getString(R.string.osm_geocoder_contact) + ) + val geocodingResult = nominatimClient.reverseGeocode(location.first, location.second) + if (geocodingResult != null) { + withContext(Dispatchers.Main) { + binding.imageLocationText.visibility = View.VISIBLE + binding.imageLocationText.text = geocodingResult.displayName + } + } + } + } + } + + private fun setImageTakenConditions(metadata: ImageMetadata, binding: FileInfoFragmentBinding) { + // camera make and model + val makeModel = if (metadata.make?.let { metadata.model?.contains(it) } == false) { + "${metadata.make} ${metadata.model}" + } else { + metadata.model ?: metadata.make + } + + if (metadata.make == null || metadata.model?.contains(metadata.make) == true) { + binding.imgTCMakeModel.text = metadata.model + } else { + binding.imgTCMakeModel.text = String.format( + fragment.getString(R.string.make_model), + metadata.make, + metadata.model + ) + } + + // image taking conditions + val imageTakingConditions = mutableListOf() + metadata.aperture?.let { + imageTakingConditions.add(String.format(fragment.getString(R.string.image_preview_unit_fnumber), it)) + } + metadata.exposure?.let { + imageTakingConditions.add(String.format(fragment.getString(R.string.image_preview_unit_seconds), it)) + } + metadata.focalLen?.let { + imageTakingConditions.add(String.format(fragment.getString(R.string.image_preview_unit_millimetres), it)) + } + metadata.iso?.let { + imageTakingConditions.add(String.format(fragment.getString(R.string.image_preview_unit_iso), it)) + } + + if (imageTakingConditions.isNotEmpty() && makeModel != null) { + binding.imgTCMakeModel.text = makeModel + binding.imgTCConditions.text = imageTakingConditions.joinToString(separator = TEXT_SEP) + binding.imgTC.visibility = View.VISIBLE + } + } + + @SuppressLint("ClickableViewAccessibility") + private fun initMap(binding: FileInfoFragmentBinding, file: OCFile, latitude: Double, longitude: Double, zoom: Double = 13.0) { + // required for OpenStreetMap + Configuration.getInstance().userAgentValue = MainApp.getUserAgent() + + val location = GeoPoint(latitude, longitude) + + binding.imageLocationMap.apply { + setTileSource(TileSourceFactory.MAPNIK) + + // set expected boundaries + setScrollableAreaLimitLatitude(SCROLL_LIMIT, -SCROLL_LIMIT, 0) + isVerticalMapRepetitionEnabled = false + minZoomLevel = 2.0 + maxZoomLevel = NominatimClient.Companion.ZoomLevel.MAX.int.toDouble() + + // initial location + controller.setCenter(location) + controller.setZoom(zoom) + + // scale labels to be legible + isTilesScaledToDpi = true + setZoomRounding(true) + + // hide zoom buttons + zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER) + + // enable multi-touch zoom + setMultiTouchControls(true) + setOnTouchListener { v, _ -> + v.parent.requestDisallowInterceptTouchEvent(true) + false + } + + val markerOverlay = ItemizedIconOverlay( + mutableListOf(OverlayItem(null, null, location)), + imagePinDrawable(context, file), + markerOnGestureListener(latitude, longitude), + context + ) + + overlays.add(markerOverlay) + + onResume() + } + + // add copyright notice + binding.imageLocationMapCopyright.text = binding.imageLocationMap.tileProvider.tileSource.copyrightNotice + } + + @SuppressLint("SimpleDateFormat") + fun gatherMetadata(file: OCFile): ImageMetadata { + val fileSize = DisplayUtils.bytesToHumanReadable(file.fileLength) + var timestamp = java.lang.Long.max(file.modificationTimestamp, file.creationTimestamp) + return if (file.isDown) { + val exif = ExifInterface(file.storagePath) + var length = exif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH)?.toInt() + var width = exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)?.toInt() + var exposure = exif.getAttribute(ExifInterface.TAG_SHUTTER_SPEED_VALUE) + + // get timestamp from date string + exif.getAttribute(ExifInterface.TAG_DATETIME)?.let { + timestamp = SimpleDateFormat("y:M:d H:m:s", Locale.ROOT).parse(it)?.time ?: timestamp + } + + // format exposure string + if (exposure == null) { + exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)?.let { + exposure = "1/" + (1 / it.toDouble()).toInt() + } + } else if ("/" in exposure) { + try { + exposure.split("/").also { + exposure = "1/" + 2f.pow(it[0].toFloat() / it[1].toFloat()).roundToInt() + } + } catch (_: NumberFormatException) { + } + } + + // determine size if not contained in exif data + if ((width ?: 0) <= 0 || (length ?: 0) <= 0) { + val res = BitmapUtils.getImageResolution(file.storagePath) + width = res[0] + length = res[1] + } + + ImageMetadata( + fileSize = fileSize, + length = length, + width = width, + exposure = exposure, + date = formatDate(timestamp), + location = exif.latLong?.let { Pair(it[0], it[1]) }, + aperture = exif.getAttribute(ExifInterface.TAG_F_NUMBER), + focalLen = exif.getAttribute(ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM), + make = exif.getAttribute(ExifInterface.TAG_MAKE), + model = exif.getAttribute(ExifInterface.TAG_MODEL), + iso = exif.getAttribute(ExifInterface.TAG_ISO_SPEED) ?: exif.getAttribute( + ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY + ) + ) + } else { + // get metadata from server + val location = if (file.geoLocation == null) { + null + } else { + Pair(file.geoLocation!!.latitude, file.geoLocation!!.longitude) + } + ImageMetadata( + fileSize = fileSize, + date = formatDate(timestamp), + location = location, + width = file.imageDimension?.width?.toInt(), + length = file.imageDimension?.height?.toInt() + ) + } + } + + @SuppressLint("SimpleDateFormat") + private fun formatDate(timestamp: Long): String = buildString { + append(SimpleDateFormat("EEEE").format(timestamp)) + append(TEXT_SEP) + append(DateFormat.getDateInstance(DateFormat.MEDIUM).format(timestamp)) + append(TEXT_SEP) + append(DateFormat.getTimeInstance(DateFormat.SHORT).format(timestamp)) + } + + private fun imagePinDrawable(context: Context, file: OCFile): LayerDrawable { + val drawable = ContextCompat.getDrawable(context, R.drawable.photo_pin) as LayerDrawable + + val bitmap = + ThumbnailsCacheManager.getBitmapFromDiskCache(ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId) + BitmapUtils.bitmapToCircularBitmapDrawable(fragment.resources, bitmap)?.let { + drawable.setDrawable(1, it) + } + + return drawable + } + + /** + * OnItemGestureListener for marker in MapView. + */ + private fun markerOnGestureListener(latitude: Double, longitude: Double) = + object : ItemizedIconOverlay.OnItemGestureListener { + override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean { + val intent = Intent(Intent.ACTION_VIEW, "geo:0,0?q=$latitude,$longitude".toUri()) + DisplayUtils.startIntentIfAppAvailable(intent, fragment.activity, R.string.no_map_app_availble) + return true + } + + override fun onItemLongPress(index: Int, item: OverlayItem): Boolean = false + } +} diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/model/ImageMetadata.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/model/ImageMetadata.kt new file mode 100644 index 000000000000..ad57a9141f40 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/model/ImageMetadata.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.fileInfo.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ImageMetadata( + val fileSize: String? = null, + val date: String? = null, + val length: Int? = null, + val width: Int? = null, + val exposure: String? = null, + val aperture: String? = null, + val focalLen: String? = null, + val iso: String? = null, + val make: String? = null, + val model: String? = null, + val location: Pair? = null +) : Parcelable diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt index 9f0f271e494c..fa66d28e7f81 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt @@ -1,88 +1,55 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2018 Andy Scherzinger * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.adapter; - -import com.nextcloud.client.account.User; -import com.nextcloud.ui.ImageDetailFragment; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.ui.fragment.FileDetailActivitiesFragment; -import com.owncloud.android.ui.fragment.FileDetailSharingFragment; -import com.owncloud.android.utils.MimeTypeUtil; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.viewpager2.adapter.FragmentStateAdapter; - -/** - * File details pager adapter. - */ -public class FileDetailTabAdapter extends FragmentStateAdapter { - private final OCFile file; - private final User user; - private final boolean showSharingTab; - - private FileDetailSharingFragment fileDetailSharingFragment; - private FileDetailActivitiesFragment fileDetailActivitiesFragment; - private ImageDetailFragment imageDetailFragment; - - public FileDetailTabAdapter(FragmentActivity fragmentActivity, - OCFile file, - User user, - boolean showSharingTab) { - super(fragmentActivity); - this.file = file; - this.user = user; - this.showSharingTab = showSharingTab; - } - - public FileDetailSharingFragment getFileDetailSharingFragment() { - return fileDetailSharingFragment; - } - - public FileDetailActivitiesFragment getFileDetailActivitiesFragment() { - return fileDetailActivitiesFragment; - } - - public ImageDetailFragment getImageDetailFragment() { - return imageDetailFragment; - } - - @NonNull - @Override - public Fragment createFragment(int position) { - return switch (position) { - case 1 -> { - fileDetailSharingFragment = FileDetailSharingFragment.newInstance(file, user); - yield fileDetailSharingFragment; +package com.owncloud.android.ui.adapter + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.nextcloud.client.account.User +import com.nextcloud.ui.fileInfo.FileInfoFragment +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.fragment.FileDetailActivitiesFragment +import com.owncloud.android.ui.fragment.FileDetailSharingFragment + +class FileDetailTabAdapter( + fragmentActivity: FragmentActivity, + private val file: OCFile, + private val user: User, + private val showSharingTab: Boolean +) : FragmentStateAdapter(fragmentActivity) { + var fileDetailSharingFragment: FileDetailSharingFragment? = null + private set + var fileDetailActivitiesFragment: FileDetailActivitiesFragment? = null + private set + + override fun createFragment(position: Int): Fragment { + return when (position) { + 1 -> { + fileDetailSharingFragment = FileDetailSharingFragment.newInstance(file, user) + fileDetailSharingFragment } - case 2 -> { - imageDetailFragment = ImageDetailFragment.newInstance(file, user); - yield imageDetailFragment; + + 2 -> { + FileInfoFragment.newInstance(file, user) } - default -> { - fileDetailActivitiesFragment = FileDetailActivitiesFragment.newInstance(file, user); - yield fileDetailActivitiesFragment; + + else -> { + fileDetailActivitiesFragment = FileDetailActivitiesFragment.newInstance(file, user) + fileDetailActivitiesFragment } - }; + }!! } - @Override - public int getItemCount() { - if (showSharingTab) { - if (MimeTypeUtil.isImage(file)) { - return 3; - } - return 2; + override fun getItemCount(): Int { + return if (showSharingTab) { + 3 } else { - if (MimeTypeUtil.isImage(file)) { - return 2; - } - return 1; + 2 } } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 319a2ad20c39..2f5a717b3d2f 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -316,8 +316,7 @@ private void setupViewPager() { binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.share_dialog_title).setIcon(R.drawable.selector_tab_share)); } - // TODO - Change detail tab icon? - binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.filedetails_details).setIcon(R.drawable.selector_media)); + binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.filedetails_details).setIcon(R.drawable.selector_file_info)); viewThemeUtils.material.themeTabLayout(binding.tabLayout); @@ -384,7 +383,6 @@ public void onTabReselected(TabLayout.Tab tab) { @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); - FileExtensionsKt.logFileSize(getFile(), TAG); outState.putParcelable(ARG_FILE, getFile()); outState.putParcelable(ARG_USER, user); } diff --git a/app/src/main/res/drawable/ic_dashboard_filled.xml b/app/src/main/res/drawable/ic_dashboard_filled.xml new file mode 100644 index 000000000000..2d5b26893695 --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_filled.xml @@ -0,0 +1,24 @@ + + + + diff --git a/app/src/main/res/drawable/ic_dashboard_outlined.xml b/app/src/main/res/drawable/ic_dashboard_outlined.xml new file mode 100644 index 000000000000..49ba8ca94c99 --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_outlined.xml @@ -0,0 +1,24 @@ + + + + diff --git a/app/src/main/res/drawable/selector_file_info.xml b/app/src/main/res/drawable/selector_file_info.xml new file mode 100644 index 000000000000..71684759b904 --- /dev/null +++ b/app/src/main/res/drawable/selector_file_info.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/layout/preview_image_details_fragment.xml b/app/src/main/res/layout/file_info_fragment.xml similarity index 100% rename from app/src/main/res/layout/preview_image_details_fragment.xml rename to app/src/main/res/layout/file_info_fragment.xml From 56b1904804d5615be6c91d9f3e385f2fc5c8be24 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 09:27:36 +0200 Subject: [PATCH 04/35] simplify, split gov logic Signed-off-by: alperozturk96 --- .../nextcloud/ui/fileInfo/FileInfoFragment.kt | 121 +------- .../ui/fileInfo/GovernanceDetailInfo.kt | 73 +++++ .../nextcloud/ui/fileInfo/ImageDetailInfo.kt | 291 +++++++++--------- .../ui/fileInfo/model/SensitivityLabel.kt | 10 + .../ui/adapter/FileDetailTabAdapter.kt | 39 +-- 5 files changed, 252 insertions(+), 282 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt create mode 100644 app/src/main/java/com/nextcloud/ui/fileInfo/model/SensitivityLabel.kt diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt index 2c45a2cf8d5f..7a04aa727f98 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt @@ -1,9 +1,8 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 ZetaTom - * SPDX-FileCopyrightText: 2023 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.nextcloud.ui.fileInfo @@ -12,17 +11,11 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ArrayAdapter -import android.widget.TextView import androidx.annotation.VisibleForTesting -import androidx.core.content.ContextCompat +import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment -import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User import com.nextcloud.client.di.Injectable -import com.nextcloud.ui.fileInfo.model.ImageMetadata -import com.nextcloud.utils.extensions.getParcelableArgument -import com.owncloud.android.R import com.owncloud.android.databinding.FileInfoFragmentBinding import com.owncloud.android.datamodel.OCFile import com.owncloud.android.utils.MimeTypeUtil @@ -33,10 +26,14 @@ class FileInfoFragment : Fragment(), Injectable { private lateinit var binding: FileInfoFragmentBinding - private lateinit var file: OCFile - private lateinit var user: User - private var metadata: ImageMetadata? = null - private var imageDetailInfo: ImageDetailInfo? = null + + private val file: OCFile? by lazy { + arguments?.let { BundleCompat.getParcelable(it, ARG_FILE, OCFile::class.java) } + } + + private val user: User? by lazy { + arguments?.let { BundleCompat.getParcelable(it, ARG_USER, User::class.java) } + } @Inject lateinit var viewThemeUtils: ViewThemeUtils @@ -44,80 +41,13 @@ class FileInfoFragment : override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FileInfoFragmentBinding.inflate(layoutInflater, container, false) - val arguments = arguments ?: throw IllegalStateException("arguments are mandatory") - file = arguments.getParcelableArgument(ARG_FILE, OCFile::class.java)!! - user = arguments.getParcelableArgument(ARG_USER, User::class.java)!! - - if (savedInstanceState != null) { - file = savedInstanceState.getParcelableArgument(ARG_FILE, OCFile::class.java)!! - user = savedInstanceState.getParcelableArgument(ARG_USER, User::class.java)!! - metadata = savedInstanceState.getParcelableArgument(ARG_METADATA, ImageMetadata::class.java)!! - } - - imageDetailInfo = ImageDetailInfo(this) - - - // TODO Bottom sheet or dropdown? - data class DropdownItem(val text: String, val iconRes: Int) - val items = listOf( - DropdownItem("Option 1", R.drawable.outline_camera_24), - DropdownItem("Option 2", R.drawable.outline_image_24), - DropdownItem("Option 3", R.drawable.ic_information_outline) - ) - - val adapter = object : ArrayAdapter(requireContext(), R.layout.item_dropdown_with_icon, items) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = super.getView(position, convertView, parent) as TextView - val item = getItem(position) - if (item != null) { - view.text = item.text - val drawable = ContextCompat.getDrawable(context, item.iconRes)?.mutate() - drawable?.let { - viewThemeUtils.platform.tintDrawable(requireContext(),it, ColorRole.ON_SURFACE) - } - view.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) - } - return view - } - } - binding.dropdownMenuAutoComplete.setAdapter(adapter) - - val defaultSelectedItem = items.firstOrNull() - if (defaultSelectedItem != null) { - binding.dropdownMenuAutoComplete.setText(defaultSelectedItem.text, false) - val drawable = ContextCompat.getDrawable(requireContext(), defaultSelectedItem.iconRes)?.mutate() - drawable?.let { - viewThemeUtils.platform.tintDrawable(requireContext(),it, ColorRole.ON_SURFACE) - } - binding.dropdownMenuAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) - binding.dropdownMenuAutoComplete.compoundDrawablePadding = resources.getDimensionPixelSize(R.dimen.standard_padding) - } - - binding.dropdownMenuAutoComplete.setOnItemClickListener { _, _, position, _ -> - val selected = items[position] - binding.dropdownMenuAutoComplete.setText(selected.text, false) - val drawable = ContextCompat.getDrawable(requireContext(), selected.iconRes)?.mutate() - binding.dropdownMenuAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) - binding.dropdownMenuAutoComplete.compoundDrawablePadding = resources.getDimensionPixelSize(R.dimen.standard_padding) + val imageDetailInfo = ImageDetailInfo(this, viewThemeUtils) + if (MimeTypeUtil.isImage(file)) { + file?.let { imageDetailInfo.init(it, binding) } } - // - - binding.fileDetailsIcon.setImageDrawable( - viewThemeUtils.platform.tintDrawable( - requireContext(), - R.drawable.outline_image_24, - ColorRole.ON_BACKGROUND - ) - ) - - binding.cameraInformationIcon.setImageDrawable( - viewThemeUtils.platform.tintDrawable( - requireContext(), - R.drawable.outline_camera_24, - ColorRole.ON_BACKGROUND - ) - ) + val governanceDetailInfo = GovernanceDetailInfo(binding, viewThemeUtils, this) + governanceDetailInfo.init() return binding.root } @@ -127,18 +57,7 @@ class FileInfoFragment : outState.run { putParcelable(ARG_FILE, file) putParcelable(ARG_USER, user) - putParcelable(ARG_METADATA, metadata) - } - } - - override fun onStart() { - super.onStart() - - if (MimeTypeUtil.isImage(file)) { - metadata = imageDetailInfo?.gatherMetadata(file) } - - setupFragment() } @VisibleForTesting @@ -146,18 +65,10 @@ class FileInfoFragment : binding.imageLocationMap.visibility = View.GONE } - private fun setupFragment() { - if (MimeTypeUtil.isImage(file)) { - metadata?.let { imageDetailInfo?.init(file, it, binding) } - } - } - companion object { private const val ARG_FILE = "FILE" private const val ARG_USER = "USER" - private const val ARG_METADATA = "METADATA" - @JvmStatic fun newInstance(file: OCFile, user: User): FileInfoFragment = FileInfoFragment().apply { arguments = Bundle().apply { putParcelable(ARG_FILE, file) diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt new file mode 100644 index 000000000000..a8dd7fb43b23 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt @@ -0,0 +1,73 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.fileInfo + +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.TextView +import androidx.core.content.ContextCompat +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.ui.fileInfo.model.SensitivityLabel +import com.owncloud.android.R +import com.owncloud.android.databinding.FileInfoFragmentBinding +import com.owncloud.android.utils.theme.ViewThemeUtils + +class GovernanceDetailInfo( + private val binding: FileInfoFragmentBinding, + private val viewThemeUtils: ViewThemeUtils, + private val fragment: FileInfoFragment +) { + + fun init() { + val items = listOf( + SensitivityLabel("Option 1", R.drawable.outline_camera_24), + SensitivityLabel("Option 2", R.drawable.outline_image_24), + SensitivityLabel("Option 3", R.drawable.ic_information_outline) + ) + + val adapter = object : + ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown_with_icon, items) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = super.getView(position, convertView, parent) as TextView + val item = getItem(position) + if (item != null) { + view.text = item.text + val drawable = ContextCompat.getDrawable(context, item.iconRes)?.mutate() + drawable?.let { + viewThemeUtils.platform.tintDrawable(fragment.requireContext(), it, ColorRole.ON_SURFACE) + } + view.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) + } + return view + } + } + binding.dropdownMenuAutoComplete.setAdapter(adapter) + + val defaultSelectedItem = items.firstOrNull() + if (defaultSelectedItem != null) { + binding.dropdownMenuAutoComplete.setText(defaultSelectedItem.text, false) + val drawable = ContextCompat.getDrawable(fragment.requireContext(), defaultSelectedItem.iconRes)?.mutate() + drawable?.let { + viewThemeUtils.platform.tintDrawable(fragment.requireContext(), it, ColorRole.ON_SURFACE) + } + binding.dropdownMenuAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) + binding.dropdownMenuAutoComplete.compoundDrawablePadding = + fragment.resources.getDimensionPixelSize(R.dimen.standard_padding) + } + + binding.dropdownMenuAutoComplete.setOnItemClickListener { _, _, position, _ -> + val selected = items[position] + binding.dropdownMenuAutoComplete.setText(selected.text, false) + val drawable = ContextCompat.getDrawable(fragment.requireContext(), selected.iconRes)?.mutate() + binding.dropdownMenuAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) + binding.dropdownMenuAutoComplete.compoundDrawablePadding = + fragment.resources.getDimensionPixelSize(R.dimen.standard_padding) + } + } +} diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/ImageDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/ImageDetailInfo.kt index 861223f3eda6..2fbb72c2cf28 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/ImageDetailInfo.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/ImageDetailInfo.kt @@ -16,6 +16,7 @@ import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.exifinterface.media.ExifInterface import androidx.lifecycle.lifecycleScope +import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.NominatimClient import com.nextcloud.ui.fileInfo.model.ImageMetadata import com.owncloud.android.MainApp @@ -25,6 +26,7 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.utils.BitmapUtils import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -39,60 +41,71 @@ import java.text.SimpleDateFormat import java.util.Locale import kotlin.math.pow import kotlin.math.roundToInt -import kotlin.ranges.contains -import kotlin.text.contains -import kotlin.text.split -class ImageDetailInfo(private val fragment: FileInfoFragment) { +class ImageDetailInfo(private val fragment: FileInfoFragment, private val viewThemeUtils: ViewThemeUtils) { + companion object { private const val TEXT_SEP = " • " private const val SCROLL_LIMIT = 80.0 } - fun init(file: OCFile, metadata: ImageMetadata, binding: FileInfoFragmentBinding) { + fun init(file: OCFile, binding: FileInfoFragmentBinding) { + val metadata = gatherMetadata(file) binding.fileInformationTime.text = metadata.date + binding.fileDetailsIcon.setImageDrawable( + viewThemeUtils.platform.tintDrawable( + fragment.requireContext(), + R.drawable.outline_image_24, + ColorRole.ON_BACKGROUND + ) + ) - // detailed file information - val fileInformation = mutableListOf() - if ((metadata.length ?: 0) > 0 && (metadata.width ?: 0) > 0) { - try { - @Suppress("MagicNumber") - val pxlCount = when (val res = metadata.length!! * metadata.width!!.toLong()) { - in 0..999999 -> "%.2f".format(res / 1000000f) - in 1000000..9999999 -> "%.1f".format(res / 1000000f) - else -> (res / 1000000).toString() + binding.cameraInformationIcon.setImageDrawable( + viewThemeUtils.platform.tintDrawable( + fragment.requireContext(), + R.drawable.outline_camera_24, + ColorRole.ON_BACKGROUND + ) + ) + + val fileInformation = buildList { + val length = metadata.length ?: 0 + val width = metadata.width ?: 0 + if (length > 0 && width > 0) { + runCatching { + @Suppress("MagicNumber") + val pxlCount = when (val res = length * width.toLong()) { + in 0..999_999 -> "%.2f".format(res / 1_000_000f) + in 1_000_000..9_999_999 -> "%.1f".format(res / 1_000_000f) + else -> (res / 1_000_000).toString() + } + add(fragment.getString(R.string.image_preview_unit_megapixel).format(pxlCount)) + add("$width × $length") } - - fileInformation.add(String.format(fragment.getString(R.string.image_preview_unit_megapixel), pxlCount)) - fileInformation.add("${metadata.width} × ${metadata.length}") - } catch (_: NumberFormatException) { } + metadata.fileSize?.let { add(it) } } - metadata.fileSize?.let { fileInformation.add(it) } if (fileInformation.isNotEmpty()) { - binding.fileInformationDetails.text = fileInformation.joinToString(separator = TEXT_SEP) + binding.fileInformationDetails.text = fileInformation.joinToString(TEXT_SEP) binding.fileInformation.visibility = View.VISIBLE } setImageTakenConditions(metadata, binding) - // initialise map and address views - metadata.location?.let { location -> - initMap(binding, file,location.first,location.second) + metadata.location?.let { (lat, lon) -> + initMap(binding, file, lat, lon) binding.imageLocation.visibility = View.VISIBLE - // launch reverse geocoding request fragment.lifecycleScope.launch(Dispatchers.IO) { val nominatimClient = NominatimClient( fragment.getString(R.string.osm_geocoder_url), fragment.getString(R.string.osm_geocoder_contact) ) - val geocodingResult = nominatimClient.reverseGeocode(location.first, location.second) - if (geocodingResult != null) { + nominatimClient.reverseGeocode(lat, lon)?.let { result -> withContext(Dispatchers.Main) { + binding.imageLocationText.text = result.displayName binding.imageLocationText.visibility = View.VISIBLE - binding.imageLocationText.text = geocodingResult.displayName } } } @@ -100,187 +113,157 @@ class ImageDetailInfo(private val fragment: FileInfoFragment) { } private fun setImageTakenConditions(metadata: ImageMetadata, binding: FileInfoFragmentBinding) { - // camera make and model - val makeModel = if (metadata.make?.let { metadata.model?.contains(it) } == false) { - "${metadata.make} ${metadata.model}" - } else { - metadata.model ?: metadata.make + val makeModel = when { + metadata.make == null -> metadata.model + metadata.model?.contains(metadata.make) == true -> metadata.model + else -> fragment.getString(R.string.make_model).format(metadata.make, metadata.model) } - if (metadata.make == null || metadata.model?.contains(metadata.make) == true) { - binding.imgTCMakeModel.text = metadata.model - } else { - binding.imgTCMakeModel.text = String.format( - fragment.getString(R.string.make_model), - metadata.make, - metadata.model - ) - } - - // image taking conditions - val imageTakingConditions = mutableListOf() - metadata.aperture?.let { - imageTakingConditions.add(String.format(fragment.getString(R.string.image_preview_unit_fnumber), it)) - } - metadata.exposure?.let { - imageTakingConditions.add(String.format(fragment.getString(R.string.image_preview_unit_seconds), it)) - } - metadata.focalLen?.let { - imageTakingConditions.add(String.format(fragment.getString(R.string.image_preview_unit_millimetres), it)) - } - metadata.iso?.let { - imageTakingConditions.add(String.format(fragment.getString(R.string.image_preview_unit_iso), it)) + val imageTakingConditions = buildList { + metadata.aperture?.let { add(fragment.getString(R.string.image_preview_unit_fnumber).format(it)) } + metadata.exposure?.let { add(fragment.getString(R.string.image_preview_unit_seconds).format(it)) } + metadata.focalLen?.let { add(fragment.getString(R.string.image_preview_unit_millimetres).format(it)) } + metadata.iso?.let { add(fragment.getString(R.string.image_preview_unit_iso).format(it)) } } if (imageTakingConditions.isNotEmpty() && makeModel != null) { binding.imgTCMakeModel.text = makeModel - binding.imgTCConditions.text = imageTakingConditions.joinToString(separator = TEXT_SEP) + binding.imgTCConditions.text = imageTakingConditions.joinToString(TEXT_SEP) binding.imgTC.visibility = View.VISIBLE } } @SuppressLint("ClickableViewAccessibility") - private fun initMap(binding: FileInfoFragmentBinding, file: OCFile, latitude: Double, longitude: Double, zoom: Double = 13.0) { - // required for OpenStreetMap + private fun initMap( + binding: FileInfoFragmentBinding, + file: OCFile, + latitude: Double, + longitude: Double, + zoom: Double = 13.0 + ) { Configuration.getInstance().userAgentValue = MainApp.getUserAgent() val location = GeoPoint(latitude, longitude) binding.imageLocationMap.apply { setTileSource(TileSourceFactory.MAPNIK) - - // set expected boundaries setScrollableAreaLimitLatitude(SCROLL_LIMIT, -SCROLL_LIMIT, 0) isVerticalMapRepetitionEnabled = false minZoomLevel = 2.0 maxZoomLevel = NominatimClient.Companion.ZoomLevel.MAX.int.toDouble() - - // initial location controller.setCenter(location) controller.setZoom(zoom) - - // scale labels to be legible isTilesScaledToDpi = true setZoomRounding(true) - - // hide zoom buttons zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER) - - // enable multi-touch zoom setMultiTouchControls(true) setOnTouchListener { v, _ -> v.parent.requestDisallowInterceptTouchEvent(true) false } - - val markerOverlay = ItemizedIconOverlay( - mutableListOf(OverlayItem(null, null, location)), - imagePinDrawable(context, file), - markerOnGestureListener(latitude, longitude), - context + overlays.add( + ItemizedIconOverlay( + mutableListOf(OverlayItem(null, null, location)), + imagePinDrawable(context, file), + markerOnGestureListener(latitude, longitude), + context + ) ) - - overlays.add(markerOverlay) - onResume() } - // add copyright notice - binding.imageLocationMapCopyright.text = binding.imageLocationMap.tileProvider.tileSource.copyrightNotice + binding.imageLocationMapCopyright.text = + binding.imageLocationMap.tileProvider.tileSource.copyrightNotice } - @SuppressLint("SimpleDateFormat") fun gatherMetadata(file: OCFile): ImageMetadata { val fileSize = DisplayUtils.bytesToHumanReadable(file.fileLength) - var timestamp = java.lang.Long.max(file.modificationTimestamp, file.creationTimestamp) + val timestamp = maxOf(file.modificationTimestamp, file.creationTimestamp) return if (file.isDown) { - val exif = ExifInterface(file.storagePath) - var length = exif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH)?.toInt() - var width = exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)?.toInt() - var exposure = exif.getAttribute(ExifInterface.TAG_SHUTTER_SPEED_VALUE) - - // get timestamp from date string - exif.getAttribute(ExifInterface.TAG_DATETIME)?.let { - timestamp = SimpleDateFormat("y:M:d H:m:s", Locale.ROOT).parse(it)?.time ?: timestamp - } + gatherLocalMetadata(file, fileSize, timestamp) + } else { + gatherRemoteMetadata(file, fileSize, timestamp) + } + } - // format exposure string - if (exposure == null) { - exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)?.let { - exposure = "1/" + (1 / it.toDouble()).toInt() - } - } else if ("/" in exposure) { - try { - exposure.split("/").also { - exposure = "1/" + 2f.pow(it[0].toFloat() / it[1].toFloat()).roundToInt() - } - } catch (_: NumberFormatException) { - } - } + private fun gatherLocalMetadata(file: OCFile, fileSize: String, fallbackTimestamp: Long): ImageMetadata { + val exif = ExifInterface(file.storagePath) + val timestamp = parseTimestamp(exif, fallbackTimestamp) + val (width, length) = parseImageDimensions(exif, file.storagePath) + + return ImageMetadata( + fileSize = fileSize, + date = formatDate(timestamp), + length = length, + width = width, + exposure = parseExposure(exif), + location = exif.latLong?.let { Pair(it[0], it[1]) }, + aperture = exif.getAttribute(ExifInterface.TAG_F_NUMBER), + focalLen = exif.getAttribute(ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM), + make = exif.getAttribute(ExifInterface.TAG_MAKE), + model = exif.getAttribute(ExifInterface.TAG_MODEL), + iso = exif.getAttribute(ExifInterface.TAG_ISO_SPEED) + ?: exif.getAttribute(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY) + ) + } - // determine size if not contained in exif data - if ((width ?: 0) <= 0 || (length ?: 0) <= 0) { - val res = BitmapUtils.getImageResolution(file.storagePath) - width = res[0] - length = res[1] - } + private fun gatherRemoteMetadata(file: OCFile, fileSize: String, timestamp: Long) = ImageMetadata( + fileSize = fileSize, + date = formatDate(timestamp), + location = file.geoLocation?.let { Pair(it.latitude, it.longitude) }, + width = file.imageDimension?.width?.toInt(), + length = file.imageDimension?.height?.toInt() + ) - ImageMetadata( - fileSize = fileSize, - length = length, - width = width, - exposure = exposure, - date = formatDate(timestamp), - location = exif.latLong?.let { Pair(it[0], it[1]) }, - aperture = exif.getAttribute(ExifInterface.TAG_F_NUMBER), - focalLen = exif.getAttribute(ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM), - make = exif.getAttribute(ExifInterface.TAG_MAKE), - model = exif.getAttribute(ExifInterface.TAG_MODEL), - iso = exif.getAttribute(ExifInterface.TAG_ISO_SPEED) ?: exif.getAttribute( - ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY - ) - ) - } else { - // get metadata from server - val location = if (file.geoLocation == null) { - null - } else { - Pair(file.geoLocation!!.latitude, file.geoLocation!!.longitude) + private fun parseTimestamp(exif: ExifInterface, fallback: Long): Long = + exif.getAttribute(ExifInterface.TAG_DATETIME)?.let { + SimpleDateFormat("y:M:d H:m:s", Locale.ROOT).parse(it)?.time + } ?: fallback + + @Suppress("ReturnCount") + private fun parseExposure(exif: ExifInterface): String? { + val shutterSpeed = exif.getAttribute(ExifInterface.TAG_SHUTTER_SPEED_VALUE) + val exposureTime = exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME) + ?.let { "1/${(1 / it.toDouble()).toInt()}" } + + val raw = shutterSpeed ?: exposureTime ?: return null + + if ('/' !in raw) return raw + + return runCatching { + raw.split("/").let { parts -> + "1/${2f.pow(parts[0].toFloat() / parts[1].toFloat()).roundToInt()}" } - ImageMetadata( - fileSize = fileSize, - date = formatDate(timestamp), - location = location, - width = file.imageDimension?.width?.toInt(), - length = file.imageDimension?.height?.toInt() - ) - } + }.getOrDefault(raw) + } + + private fun parseImageDimensions(exif: ExifInterface, storagePath: String): Pair { + val width = exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)?.toInt() + val length = exif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH)?.toInt() + + if ((width ?: 0) > 0 && (length ?: 0) > 0) return Pair(width, length) + + return BitmapUtils.getImageResolution(storagePath).let { Pair(it[0], it[1]) } } - @SuppressLint("SimpleDateFormat") private fun formatDate(timestamp: Long): String = buildString { - append(SimpleDateFormat("EEEE").format(timestamp)) + append(SimpleDateFormat("EEEE", Locale.getDefault()).format(timestamp)) append(TEXT_SEP) append(DateFormat.getDateInstance(DateFormat.MEDIUM).format(timestamp)) append(TEXT_SEP) append(DateFormat.getTimeInstance(DateFormat.SHORT).format(timestamp)) } - private fun imagePinDrawable(context: Context, file: OCFile): LayerDrawable { - val drawable = ContextCompat.getDrawable(context, R.drawable.photo_pin) as LayerDrawable - - val bitmap = - ThumbnailsCacheManager.getBitmapFromDiskCache(ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId) - BitmapUtils.bitmapToCircularBitmapDrawable(fragment.resources, bitmap)?.let { - drawable.setDrawable(1, it) + private fun imagePinDrawable(context: Context, file: OCFile): LayerDrawable = + (ContextCompat.getDrawable(context, R.drawable.photo_pin) as LayerDrawable).apply { + val bitmap = ThumbnailsCacheManager.getBitmapFromDiskCache( + ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId + ) + BitmapUtils.bitmapToCircularBitmapDrawable(fragment.resources, bitmap)?.let { + setDrawable(1, it) + } } - return drawable - } - - /** - * OnItemGestureListener for marker in MapView. - */ private fun markerOnGestureListener(latitude: Double, longitude: Double) = object : ItemizedIconOverlay.OnItemGestureListener { override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean { @@ -289,6 +272,6 @@ class ImageDetailInfo(private val fragment: FileInfoFragment) { return true } - override fun onItemLongPress(index: Int, item: OverlayItem): Boolean = false + override fun onItemLongPress(index: Int, item: OverlayItem) = false } } diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/model/SensitivityLabel.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/model/SensitivityLabel.kt new file mode 100644 index 000000000000..d9895ee6fe3a --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/model/SensitivityLabel.kt @@ -0,0 +1,10 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.fileInfo.model + +data class SensitivityLabel(val text: String, val iconRes: Int) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt index fa66d28e7f81..9fbcba02eb8f 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt @@ -22,34 +22,27 @@ class FileDetailTabAdapter( private val user: User, private val showSharingTab: Boolean ) : FragmentStateAdapter(fragmentActivity) { + + private enum class Tab(val position: Int) { + Activities(0), + Sharing(1), + Details(2) + } + var fileDetailSharingFragment: FileDetailSharingFragment? = null private set var fileDetailActivitiesFragment: FileDetailActivitiesFragment? = null private set - override fun createFragment(position: Int): Fragment { - return when (position) { - 1 -> { - fileDetailSharingFragment = FileDetailSharingFragment.newInstance(file, user) - fileDetailSharingFragment - } - - 2 -> { - FileInfoFragment.newInstance(file, user) - } - - else -> { - fileDetailActivitiesFragment = FileDetailActivitiesFragment.newInstance(file, user) - fileDetailActivitiesFragment - } - }!! - } + override fun createFragment(position: Int): Fragment = when (position) { + Tab.Sharing.position -> FileDetailSharingFragment.newInstance(file, user) + .also { fileDetailSharingFragment = it } + + Tab.Details.position -> FileInfoFragment.newInstance(file, user) - override fun getItemCount(): Int { - return if (showSharingTab) { - 3 - } else { - 2 - } + else -> FileDetailActivitiesFragment.newInstance(file, user) + .also { fileDetailActivitiesFragment = it } } + + override fun getItemCount(): Int = if (showSharingTab) Tab.entries.size else Tab.entries.size - 1 } From 0cd4e964ae60f9ed1ca210d44b9c9e6774fb5d47 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 09:28:27 +0200 Subject: [PATCH 05/35] simplify, split gov logic Signed-off-by: alperozturk96 --- app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt index 7a04aa727f98..3eddf0c3e0ac 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt @@ -41,8 +41,8 @@ class FileInfoFragment : override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FileInfoFragmentBinding.inflate(layoutInflater, container, false) - val imageDetailInfo = ImageDetailInfo(this, viewThemeUtils) if (MimeTypeUtil.isImage(file)) { + val imageDetailInfo = ImageDetailInfo(this, viewThemeUtils) file?.let { imageDetailInfo.init(it, binding) } } From f15e473213ea5d0f49f553f6f3b00f3c7ef75cb6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 09:55:22 +0200 Subject: [PATCH 06/35] split ui, better margins Signed-off-by: alperozturk96 --- .../ui/fileInfo/GovernanceDetailInfo.kt | 16 +- .../main/res/layout/file_info_fragment.xml | 260 ++++++++++-------- app/src/main/res/values/dims.xml | 2 +- app/src/main/res/values/strings.xml | 3 + 4 files changed, 155 insertions(+), 126 deletions(-) diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt index a8dd7fb43b23..90c2a1e35be2 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt @@ -47,26 +47,26 @@ class GovernanceDetailInfo( return view } } - binding.dropdownMenuAutoComplete.setAdapter(adapter) + binding.sensitivityLabelAutoComplete.setAdapter(adapter) val defaultSelectedItem = items.firstOrNull() if (defaultSelectedItem != null) { - binding.dropdownMenuAutoComplete.setText(defaultSelectedItem.text, false) + binding.sensitivityLabelAutoComplete.setText(defaultSelectedItem.text, false) val drawable = ContextCompat.getDrawable(fragment.requireContext(), defaultSelectedItem.iconRes)?.mutate() drawable?.let { viewThemeUtils.platform.tintDrawable(fragment.requireContext(), it, ColorRole.ON_SURFACE) } - binding.dropdownMenuAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) - binding.dropdownMenuAutoComplete.compoundDrawablePadding = + binding.sensitivityLabelAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) + binding.sensitivityLabelAutoComplete.compoundDrawablePadding = fragment.resources.getDimensionPixelSize(R.dimen.standard_padding) } - binding.dropdownMenuAutoComplete.setOnItemClickListener { _, _, position, _ -> + binding.sensitivityLabelAutoComplete.setOnItemClickListener { _, _, position, _ -> val selected = items[position] - binding.dropdownMenuAutoComplete.setText(selected.text, false) + binding.sensitivityLabelAutoComplete.setText(selected.text, false) val drawable = ContextCompat.getDrawable(fragment.requireContext(), selected.iconRes)?.mutate() - binding.dropdownMenuAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) - binding.dropdownMenuAutoComplete.compoundDrawablePadding = + binding.sensitivityLabelAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) + binding.sensitivityLabelAutoComplete.compoundDrawablePadding = fragment.resources.getDimensionPixelSize(R.dimen.standard_padding) } } diff --git a/app/src/main/res/layout/file_info_fragment.xml b/app/src/main/res/layout/file_info_fragment.xml index d49741a3f741..d70d31676595 100644 --- a/app/src/main/res/layout/file_info_fragment.xml +++ b/app/src/main/res/layout/file_info_fragment.xml @@ -15,164 +15,190 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:padding="16dp"> + android:padding="@dimen/standard_padding"> - + android:orientation="vertical"> + + + + + + - + android:layout_marginBottom="16dp" + android:hint="@string/governance_file_detention_label"> + + + + + + - + android:orientation="vertical"> + android:orientation="vertical"> + + - - android:src="@drawable/outline_image_24" /> + + + + + + + + android:gravity="center_vertical" + android:orientation="horizontal" + android:visibility="gone" + tools:visibility="visible"> - + - + android:orientation="vertical"> + + + + + + - - - + android:layout_width="match_parent" + android:layout_height="@dimen/image_detail_map_height" + android:background="@drawable/rounded_rect" + android:elevation="1dp" + android:orientation="vertical"> - - - - - - - - + android:visibility="gone" + tools:text="@string/placeholder_image_detail_location" + tools:visibility="visible" /> - - - - - - - + android:layout_height="match_parent"> + + + + + - - + - + - + + diff --git a/app/src/main/res/values/dims.xml b/app/src/main/res/values/dims.xml index 4f6ac5d3e1fa..1123e39c7597 100644 --- a/app/src/main/res/values/dims.xml +++ b/app/src/main/res/values/dims.xml @@ -38,7 +38,7 @@ 100dp 100dp 4dp - + 300dp 14dp 12sp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 28788ecd96f3..2c02a01ca573 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,6 +7,9 @@ --> Local contacts + Sensitivity label + File detention + Failed to open text editor All files Favorites From 7f73b009d66be820f94bf6bb5286c71004132891 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 10:08:27 +0200 Subject: [PATCH 07/35] use m3 card to distinc between sections Signed-off-by: alperozturk96 --- .../nextcloud/ui/fileInfo/FileInfoFragment.kt | 1 + .../ui/fileInfo/GovernanceDetailInfo.kt | 5 + .../nextcloud/ui/fileInfo/ImageDetailInfo.kt | 3 + .../main/res/layout/file_info_fragment.xml | 171 ++++++++++++------ app/src/main/res/values/strings.xml | 2 + 5 files changed, 122 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt index 3eddf0c3e0ac..c9d24a608348 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt @@ -46,6 +46,7 @@ class FileInfoFragment : file?.let { imageDetailInfo.init(it, binding) } } + val governanceDetailInfo = GovernanceDetailInfo(binding, viewThemeUtils, this) governanceDetailInfo.init() diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt index 90c2a1e35be2..ab2203252530 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt @@ -25,6 +25,8 @@ class GovernanceDetailInfo( ) { fun init() { + viewThemeUtils.material.themeCardView(binding.governanceLayout) + val items = listOf( SensitivityLabel("Option 1", R.drawable.outline_camera_24), SensitivityLabel("Option 2", R.drawable.outline_image_24), @@ -69,5 +71,8 @@ class GovernanceDetailInfo( binding.sensitivityLabelAutoComplete.compoundDrawablePadding = fragment.resources.getDimensionPixelSize(R.dimen.standard_padding) } + + viewThemeUtils.material.colorTextInputLayout(binding.sensitivityLabel) + viewThemeUtils.material.colorTextInputLayout(binding.fileDetentionLabel) } } diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/ImageDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/ImageDetailInfo.kt index 2fbb72c2cf28..00baceba14d9 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/ImageDetailInfo.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/ImageDetailInfo.kt @@ -50,6 +50,9 @@ class ImageDetailInfo(private val fragment: FileInfoFragment, private val viewTh } fun init(file: OCFile, binding: FileInfoFragmentBinding) { + viewThemeUtils.material.themeCardView(binding.imageDetailLayout) + binding.imageDetailLayout.visibility = View.VISIBLE + val metadata = gatherMetadata(file) binding.fileInformationTime.text = metadata.date binding.fileDetailsIcon.setImageDrawable( diff --git a/app/src/main/res/layout/file_info_fragment.xml b/app/src/main/res/layout/file_info_fragment.xml index d70d31676595..8b746716a2db 100644 --- a/app/src/main/res/layout/file_info_fragment.xml +++ b/app/src/main/res/layout/file_info_fragment.xml @@ -2,11 +2,13 @@ @@ -17,58 +19,91 @@ android:orientation="vertical" android:padding="@dimen/standard_padding"> - + android:layout_marginBottom="@dimen/standard_margin" + app:cardElevation="0dp" + app:strokeColor="?attr/colorOutline" + app:strokeWidth="1dp" + app:cardCornerRadius="@dimen/card_corner_radius" + app:cardBackgroundColor="?attr/colorSurface"> - + android:orientation="vertical" + android:padding="@dimen/standard_padding"> - + + + android:layout_marginBottom="@dimen/standard_margin" + android:hint="@string/governance_sensitivity_label"> - + - + - + android:hint="@string/governance_file_detention_label"> - + - + + + + - + app:cardElevation="0dp" + app:strokeColor="?attr/colorOutline" + app:strokeWidth="1dp" + app:cardCornerRadius="@dimen/card_corner_radius" + app:cardBackgroundColor="?attr/colorSurface" + android:visibility="gone" + tools:visibility="visible"> + android:orientation="vertical" + android:padding="@dimen/standard_padding"> + + @@ -103,6 +139,8 @@ android:id="@+id/fileInformation_details" android:layout_width="match_parent" android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.Material3.BodySmall" + android:textColor="?attr/colorOnSurfaceVariant" tools:text="@string/placeholder_image_details" /> @@ -111,6 +149,7 @@ android:id="@+id/imgTC" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginTop="@dimen/standard_quarter_margin" android:gravity="center_vertical" android:orientation="horizontal" android:visibility="gone" @@ -133,6 +172,7 @@ android:id="@+id/imgTC_makeModel" android:layout_width="match_parent" android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.Material3.BodyMedium" android:textStyle="bold" tools:text="@string/placeholder_image_detail_model" /> @@ -140,26 +180,25 @@ android:id="@+id/imgTC_conditions" android:layout_width="match_parent" android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.Material3.BodySmall" + android:textColor="?attr/colorOnSurfaceVariant" tools:text="@string/placeholder_image_detail_condition" /> - - - - + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/standard_margin" + android:orientation="vertical" + android:visibility="gone" + tools:visibility="visible"> + + - + android:layout_height="@dimen/image_detail_map_height" + app:cardElevation="0dp" + app:strokeColor="?attr/colorOutlineVariant" + app:strokeWidth="1dp" + app:cardCornerRadius="@dimen/standard_half_padding"> - - - - + android:layout_height="match_parent"> + + + + + + + - - + - + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2c02a01ca573..0ebe2aa05d42 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,6 +9,8 @@ Local contacts Sensitivity label File detention + Governance + Image details Failed to open text editor All files From bee18e19a516e23a60dfdfd7f1ee8c224128f00e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 10:21:01 +0200 Subject: [PATCH 08/35] simplify Signed-off-by: alperozturk96 --- .../nextcloud/ui/fileInfo/FileInfoFragment.kt | 1 - .../ui/fileInfo/GovernanceDetailInfo.kt | 95 +++++++++++-------- ...SensitivityLabel.kt => GovernanceLabel.kt} | 2 +- .../theme/FilesSpecificViewThemeUtils.kt | 11 +++ 4 files changed, 70 insertions(+), 39 deletions(-) rename app/src/main/java/com/nextcloud/ui/fileInfo/model/{SensitivityLabel.kt => GovernanceLabel.kt} (75%) diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt index c9d24a608348..3eddf0c3e0ac 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt @@ -46,7 +46,6 @@ class FileInfoFragment : file?.let { imageDetailInfo.init(it, binding) } } - val governanceDetailInfo = GovernanceDetailInfo(binding, viewThemeUtils, this) governanceDetailInfo.init() diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt index ab2203252530..a1cfad20f3ee 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt @@ -12,8 +12,10 @@ import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.TextView import androidx.core.content.ContextCompat +import com.google.android.material.textfield.MaterialAutoCompleteTextView +import com.google.android.material.textfield.TextInputLayout import com.nextcloud.android.common.ui.theme.utils.ColorRole -import com.nextcloud.ui.fileInfo.model.SensitivityLabel +import com.nextcloud.ui.fileInfo.model.GovernanceLabel import com.owncloud.android.R import com.owncloud.android.databinding.FileInfoFragmentBinding import com.owncloud.android.utils.theme.ViewThemeUtils @@ -23,56 +25,75 @@ class GovernanceDetailInfo( private val viewThemeUtils: ViewThemeUtils, private val fragment: FileInfoFragment ) { + private val context get() = fragment.requireContext() fun init() { viewThemeUtils.material.themeCardView(binding.governanceLayout) + initSensitivityLabel() + initFileDetentionLabel() + } + + private fun initSensitivityLabel() { + initDropdown( + textInputLayout = binding.sensitivityLabel, + autoComplete = binding.sensitivityLabelAutoComplete, + items = listOf( + GovernanceLabel("Sharing restricted", R.drawable.ic_share), + GovernanceLabel("Download restricted", R.drawable.ic_download_grey600), + GovernanceLabel("Upload restricted", R.drawable.uploads) + ) + ) + } - val items = listOf( - SensitivityLabel("Option 1", R.drawable.outline_camera_24), - SensitivityLabel("Option 2", R.drawable.outline_image_24), - SensitivityLabel("Option 3", R.drawable.ic_information_outline) + private fun initFileDetentionLabel() { + initDropdown( + textInputLayout = binding.fileDetentionLabel, + autoComplete = binding.fileDetentionAutoComplete, + items = listOf( + GovernanceLabel("Public", R.drawable.file_link), + GovernanceLabel("Internal use only", R.drawable.ic_group), + GovernanceLabel("Restricted", R.drawable.ic_cancel) + ) ) + } + + private fun initDropdown( + textInputLayout: TextInputLayout, + autoComplete: MaterialAutoCompleteTextView, + items: List + ) { + viewThemeUtils.material.colorTextInputLayout(textInputLayout) + viewThemeUtils.files.themeAutoCompleteTextView(autoComplete) + + autoComplete.setAdapter(buildAdapter(items)) + + items.firstOrNull()?.let { applySelection(autoComplete, it) } - val adapter = object : - ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown_with_icon, items) { + autoComplete.setOnItemClickListener { _, _, position, _ -> + applySelection(autoComplete, items[position]) + } + } + + private fun buildAdapter(items: List) = + object : ArrayAdapter(context, R.layout.item_dropdown_with_icon, items) { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val view = super.getView(position, convertView, parent) as TextView - val item = getItem(position) - if (item != null) { + getItem(position)?.let { item -> view.text = item.text - val drawable = ContextCompat.getDrawable(context, item.iconRes)?.mutate() - drawable?.let { - viewThemeUtils.platform.tintDrawable(fragment.requireContext(), it, ColorRole.ON_SURFACE) - } - view.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) + view.setCompoundDrawablesWithIntrinsicBounds(tintedDrawable(item), null, null, null) } return view } } - binding.sensitivityLabelAutoComplete.setAdapter(adapter) - val defaultSelectedItem = items.firstOrNull() - if (defaultSelectedItem != null) { - binding.sensitivityLabelAutoComplete.setText(defaultSelectedItem.text, false) - val drawable = ContextCompat.getDrawable(fragment.requireContext(), defaultSelectedItem.iconRes)?.mutate() - drawable?.let { - viewThemeUtils.platform.tintDrawable(fragment.requireContext(), it, ColorRole.ON_SURFACE) - } - binding.sensitivityLabelAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) - binding.sensitivityLabelAutoComplete.compoundDrawablePadding = - fragment.resources.getDimensionPixelSize(R.dimen.standard_padding) - } + private fun applySelection(autoComplete: MaterialAutoCompleteTextView, item: GovernanceLabel) { + autoComplete.setText(item.text, false) + autoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(tintedDrawable(item), null, null, null) + autoComplete.compoundDrawablePadding = fragment.resources.getDimensionPixelSize(R.dimen.standard_padding) + } - binding.sensitivityLabelAutoComplete.setOnItemClickListener { _, _, position, _ -> - val selected = items[position] - binding.sensitivityLabelAutoComplete.setText(selected.text, false) - val drawable = ContextCompat.getDrawable(fragment.requireContext(), selected.iconRes)?.mutate() - binding.sensitivityLabelAutoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) - binding.sensitivityLabelAutoComplete.compoundDrawablePadding = - fragment.resources.getDimensionPixelSize(R.dimen.standard_padding) + private fun tintedDrawable(item: GovernanceLabel) = + ContextCompat.getDrawable(context, item.iconRes)?.mutate()?.also { + viewThemeUtils.platform.tintDrawable(context, it, ColorRole.ON_SURFACE) } - - viewThemeUtils.material.colorTextInputLayout(binding.sensitivityLabel) - viewThemeUtils.material.colorTextInputLayout(binding.fileDetentionLabel) - } } diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/model/SensitivityLabel.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceLabel.kt similarity index 75% rename from app/src/main/java/com/nextcloud/ui/fileInfo/model/SensitivityLabel.kt rename to app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceLabel.kt index d9895ee6fe3a..1904c790c40d 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/model/SensitivityLabel.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceLabel.kt @@ -7,4 +7,4 @@ package com.nextcloud.ui.fileInfo.model -data class SensitivityLabel(val text: String, val iconRes: Int) +data class GovernanceLabel(val text: String, val iconRes: Int) diff --git a/app/src/main/java/com/owncloud/android/utils/theme/FilesSpecificViewThemeUtils.kt b/app/src/main/java/com/owncloud/android/utils/theme/FilesSpecificViewThemeUtils.kt index c52e6c509773..ccf8152a8f8d 100644 --- a/app/src/main/java/com/owncloud/android/utils/theme/FilesSpecificViewThemeUtils.kt +++ b/app/src/main/java/com/owncloud/android/utils/theme/FilesSpecificViewThemeUtils.kt @@ -28,6 +28,7 @@ import androidx.appcompat.widget.SearchView import androidx.core.content.res.ResourcesCompat import com.google.android.material.card.MaterialCardView import com.google.android.material.navigation.NavigationView +import com.google.android.material.textfield.MaterialAutoCompleteTextView import com.nextcloud.android.common.ui.color.ColorUtil import com.nextcloud.android.common.ui.theme.MaterialSchemes import com.nextcloud.android.common.ui.theme.ViewThemeUtilsBase @@ -312,6 +313,16 @@ class FilesSpecificViewThemeUtils @Inject constructor( } } + fun themeAutoCompleteTextView(autoCompleteTextView: MaterialAutoCompleteTextView) { + withScheme(autoCompleteTextView) { scheme -> + autoCompleteTextView.setTextColor(dynamicColor.onSurface().getArgb(scheme)) + autoCompleteTextView.setHintTextColor(dynamicColor.onSurfaceVariant().getArgb(scheme)) + autoCompleteTextView.setDropDownBackgroundTintList( + ColorStateList.valueOf(dynamicColor.surfaceContainer().getArgb(scheme)) + ) + } + } + companion object { private val TAG = FilesSpecificViewThemeUtils::class.simpleName From a9c23fa7843333cdf6c0d48327f3254357ddb2c9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 10:35:20 +0200 Subject: [PATCH 09/35] wip Signed-off-by: alperozturk96 --- .../com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt | 8 ++++---- app/src/main/res/layout/file_info_fragment.xml | 6 +++--- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt index a1cfad20f3ee..d951c0f55ab5 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt @@ -30,7 +30,7 @@ class GovernanceDetailInfo( fun init() { viewThemeUtils.material.themeCardView(binding.governanceLayout) initSensitivityLabel() - initFileDetentionLabel() + initFileRetentionLabel() } private fun initSensitivityLabel() { @@ -45,10 +45,10 @@ class GovernanceDetailInfo( ) } - private fun initFileDetentionLabel() { + private fun initFileRetentionLabel() { initDropdown( - textInputLayout = binding.fileDetentionLabel, - autoComplete = binding.fileDetentionAutoComplete, + textInputLayout = binding.fileRetentionLabel, + autoComplete = binding.fileRetentionAutoComplete, items = listOf( GovernanceLabel("Public", R.drawable.file_link), GovernanceLabel("Internal use only", R.drawable.ic_group), diff --git a/app/src/main/res/layout/file_info_fragment.xml b/app/src/main/res/layout/file_info_fragment.xml index 8b746716a2db..3ae7570c8462 100644 --- a/app/src/main/res/layout/file_info_fragment.xml +++ b/app/src/main/res/layout/file_info_fragment.xml @@ -61,14 +61,14 @@ + android:hint="@string/governance_file_retention_label"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0ebe2aa05d42..809f676b29f7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,7 +8,7 @@ Local contacts Sensitivity label - File detention + File retention Governance Image details From 89e25aa73dcb51317febd539eaeba6b3b4ccba7a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 11:03:09 +0200 Subject: [PATCH 10/35] wip Signed-off-by: alperozturk96 --- .../main/res/drawable/ic_dashboard_filled.xml | 17 ++++------------- .../main/res/drawable/ic_dashboard_outlined.xml | 17 ++++------------- .../main/res/layout/item_dropdown_with_icon.xml | 6 ++++++ 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/app/src/main/res/drawable/ic_dashboard_filled.xml b/app/src/main/res/drawable/ic_dashboard_filled.xml index 2d5b26893695..111b1e97f44a 100644 --- a/app/src/main/res/drawable/ic_dashboard_filled.xml +++ b/app/src/main/res/drawable/ic_dashboard_filled.xml @@ -1,18 +1,9 @@ + ~ SPDX-FileCopyrightText: 2018-2025 Google LLC + ~ SPDX-License-Identifier: Apache-2.0 +--> + ~ SPDX-FileCopyrightText: 2018-2025 Google LLC + ~ SPDX-License-Identifier: Apache-2.0 +--> + Date: Tue, 5 May 2026 11:09:18 +0200 Subject: [PATCH 11/35] wip Signed-off-by: alperozturk96 --- app/src/main/res/layout/item_dropdown_with_icon.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/item_dropdown_with_icon.xml b/app/src/main/res/layout/item_dropdown_with_icon.xml index 4df877f93896..ac31e3f898fd 100644 --- a/app/src/main/res/layout/item_dropdown_with_icon.xml +++ b/app/src/main/res/layout/item_dropdown_with_icon.xml @@ -6,10 +6,9 @@ ~ SPDX-License-Identifier: AGPL-3.0-or-later --> From e6daf81dad833ef3a6742911f4fae05a45679e5d Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Tue, 16 Jun 2026 09:41:15 +0200 Subject: [PATCH 12/35] show details depending on governance app Signed-off-by: tobiasKaminsky --- .../client/database/NextcloudDatabase.kt | 5 +- .../database/entity/CapabilityEntity.kt | 3 + .../database/migrations/Migration100to101.kt | 93 +++++++++++++++++++ .../nextcloud/ui/fileInfo/FileInfoFragment.kt | 9 +- .../datamodel/FileDataStorageManager.java | 2 + .../com/owncloud/android/db/ProviderMeta.java | 1 + .../ui/adapter/FileDetailTabAdapter.kt | 20 +++- .../ui/fragment/FileDetailFragment.java | 14 ++- settings.gradle.kts | 7 +- 9 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/database/migrations/Migration100to101.kt diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index abad90195739..788d09995dc7 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -40,6 +40,7 @@ import com.nextcloud.client.database.entity.VirtualEntity import com.nextcloud.client.database.migrations.DatabaseMigrationUtil import com.nextcloud.client.database.migrations.MIGRATION_88_89 import com.nextcloud.client.database.migrations.MIGRATION_97_98 +import com.nextcloud.client.database.migrations.MIGRATION_100_101 import com.nextcloud.client.database.migrations.MIGRATION_99_100 import com.nextcloud.client.database.migrations.Migration67to68 import com.nextcloud.client.database.migrations.RoomMigration @@ -99,7 +100,8 @@ import com.owncloud.android.db.ProviderMeta // manual migration used for 97 to 98 AutoMigration(from = 98, to = 99), // manual migration used for 99 to 100 - AutoMigration(from = 100, to = 101, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class) + // manual migration used for 100 to 101 + AutoMigration(from = 101, to = 102, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class) ], exportSchema = true ) @@ -140,6 +142,7 @@ abstract class NextcloudDatabase : RoomDatabase() { .addMigrations(MIGRATION_88_89) .addMigrations(MIGRATION_97_98) .addMigrations(MIGRATION_99_100) + .addMigrations(MIGRATION_100_101) .build() } return instance!! diff --git a/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt index 7cf3ed100ebf..ca8466a602ca 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt @@ -126,6 +126,8 @@ data class CapabilityEntity( val dropAccount: Int?, @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SECURITY_GUARD) val securityGuard: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_GOVERNANCE) + val governance: Int?, @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS) val forbiddenFileNameCharacters: String?, @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAMES) @@ -222,6 +224,7 @@ fun CapabilityEntity?.toOCCapability(): OCCapability { capability.groupfolders = intToBoolean(this.groupfolders) capability.dropAccount = intToBoolean(this.dropAccount) capability.securityGuard = intToBoolean(this.securityGuard) + capability.governance = intToBoolean(this.governance) capability.forbiddenFilenameCharactersJson = this.forbiddenFileNameCharacters capability.forbiddenFilenamesJson = this.forbiddenFileNames capability.forbiddenFilenameExtensionJson = this.forbiddenFileNameExtensions diff --git a/app/src/main/java/com/nextcloud/client/database/migrations/Migration100to101.kt b/app/src/main/java/com/nextcloud/client/database/migrations/Migration100to101.kt new file mode 100644 index 000000000000..e4b5232859d1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/migrations/Migration100to101.kt @@ -0,0 +1,93 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +@Suppress("MagicNumber", "LongMethod") +val MIGRATION_100_101 = object : Migration(100, 101) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS capabilities") + + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS capabilities ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + account TEXT, + version_mayor INTEGER, + version_minor INTEGER, + version_micro INTEGER, + version_string TEXT, + version_edition TEXT, + extended_support INTEGER, + core_pollinterval INTEGER, + sharing_api_enabled INTEGER, + sharing_public_enabled INTEGER, + sharing_public_password_enforced INTEGER, + sharing_public_ask_for_optional_password INTEGER, + sharing_public_expire_date_enabled INTEGER, + sharing_public_expire_date_days INTEGER, + sharing_public_expire_date_enforced INTEGER, + sharing_public_send_mail INTEGER, + sharing_public_upload INTEGER, + sharing_user_send_mail INTEGER, + sharing_resharing INTEGER, + sharing_federation_outgoing INTEGER, + sharing_federation_incoming INTEGER, + files_bigfilechunking INTEGER, + files_undelete INTEGER, + files_versioning INTEGER, + files_locking_version TEXT, + external_links INTEGER, + server_name TEXT, + server_color TEXT, + server_text_color TEXT, + server_element_color TEXT, + background_url TEXT, + server_slogan TEXT, + server_logo TEXT, + background_default INTEGER, + background_plain INTEGER, + end_to_end_encryption INTEGER, + end_to_end_encryption_keys_exist INTEGER, + end_to_end_encryption_api_version TEXT, + activity INTEGER, + richdocument INTEGER, + recommendation INTEGER, + richdocument_mimetype_list TEXT, + richdocument_optional_mimetype_list TEXT, + richdocument_direct_editing INTEGER, + richdocument_direct_templates INTEGER, + richdocument_product_name TEXT, + direct_editing_etag TEXT, + etag TEXT, + user_status INTEGER, + user_status_supports_emoji INTEGER, + user_status_supports_busy INTEGER, + assistant INTEGER, + groupfolders INTEGER, + drop_account INTEGER, + security_guard INTEGER, + governance INTEGER, + forbidden_filename_characters TEXT, + forbidden_filenames TEXT, + forbidden_filename_extensions TEXT, + forbidden_filename_basenames TEXT, + windows_compatible_filenames INTEGER, + files_download_limit INTEGER, + files_download_limit_default INTEGER, + notes_folder_path TEXT, + default_permissions INTEGER, + has_valid_subscription INTEGER, + client_integration_json TEXT + ) + """.trimIndent() + ) + } +} diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt index 3eddf0c3e0ac..ce2c2f6b51ee 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt @@ -19,6 +19,7 @@ import com.nextcloud.client.di.Injectable import com.owncloud.android.databinding.FileInfoFragmentBinding import com.owncloud.android.datamodel.OCFile import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.theme.CapabilityUtils import com.owncloud.android.utils.theme.ViewThemeUtils import javax.inject.Inject @@ -46,8 +47,12 @@ class FileInfoFragment : file?.let { imageDetailInfo.init(it, binding) } } - val governanceDetailInfo = GovernanceDetailInfo(binding, viewThemeUtils, this) - governanceDetailInfo.init() + if (CapabilityUtils.getCapability(context).governance.isTrue) { + val governanceDetailInfo = GovernanceDetailInfo(binding, viewThemeUtils, this) + governanceDetailInfo.init() + } else { + binding.governanceLayout.visibility = View.GONE + } return binding.root } diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 4d1daf8835b4..1df445d5d3e6 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -2412,6 +2412,7 @@ private ContentValues createContentValues(String accountName, OCCapability capab contentValues.put(ProviderTableMeta.CAPABILITIES_GROUPFOLDERS, capability.getGroupfolders().getValue()); contentValues.put(ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT, capability.getDropAccount().getValue()); contentValues.put(ProviderTableMeta.CAPABILITIES_SECURITY_GUARD, capability.getSecurityGuard().getValue()); + contentValues.put(ProviderTableMeta.CAPABILITIES_GOVERNANCE, capability.getGovernance().getValue()); contentValues.put(ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS, capability.getForbiddenFilenameCharactersJson()); contentValues.put(ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAMES, capability.getForbiddenFilenamesJson()); @@ -2613,6 +2614,7 @@ private OCCapability createCapabilityInstance(Cursor cursor) { capability.setGroupfolders(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_GROUPFOLDERS)); capability.setDropAccount(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT)); capability.setSecurityGuard(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_SECURITY_GUARD)); + capability.setGovernance(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_GOVERNANCE)); capability.setForbiddenFilenameCharactersJson(getString(cursor, ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS)); capability.setForbiddenFilenamesJson(getString(cursor, ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAMES)); diff --git a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java index 03fe56306fc2..830d7c876fb7 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -282,6 +282,7 @@ static public class ProviderTableMeta implements BaseColumns { public static final String CAPABILITIES_GROUPFOLDERS = "groupfolders"; public static final String CAPABILITIES_DROP_ACCOUNT = "drop_account"; public static final String CAPABILITIES_SECURITY_GUARD = "security_guard"; + public static final String CAPABILITIES_GOVERNANCE = "governance"; public static final String CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS = "forbidden_filename_characters"; public static final String CAPABILITIES_FORBIDDEN_FILENAMES = "forbidden_filenames"; public static final String CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_EXTENSIONS = "forbidden_filename_extensions"; diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt index 9fbcba02eb8f..7633297ce066 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.kt @@ -17,10 +17,11 @@ import com.owncloud.android.ui.fragment.FileDetailActivitiesFragment import com.owncloud.android.ui.fragment.FileDetailSharingFragment class FileDetailTabAdapter( - fragmentActivity: FragmentActivity, + private val fragmentActivity: FragmentActivity, private val file: OCFile, private val user: User, - private val showSharingTab: Boolean + private val showSharingTab: Boolean, + private val showDetailsTab: Boolean ) : FragmentStateAdapter(fragmentActivity) { private enum class Tab(val position: Int) { @@ -44,5 +45,18 @@ class FileDetailTabAdapter( .also { fileDetailActivitiesFragment = it } } - override fun getItemCount(): Int = if (showSharingTab) Tab.entries.size else Tab.entries.size - 1 + override fun getItemCount(): Int { + // always show Activities + var count = 1 + + if (showSharingTab) { + count++ + } + + if (showDetailsTab) { + count++ + } + + return count + } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 2f5a717b3d2f..3a7e4dd66566 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -36,7 +36,6 @@ import com.nextcloud.ui.fileactions.FileActionsBottomSheet; import com.nextcloud.utils.MenuUtils; import com.nextcloud.utils.extensions.BundleExtensionsKt; -import com.nextcloud.utils.extensions.FileExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; import com.owncloud.android.MainApp; import com.owncloud.android.R; @@ -65,6 +64,7 @@ import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.EncryptionUtils; import com.owncloud.android.utils.MimeTypeUtil; +import com.owncloud.android.utils.theme.CapabilityUtils; import com.owncloud.android.utils.theme.ViewThemeUtils; import org.greenrobot.eventbus.EventBus; @@ -316,14 +316,17 @@ private void setupViewPager() { binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.share_dialog_title).setIcon(R.drawable.selector_tab_share)); } - binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.filedetails_details).setIcon(R.drawable.selector_file_info)); + if (showDetailsTab()) { + binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.filedetails_details).setIcon(R.drawable.selector_file_info)); + } viewThemeUtils.material.themeTabLayout(binding.tabLayout); final FileDetailTabAdapter adapter = new FileDetailTabAdapter(requireActivity(), getFile(), user, - showSharingTab()); + showSharingTab(), + showDetailsTab()); binding.pager.setAdapter(adapter); binding.pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @@ -898,4 +901,9 @@ private boolean showSharingTab() { return true; } } + + private boolean showDetailsTab() { + return CapabilityUtils.getCapability(getContext()).getGovernance().isTrue() || + MimeTypeUtil.isMedia(getFile().getMimeType()); + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 651d7a880838..7a4fca194396 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,20 +43,19 @@ dependencyResolutionManagement { /* Needed for local android library - -includeBuild("../android_library") { +*/ +includeBuild("../android-library") { dependencySubstitution { substitute(module("com.github.nextcloud:android-library")) .using(project(":library")) } } -*/ /* Needed for local android common library -includeBuild("../android_common") { +includeBuild("../android-common") { dependencySubstitution { substitute(module("com.github.nextcloud.android-common:core")) .using(project(":core")) From 050219a409d162a13962db54dc6fe05a4af51779 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Tue, 23 Jun 2026 08:16:08 +0200 Subject: [PATCH 13/35] wip Signed-off-by: tobiasKaminsky --- .../nextcloud/ui/fileInfo/FileInfoFragment.kt | 14 ++- .../ui/fileInfo/GovernanceDetailInfo.kt | 119 ++++++++++++++---- .../ui/fileInfo/model/GovernanceLabel.kt | 2 +- app/src/main/res/values/dims.xml | 1 + 4 files changed, 109 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt index ce2c2f6b51ee..e4a9ff7bd887 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt @@ -16,6 +16,7 @@ import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment import com.nextcloud.client.account.User import com.nextcloud.client.di.Injectable +import com.nextcloud.client.network.ClientFactory import com.owncloud.android.databinding.FileInfoFragmentBinding import com.owncloud.android.datamodel.OCFile import com.owncloud.android.utils.MimeTypeUtil @@ -28,17 +29,20 @@ class FileInfoFragment : Injectable { private lateinit var binding: FileInfoFragmentBinding - private val file: OCFile? by lazy { + val file: OCFile? by lazy { arguments?.let { BundleCompat.getParcelable(it, ARG_FILE, OCFile::class.java) } } - private val user: User? by lazy { + val user: User? by lazy { arguments?.let { BundleCompat.getParcelable(it, ARG_USER, User::class.java) } } @Inject lateinit var viewThemeUtils: ViewThemeUtils + @Inject + lateinit var clientFactory: ClientFactory + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FileInfoFragmentBinding.inflate(layoutInflater, container, false) @@ -47,14 +51,16 @@ class FileInfoFragment : file?.let { imageDetailInfo.init(it, binding) } } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { if (CapabilityUtils.getCapability(context).governance.isTrue) { val governanceDetailInfo = GovernanceDetailInfo(binding, viewThemeUtils, this) governanceDetailInfo.init() } else { binding.governanceLayout.visibility = View.GONE } - - return binding.root } override fun onSaveInstanceState(outState: Bundle) { diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt index d951c0f55ab5..2debb8b84f62 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt @@ -7,18 +7,30 @@ package com.nextcloud.ui.fileInfo +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.TextView import androidx.core.content.ContextCompat +import androidx.core.graphics.toColorInt +import androidx.lifecycle.lifecycleScope import com.google.android.material.textfield.MaterialAutoCompleteTextView import com.google.android.material.textfield.TextInputLayout -import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.android.lib.resources.governance.GetAvailableRetentionLabelsRemoteOperation +import com.nextcloud.android.lib.resources.governance.GetAvailableSensitivityLabelsRemoteOperation +import com.nextcloud.android.lib.resources.governance.RetentionLabelInfo +import com.nextcloud.android.lib.resources.governance.SensitivityLabelInfo +import com.nextcloud.client.network.ClientFactoryImpl import com.nextcloud.ui.fileInfo.model.GovernanceLabel import com.owncloud.android.R import com.owncloud.android.databinding.FileInfoFragmentBinding +import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class GovernanceDetailInfo( private val binding: FileInfoFragmentBinding, @@ -34,29 +46,77 @@ class GovernanceDetailInfo( } private fun initSensitivityLabel() { - initDropdown( - textInputLayout = binding.sensitivityLabel, - autoComplete = binding.sensitivityLabelAutoComplete, - items = listOf( - GovernanceLabel("Sharing restricted", R.drawable.ic_share), - GovernanceLabel("Download restricted", R.drawable.ic_download_grey600), - GovernanceLabel("Upload restricted", R.drawable.uploads) + fragment.lifecycleScope.launch { + val labels = withContext(Dispatchers.IO) { fetchAvailableSensitivityLabels() } + + if (labels.isEmpty()) { + binding.sensitivityLabel.visibility = View.GONE + return@launch + } + + initDropdown( + textInputLayout = binding.sensitivityLabel, + autoComplete = binding.sensitivityLabelAutoComplete, + items = labels ) - ) + } + } + + @Suppress("TooGenericExceptionCaught") + private fun fetchAvailableSensitivityLabels(): List = try { + val user = fragment.user ?: return emptyList() + val file = fragment.file ?: return emptyList() + val client = ClientFactoryImpl(context).createNextcloudClient(user) + val result = GetAvailableSensitivityLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) + + if (result.isSuccess) { + result.resultData.orEmpty().map { it.toGovernanceLabel() } + } else { + emptyList() + } + } catch (e: Exception) { + Log_OC.e(TAG, "Could not fetch available sensitivity labels", e) + emptyList() } + private fun SensitivityLabelInfo.toGovernanceLabel() = GovernanceLabel(name, color) + private fun initFileRetentionLabel() { - initDropdown( - textInputLayout = binding.fileRetentionLabel, - autoComplete = binding.fileRetentionAutoComplete, - items = listOf( - GovernanceLabel("Public", R.drawable.file_link), - GovernanceLabel("Internal use only", R.drawable.ic_group), - GovernanceLabel("Restricted", R.drawable.ic_cancel) + fragment.lifecycleScope.launch { + val labels = withContext(Dispatchers.IO) { fetchAvailableRetentionLabels() } + + if (labels.isEmpty()) { + binding.fileRetentionLabel.visibility = View.GONE + return@launch + } + + initDropdown( + textInputLayout = binding.fileRetentionLabel, + autoComplete = binding.fileRetentionAutoComplete, + items = labels ) - ) + } + } + + @Suppress("TooGenericExceptionCaught") + private fun fetchAvailableRetentionLabels(): List = try { + val user = fragment.user ?: return emptyList() + val file = fragment.file ?: return emptyList() + val client = ClientFactoryImpl(context).createNextcloudClient(user) + val result = GetAvailableRetentionLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) + + if (result.isSuccess) { + result.resultData.orEmpty().map { it.toGovernanceLabel() } + } else { + emptyList() + } + } catch (e: Exception) { + Log_OC.e(TAG, "Could not fetch available retention labels", e) + emptyList() } + private fun RetentionLabelInfo.toGovernanceLabel() = GovernanceLabel(name, color) + private fun initDropdown( textInputLayout: TextInputLayout, autoComplete: MaterialAutoCompleteTextView, @@ -80,7 +140,7 @@ class GovernanceDetailInfo( val view = super.getView(position, convertView, parent) as TextView getItem(position)?.let { item -> view.text = item.text - view.setCompoundDrawablesWithIntrinsicBounds(tintedDrawable(item), null, null, null) + view.setCompoundDrawablesWithIntrinsicBounds(colorDot(item.color), null, null, null) } return view } @@ -88,12 +148,27 @@ class GovernanceDetailInfo( private fun applySelection(autoComplete: MaterialAutoCompleteTextView, item: GovernanceLabel) { autoComplete.setText(item.text, false) - autoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(tintedDrawable(item), null, null, null) + autoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(colorDot(item.color), null, null, null) autoComplete.compoundDrawablePadding = fragment.resources.getDimensionPixelSize(R.dimen.standard_padding) } - private fun tintedDrawable(item: GovernanceLabel) = - ContextCompat.getDrawable(context, item.iconRes)?.mutate()?.also { - viewThemeUtils.platform.tintDrawable(context, it, ColorRole.ON_SURFACE) + private fun colorDot(color: String): Drawable { + val size = context.resources.getDimensionPixelSize(R.dimen.governance_color_dot_size) + return GradientDrawable().apply { + shape = GradientDrawable.OVAL + setSize(size, size) + setColor(parseColor(color)) } + } + + private fun parseColor(color: String): Int = runCatching { color.toColorInt() } + .getOrElse { + Log_OC.w(TAG, "Could not parse label color: $color") + ContextCompat.getColor(context, R.color.grey_600) + } + + companion object { + private val TAG = GovernanceDetailInfo::class.java.simpleName + private const val ENTITY_TYPE_FILES = "FILES" + } } diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceLabel.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceLabel.kt index 1904c790c40d..b8f1f4749cc5 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceLabel.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceLabel.kt @@ -7,4 +7,4 @@ package com.nextcloud.ui.fileInfo.model -data class GovernanceLabel(val text: String, val iconRes: Int) +data class GovernanceLabel(val text: String, val color: String) diff --git a/app/src/main/res/values/dims.xml b/app/src/main/res/values/dims.xml index 1123e39c7597..7c949e01fbb8 100644 --- a/app/src/main/res/values/dims.xml +++ b/app/src/main/res/values/dims.xml @@ -31,6 +31,7 @@ 8dp 4dp 32dp + 12dp 2dp 16dp 32dp From a544aee913a52216f5156cc3f4a129ff3d28322f Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Tue, 23 Jun 2026 09:06:39 +0200 Subject: [PATCH 14/35] wip Signed-off-by: tobiasKaminsky --- .../nextcloud/client/di/ViewModelModule.kt | 6 ++ .../nextcloud/ui/fileInfo/FileInfoFragment.kt | 17 +++- .../ui/fileInfo/FileInfoViewModel.kt | 74 ++++++++++++++++ .../ui/fileInfo/GovernanceDetailInfo.kt | 84 ++++--------------- .../main/res/layout/file_info_fragment.xml | 4 + 5 files changed, 113 insertions(+), 72 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt diff --git a/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt b/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt index eb3e98a6c570..c0a3c1a85927 100644 --- a/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt @@ -12,6 +12,7 @@ import androidx.lifecycle.ViewModelProvider import com.nextcloud.client.documentscan.DocumentScanViewModel import com.nextcloud.client.etm.EtmViewModel import com.nextcloud.client.logger.ui.LogsViewModel +import com.nextcloud.ui.fileInfo.FileInfoViewModel import com.nextcloud.ui.fileactions.FileActionsViewModel import com.owncloud.android.ui.preview.pdf.PreviewPdfViewModel import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsViewModel @@ -57,6 +58,11 @@ abstract class ViewModelModule { @ViewModelKey(TrashbinFileActionsViewModel::class) abstract fun trashbinFileActionsViewModel(vm: TrashbinFileActionsViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(FileInfoViewModel::class) + abstract fun fileInfoViewModel(vm: FileInfoViewModel): ViewModel + @Binds abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory } diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt index e4a9ff7bd887..d3c792745275 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt @@ -14,9 +14,10 @@ import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider import com.nextcloud.client.account.User import com.nextcloud.client.di.Injectable -import com.nextcloud.client.network.ClientFactory +import com.nextcloud.client.di.ViewModelFactory import com.owncloud.android.databinding.FileInfoFragmentBinding import com.owncloud.android.datamodel.OCFile import com.owncloud.android.utils.MimeTypeUtil @@ -41,7 +42,17 @@ class FileInfoFragment : lateinit var viewThemeUtils: ViewThemeUtils @Inject - lateinit var clientFactory: ClientFactory + lateinit var vmFactory: ViewModelFactory + + private lateinit var viewModel: FileInfoViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel = ViewModelProvider(this, vmFactory)[FileInfoViewModel::class.java] + val file = file ?: return + val user = user ?: return + viewModel.load(file, user) + } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FileInfoFragmentBinding.inflate(layoutInflater, container, false) @@ -56,7 +67,7 @@ class FileInfoFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { if (CapabilityUtils.getCapability(context).governance.isTrue) { - val governanceDetailInfo = GovernanceDetailInfo(binding, viewThemeUtils, this) + val governanceDetailInfo = GovernanceDetailInfo(binding, viewThemeUtils, this, viewModel) governanceDetailInfo.init() } else { binding.governanceLayout.visibility = View.GONE diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt new file mode 100644 index 000000000000..80b300234bbb --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt @@ -0,0 +1,74 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.fileInfo + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.android.lib.resources.governance.GetAvailableRetentionLabelsRemoteOperation +import com.nextcloud.android.lib.resources.governance.GetAvailableSensitivityLabelsRemoteOperation +import com.nextcloud.android.lib.resources.governance.RetentionLabelInfo +import com.nextcloud.android.lib.resources.governance.SensitivityLabelInfo +import com.nextcloud.client.account.User +import com.nextcloud.client.network.ClientFactory +import com.nextcloud.ui.fileInfo.model.GovernanceLabel +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +class FileInfoViewModel @Inject constructor( + private val clientFactory: ClientFactory +) : ViewModel() { + + private val _sensitivityLabels = MutableStateFlow?>(null) + val sensitivityLabels: StateFlow?> = _sensitivityLabels + + private val _retentionLabels = MutableStateFlow?>(null) + val retentionLabels: StateFlow?> = _retentionLabels + + fun load(file: OCFile, user: User) { + viewModelScope.launch(Dispatchers.IO) { + _sensitivityLabels.value = fetchSensitivityLabels(file, user) + } + viewModelScope.launch(Dispatchers.IO) { + _retentionLabels.value = fetchRetentionLabels(file, user) + } + } + + @Suppress("TooGenericExceptionCaught") + private fun fetchSensitivityLabels(file: OCFile, user: User): List = try { + val client = clientFactory.createNextcloudClient(user) + val result = GetAvailableSensitivityLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) + if (result.isSuccess) result.resultData.orEmpty().map { it.toGovernanceLabel() } else emptyList() + } catch (e: Exception) { + Log_OC.e(TAG, "Could not fetch available sensitivity labels", e) + emptyList() + } + + @Suppress("TooGenericExceptionCaught") + private fun fetchRetentionLabels(file: OCFile, user: User): List = try { + val client = clientFactory.createNextcloudClient(user) + val result = GetAvailableRetentionLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) + if (result.isSuccess) result.resultData.orEmpty().map { it.toGovernanceLabel() } else emptyList() + } catch (e: Exception) { + Log_OC.e(TAG, "Could not fetch available retention labels", e) + emptyList() + } + + private fun SensitivityLabelInfo.toGovernanceLabel() = GovernanceLabel(name, color) + + private fun RetentionLabelInfo.toGovernanceLabel() = GovernanceLabel(name, color) + + companion object { + private val TAG = FileInfoViewModel::class.java.simpleName + private const val ENTITY_TYPE_FILES = "FILES" + } +} diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt index 2debb8b84f62..56d694a6da99 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt @@ -18,110 +18,57 @@ import androidx.core.graphics.toColorInt import androidx.lifecycle.lifecycleScope import com.google.android.material.textfield.MaterialAutoCompleteTextView import com.google.android.material.textfield.TextInputLayout -import com.nextcloud.android.lib.resources.governance.GetAvailableRetentionLabelsRemoteOperation -import com.nextcloud.android.lib.resources.governance.GetAvailableSensitivityLabelsRemoteOperation -import com.nextcloud.android.lib.resources.governance.RetentionLabelInfo -import com.nextcloud.android.lib.resources.governance.SensitivityLabelInfo -import com.nextcloud.client.network.ClientFactoryImpl import com.nextcloud.ui.fileInfo.model.GovernanceLabel import com.owncloud.android.R import com.owncloud.android.databinding.FileInfoFragmentBinding import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.theme.ViewThemeUtils -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class GovernanceDetailInfo( private val binding: FileInfoFragmentBinding, private val viewThemeUtils: ViewThemeUtils, - private val fragment: FileInfoFragment + private val fragment: FileInfoFragment, + private val viewModel: FileInfoViewModel ) { private val context get() = fragment.requireContext() fun init() { viewThemeUtils.material.themeCardView(binding.governanceLayout) - initSensitivityLabel() - initFileRetentionLabel() + collectSensitivityLabels() + collectRetentionLabels() } - private fun initSensitivityLabel() { + private fun collectSensitivityLabels() { fragment.lifecycleScope.launch { - val labels = withContext(Dispatchers.IO) { fetchAvailableSensitivityLabels() } - + val labels = viewModel.sensitivityLabels.filterNotNull().first() if (labels.isEmpty()) { binding.sensitivityLabel.visibility = View.GONE - return@launch + } else { + initDropdown(binding.sensitivityLabel, binding.sensitivityLabelAutoComplete, labels) } - - initDropdown( - textInputLayout = binding.sensitivityLabel, - autoComplete = binding.sensitivityLabelAutoComplete, - items = labels - ) } } - @Suppress("TooGenericExceptionCaught") - private fun fetchAvailableSensitivityLabels(): List = try { - val user = fragment.user ?: return emptyList() - val file = fragment.file ?: return emptyList() - val client = ClientFactoryImpl(context).createNextcloudClient(user) - val result = GetAvailableSensitivityLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) - - if (result.isSuccess) { - result.resultData.orEmpty().map { it.toGovernanceLabel() } - } else { - emptyList() - } - } catch (e: Exception) { - Log_OC.e(TAG, "Could not fetch available sensitivity labels", e) - emptyList() - } - - private fun SensitivityLabelInfo.toGovernanceLabel() = GovernanceLabel(name, color) - - private fun initFileRetentionLabel() { + private fun collectRetentionLabels() { fragment.lifecycleScope.launch { - val labels = withContext(Dispatchers.IO) { fetchAvailableRetentionLabels() } - + val labels = viewModel.retentionLabels.filterNotNull().first() if (labels.isEmpty()) { binding.fileRetentionLabel.visibility = View.GONE - return@launch + } else { + initDropdown(binding.fileRetentionLabel, binding.fileRetentionAutoComplete, labels) } - - initDropdown( - textInputLayout = binding.fileRetentionLabel, - autoComplete = binding.fileRetentionAutoComplete, - items = labels - ) } } - @Suppress("TooGenericExceptionCaught") - private fun fetchAvailableRetentionLabels(): List = try { - val user = fragment.user ?: return emptyList() - val file = fragment.file ?: return emptyList() - val client = ClientFactoryImpl(context).createNextcloudClient(user) - val result = GetAvailableRetentionLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) - - if (result.isSuccess) { - result.resultData.orEmpty().map { it.toGovernanceLabel() } - } else { - emptyList() - } - } catch (e: Exception) { - Log_OC.e(TAG, "Could not fetch available retention labels", e) - emptyList() - } - - private fun RetentionLabelInfo.toGovernanceLabel() = GovernanceLabel(name, color) - private fun initDropdown( textInputLayout: TextInputLayout, autoComplete: MaterialAutoCompleteTextView, items: List ) { + textInputLayout.visibility = View.VISIBLE viewThemeUtils.material.colorTextInputLayout(textInputLayout) viewThemeUtils.files.themeAutoCompleteTextView(autoComplete) @@ -169,6 +116,5 @@ class GovernanceDetailInfo( companion object { private val TAG = GovernanceDetailInfo::class.java.simpleName - private const val ENTITY_TYPE_FILES = "FILES" } } diff --git a/app/src/main/res/layout/file_info_fragment.xml b/app/src/main/res/layout/file_info_fragment.xml index 3ae7570c8462..10603b333e73 100644 --- a/app/src/main/res/layout/file_info_fragment.xml +++ b/app/src/main/res/layout/file_info_fragment.xml @@ -49,6 +49,8 @@ style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu" android:layout_width="match_parent" android:layout_height="wrap_content" + android:visibility="gone" + tools:visibility="visible" android:layout_marginBottom="@dimen/standard_margin" android:hint="@string/governance_sensitivity_label"> @@ -65,6 +67,8 @@ style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu" android:layout_width="match_parent" android:layout_height="wrap_content" + android:visibility="gone" + tools:visibility="visible" android:hint="@string/governance_file_retention_label"> Date: Tue, 23 Jun 2026 10:47:15 +0200 Subject: [PATCH 15/35] wip Signed-off-by: tobiasKaminsky --- .../ui/fileInfo/FileInfoViewModel.kt | 71 ++++++++++++++++++- .../ui/fileInfo/GovernanceDetailInfo.kt | 48 ++++++++++--- .../ui/fileInfo/model/GovernanceLabel.kt | 4 +- 3 files changed, 109 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt index 80b300234bbb..267d76632e09 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt @@ -9,10 +9,15 @@ package com.nextcloud.ui.fileInfo import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.nextcloud.android.lib.resources.governance.EntityLabels import com.nextcloud.android.lib.resources.governance.GetAvailableRetentionLabelsRemoteOperation import com.nextcloud.android.lib.resources.governance.GetAvailableSensitivityLabelsRemoteOperation +import com.nextcloud.android.lib.resources.governance.GetEntityLabelsRemoteOperation +import com.nextcloud.android.lib.resources.governance.LabelType +import com.nextcloud.android.lib.resources.governance.RemoveLabelRemoteOperation import com.nextcloud.android.lib.resources.governance.RetentionLabelInfo import com.nextcloud.android.lib.resources.governance.SensitivityLabelInfo +import com.nextcloud.android.lib.resources.governance.SetLabelRemoteOperation import com.nextcloud.client.account.User import com.nextcloud.client.network.ClientFactory import com.nextcloud.ui.fileInfo.model.GovernanceLabel @@ -34,13 +39,47 @@ class FileInfoViewModel @Inject constructor( private val _retentionLabels = MutableStateFlow?>(null) val retentionLabels: StateFlow?> = _retentionLabels + private val _currentSensitivityLabelId = MutableStateFlow(null) + val currentSensitivityLabelId: StateFlow = _currentSensitivityLabelId + + private var cachedFile: OCFile? = null + private var cachedUser: User? = null + fun load(file: OCFile, user: User) { + cachedFile = file + cachedUser = user viewModelScope.launch(Dispatchers.IO) { _sensitivityLabels.value = fetchSensitivityLabels(file, user) } viewModelScope.launch(Dispatchers.IO) { _retentionLabels.value = fetchRetentionLabels(file, user) } + viewModelScope.launch(Dispatchers.IO) { + _currentSensitivityLabelId.value = fetchEntityLabels(file, user)?.sensitivity?.id ?: "" + } + } + + fun setSensitivityLabel(labelId: String) { + val file = cachedFile ?: return + val user = cachedUser ?: return + viewModelScope.launch(Dispatchers.IO) { + val result = applyLabel(file, user, LabelType.SENSITIVITY, labelId) + if (result) { + _currentSensitivityLabelId.value = labelId + } + } + } + + fun removeSensitivityLabel() { + val labelId = _currentSensitivityLabelId.value?.takeIf { it.isNotEmpty() } ?: return + val file = cachedFile ?: return + val user = cachedUser ?: return + viewModelScope.launch(Dispatchers.IO) { + val result = removeLabel(file, user, LabelType.SENSITIVITY, labelId) + if (result) { + _currentSensitivityLabelId.value = "" + } + } } @Suppress("TooGenericExceptionCaught") @@ -63,9 +102,37 @@ class FileInfoViewModel @Inject constructor( emptyList() } - private fun SensitivityLabelInfo.toGovernanceLabel() = GovernanceLabel(name, color) + @Suppress("TooGenericExceptionCaught") + private fun fetchEntityLabels(file: OCFile, user: User): EntityLabels? = try { + val client = clientFactory.createNextcloudClient(user) + val result = GetEntityLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId.toString()).execute(client) + if (result.isSuccess) result.resultData else null + } catch (e: Exception) { + Log_OC.e(TAG, "Could not fetch entity labels", e) + null + } + + @Suppress("TooGenericExceptionCaught") + private fun applyLabel(file: OCFile, user: User, labelType: LabelType, labelId: String): Boolean = try { + val client = clientFactory.createNextcloudClient(user) + SetLabelRemoteOperation(ENTITY_TYPE_FILES, file.localId, labelType, labelId).execute(client).isSuccess + } catch (e: Exception) { + Log_OC.e(TAG, "Could not set label", e) + false + } + + @Suppress("TooGenericExceptionCaught") + private fun removeLabel(file: OCFile, user: User, labelType: LabelType, labelId: String): Boolean = try { + val client = clientFactory.createNextcloudClient(user) + RemoveLabelRemoteOperation(ENTITY_TYPE_FILES, file.localId, labelType, labelId).execute(client).isSuccess + } catch (e: Exception) { + Log_OC.e(TAG, "Could not remove label", e) + false + } + + private fun SensitivityLabelInfo.toGovernanceLabel() = GovernanceLabel(id, name, color) - private fun RetentionLabelInfo.toGovernanceLabel() = GovernanceLabel(name, color) + private fun RetentionLabelInfo.toGovernanceLabel() = GovernanceLabel(id, name, color) companion object { private val TAG = FileInfoViewModel::class.java.simpleName diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt index 56d694a6da99..5f980e4a4a88 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt @@ -46,8 +46,22 @@ class GovernanceDetailInfo( val labels = viewModel.sensitivityLabels.filterNotNull().first() if (labels.isEmpty()) { binding.sensitivityLabel.visibility = View.GONE - } else { - initDropdown(binding.sensitivityLabel, binding.sensitivityLabelAutoComplete, labels) + return@launch + } + val currentId = viewModel.currentSensitivityLabelId.filterNotNull().first() + val items = listOf(GovernanceLabel("", "", "")) + labels + val initialItem = items.find { it.id == currentId } ?: items.first() + initDropdown( + textInputLayout = binding.sensitivityLabel, + autoComplete = binding.sensitivityLabelAutoComplete, + items = items, + initialItem = initialItem + ) { label -> + if (label.id.isEmpty()) { + viewModel.removeSensitivityLabel() + } else { + viewModel.setSensitivityLabel(label.id) + } } } } @@ -57,27 +71,34 @@ class GovernanceDetailInfo( val labels = viewModel.retentionLabels.filterNotNull().first() if (labels.isEmpty()) { binding.fileRetentionLabel.visibility = View.GONE - } else { - initDropdown(binding.fileRetentionLabel, binding.fileRetentionAutoComplete, labels) + return@launch } + initDropdown( + textInputLayout = binding.fileRetentionLabel, + autoComplete = binding.fileRetentionAutoComplete, + items = labels + ) } } private fun initDropdown( textInputLayout: TextInputLayout, autoComplete: MaterialAutoCompleteTextView, - items: List + items: List, + initialItem: GovernanceLabel? = items.firstOrNull(), + onSelect: (GovernanceLabel) -> Unit = {} ) { textInputLayout.visibility = View.VISIBLE viewThemeUtils.material.colorTextInputLayout(textInputLayout) viewThemeUtils.files.themeAutoCompleteTextView(autoComplete) autoComplete.setAdapter(buildAdapter(items)) - - items.firstOrNull()?.let { applySelection(autoComplete, it) } + initialItem?.let { applySelection(autoComplete, it) } autoComplete.setOnItemClickListener { _, _, position, _ -> - applySelection(autoComplete, items[position]) + val item = items[position] + applySelection(autoComplete, item) + onSelect(item) } } @@ -87,7 +108,8 @@ class GovernanceDetailInfo( val view = super.getView(position, convertView, parent) as TextView getItem(position)?.let { item -> view.text = item.text - view.setCompoundDrawablesWithIntrinsicBounds(colorDot(item.color), null, null, null) + val dot = if (item.color.isNotEmpty()) colorDot(item.color) else null + view.setCompoundDrawablesWithIntrinsicBounds(dot, null, null, null) } return view } @@ -95,8 +117,12 @@ class GovernanceDetailInfo( private fun applySelection(autoComplete: MaterialAutoCompleteTextView, item: GovernanceLabel) { autoComplete.setText(item.text, false) - autoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(colorDot(item.color), null, null, null) - autoComplete.compoundDrawablePadding = fragment.resources.getDimensionPixelSize(R.dimen.standard_padding) + if (item.color.isNotEmpty()) { + autoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(colorDot(item.color), null, null, null) + autoComplete.compoundDrawablePadding = fragment.resources.getDimensionPixelSize(R.dimen.standard_padding) + } else { + autoComplete.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null) + } } private fun colorDot(color: String): Drawable { diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceLabel.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceLabel.kt index b8f1f4749cc5..10c65aeaaf3a 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceLabel.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceLabel.kt @@ -7,4 +7,6 @@ package com.nextcloud.ui.fileInfo.model -data class GovernanceLabel(val text: String, val color: String) +data class GovernanceLabel(val id: String, val text: String, val color: String) { + override fun toString(): String = text +} From 4b65bb695a7ad4e927c33fe30fd7e00f3527d493 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Mon, 13 Jul 2026 14:52:22 +0200 Subject: [PATCH 16/35] wip Signed-off-by: tobiasKaminsky --- .idea/codeStyles/Project.xml | 25 ----- .../ui/fileInfo/FileInfoViewModel.kt | 70 ++++++++++++-- .../ui/fileInfo/GovernanceDetailInfo.kt | 94 ++++++++++++++++++- .../main/res/layout/file_info_fragment.xml | 34 ++++++- app/src/main/res/values/strings.xml | 6 ++ 5 files changed, 189 insertions(+), 40 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 23a6687d1074..4dc0c1db2c87 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -2,31 +2,6 @@ - From 928f837b733892d9d770366c9a99b0f7827c17f2 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Wed, 15 Jul 2026 08:58:50 +0200 Subject: [PATCH 17/35] wip Signed-off-by: tobiasKaminsky --- .../com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt | 10 ++-------- .../android/ui/fragment/FileDetailFragment.java | 2 +- app/src/main/res/drawable/selector_file_info.xml | 11 ----------- app/src/main/res/values/strings.xml | 1 - 4 files changed, 3 insertions(+), 21 deletions(-) delete mode 100644 app/src/main/res/drawable/selector_file_info.xml diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt index fe45a27b3bea..00cba4ed3c24 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt @@ -158,13 +158,7 @@ class GovernanceDetailInfo( selectedIds: Set ) { val selectedNames = items.filter { it.id in selectedIds }.map { it.text } - labelText.setText( - when { - selectedNames.isEmpty() -> "" - selectedNames.size == 1 -> selectedNames.first() - else -> context.getString(R.string.governance_n_selected, selectedNames.size) - } - ) + labelText.setText(selectedNames.joinToString(", ")) } private fun initDropdown( @@ -220,7 +214,7 @@ class GovernanceDetailInfo( } } - private fun parseColor(color: String): Int = runCatching { color.toColorInt() } + private fun parseColor(color: String): Int = runCatching { "#$color".toColorInt() } .getOrElse { Log_OC.w(TAG, "Could not parse label color: $color") ContextCompat.getColor(context, R.color.grey_600) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 3a7e4dd66566..4774dfc5f996 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -317,7 +317,7 @@ private void setupViewPager() { } if (showDetailsTab()) { - binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.filedetails_details).setIcon(R.drawable.selector_file_info)); + binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.filedetails_details).setIcon(R.drawable.info_24)); } viewThemeUtils.material.themeTabLayout(binding.tabLayout); diff --git a/app/src/main/res/drawable/selector_file_info.xml b/app/src/main/res/drawable/selector_file_info.xml deleted file mode 100644 index 71684759b904..000000000000 --- a/app/src/main/res/drawable/selector_file_info.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e6c88f427239..669e3ffdbc72 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1533,7 +1533,6 @@ Sensitivity label File retention Legal hold - %d selected Governance Image details From 12f38b3ab545bee66cf219999b469092ef5d150e Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Wed, 15 Jul 2026 08:59:33 +0200 Subject: [PATCH 18/35] wip Signed-off-by: tobiasKaminsky --- app/src/main/res/drawable/info_24.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 app/src/main/res/drawable/info_24.xml diff --git a/app/src/main/res/drawable/info_24.xml b/app/src/main/res/drawable/info_24.xml new file mode 100644 index 000000000000..d98f00a4f62a --- /dev/null +++ b/app/src/main/res/drawable/info_24.xml @@ -0,0 +1,15 @@ + + + + From 9c1a7de3cd39b8d2432db884997ca1cea235a141 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Wed, 15 Jul 2026 09:13:55 +0200 Subject: [PATCH 19/35] wip Signed-off-by: tobiasKaminsky --- gradle/libs.versions.toml | 2 +- settings.gradle.kts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 593ef3bc214b..0069d9f6089e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidCommonLibraryVersion = "0.33.2" androidGifDrawableVersion = "1.2.32" androidImageCropperVersion = "4.7.0" -androidLibraryVersion ="6249f83917b2c3cc0c72082b3f34765a239f047b" +androidLibraryVersion ="baa16c263d0277f406048eef043bef04590caa38" androidOpensslVersion = "3.5.6" androidPluginVersion = "9.2.1" androidsvgVersion = "1.4" diff --git a/settings.gradle.kts b/settings.gradle.kts index 7a4fca194396..fce8b85c32d1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,14 +43,14 @@ dependencyResolutionManagement { /* Needed for local android library -*/ + includeBuild("../android-library") { dependencySubstitution { substitute(module("com.github.nextcloud:android-library")) .using(project(":library")) } } - +*/ /* Needed for local android common library From c4a1b243fdc54b9377226c406194a6c4b32ad752 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Wed, 15 Jul 2026 10:47:43 +0200 Subject: [PATCH 20/35] wip Signed-off-by: tobiasKaminsky --- .../102.json | 1318 +++++++++++++++++ .../com/owncloud/android/db/ProviderMeta.java | 2 +- app/src/main/res/values/strings.xml | 4 - gradle/libs.versions.toml | 2 +- 4 files changed, 1320 insertions(+), 6 deletions(-) create mode 100644 app/schemas/com.nextcloud.client.database.NextcloudDatabase/102.json diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/102.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/102.json new file mode 100644 index 000000000000..9711a0f2b3e6 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/102.json @@ -0,0 +1,1318 @@ +{ + "formatVersion": 1, + "database": { + "version": 102, + "identityHash": "bacfc0ea6c4e6760883d46311050bdfd", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `governance` INTEGER, `forbidden_filename_characters` TEXT, `forbidden_filenames` TEXT, `forbidden_filename_extensions` TEXT, `forbidden_filename_basenames` TEXT, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER, `user_status_supports_busy` INTEGER, `windows_compatible_filenames` INTEGER, `has_valid_subscription` INTEGER, `client_integration_json` TEXT, `mod_rewrite_working` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT" + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT" + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER" + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER" + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT" + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT" + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT" + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT" + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT" + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT" + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT" + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER" + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER" + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER" + }, + { + "fieldPath": "governance", + "columnName": "governance", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "TEXT" + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER" + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT" + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsBusy", + "columnName": "user_status_supports_busy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWCFEnabled", + "columnName": "windows_compatible_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasValidSubscription", + "columnName": "has_valid_subscription", + "affinity": "INTEGER" + }, + { + "fieldPath": "clientIntegrationJson", + "columnName": "client_integration_json", + "affinity": "TEXT" + }, + { + "fieldPath": "modRewriteWorking", + "columnName": "mod_rewrite_working", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT" + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT" + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER" + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT" + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER" + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER" + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER" + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER" + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER" + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER" + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT" + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT" + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT" + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT" + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT" + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER" + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT" + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT" + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER, `attributes` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER" + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT" + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT" + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT" + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT" + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER" + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER" + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `upload_end_timestamp_long` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER" + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestampLong", + "columnName": "upload_end_timestamp_long", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT" + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "recommended_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `directory` TEXT NOT NULL, `extension` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `has_preview` INTEGER NOT NULL, `reason` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `account_name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directory", + "columnName": "directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extension", + "columnName": "extension", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "assistant", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT, `type` TEXT, `status` TEXT, `userId` TEXT, `appId` TEXT, `input` TEXT, `output` TEXT, `completionExpectedAt` INTEGER, `progress` INTEGER, `lastUpdated` INTEGER, `scheduledAt` INTEGER, `endedAt` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "appId", + "columnName": "appId", + "affinity": "TEXT" + }, + { + "fieldPath": "input", + "columnName": "input", + "affinity": "TEXT" + }, + { + "fieldPath": "output", + "columnName": "output", + "affinity": "TEXT" + }, + { + "fieldPath": "completionExpectedAt", + "columnName": "completionExpectedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER" + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bacfc0ea6c4e6760883d46311050bdfd')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java index 830d7c876fb7..3471d02d03a9 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -23,7 +23,7 @@ */ public class ProviderMeta { public static final String DB_NAME = "filelist"; - public static final int DB_VERSION = 101; + public static final int DB_VERSION = 102; private ProviderMeta() { // No instance diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 669e3ffdbc72..accea8cbcee2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,10 +7,6 @@ --> Local contacts - Sensitivity label - File retention - Governance - Image details Failed to open text editor All files diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0069d9f6089e..0d7512ca0a43 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidCommonLibraryVersion = "0.33.2" androidGifDrawableVersion = "1.2.32" androidImageCropperVersion = "4.7.0" -androidLibraryVersion ="baa16c263d0277f406048eef043bef04590caa38" +androidLibraryVersion ="2d47a5a57cef54b581fa5e2cfb10466a0bfd3c55" androidOpensslVersion = "3.5.6" androidPluginVersion = "9.2.1" androidsvgVersion = "1.4" From 2df9158b539ed0bf8efa37b07a8106117f052534 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Wed, 15 Jul 2026 10:49:28 +0200 Subject: [PATCH 21/35] wip Signed-off-by: tobiasKaminsky --- gradle/verification-metadata.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 4057292e7e05..cc61c0a0d3d1 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -20712,6 +20712,14 @@ + + + + + + + + @@ -21136,6 +21144,14 @@ + + + + + + + + From 37db1b801b8dd4b8507445c80a23663d9dcf132c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 15 Jul 2026 11:22:29 +0200 Subject: [PATCH 22/35] use repository pattern Signed-off-by: alperozturk96 --- .../nextcloud/ui/fileInfo/FileInfoFragment.kt | 2 +- .../ui/fileInfo/FileInfoRepository.kt | 103 +++++++++++++ .../ui/fileInfo/FileInfoViewModel.kt | 141 ++++-------------- .../ui/fileInfo/model/CurrentEntityLabels.kt | 14 ++ 4 files changed, 146 insertions(+), 114 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt create mode 100644 app/src/main/java/com/nextcloud/ui/fileInfo/model/CurrentEntityLabels.kt diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt index d3c792745275..35f638d5779c 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt @@ -51,7 +51,7 @@ class FileInfoFragment : viewModel = ViewModelProvider(this, vmFactory)[FileInfoViewModel::class.java] val file = file ?: return val user = user ?: return - viewModel.load(file, user) + viewModel.init(file, user) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt new file mode 100644 index 000000000000..ecdf5523d03c --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt @@ -0,0 +1,103 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.fileInfo + +import com.nextcloud.android.lib.resources.governance.GetAvailableHoldLabelsRemoteOperation +import com.nextcloud.android.lib.resources.governance.GetAvailableRetentionLabelsRemoteOperation +import com.nextcloud.android.lib.resources.governance.GetAvailableSensitivityLabelsRemoteOperation +import com.nextcloud.android.lib.resources.governance.GetEntityLabelsRemoteOperation +import com.nextcloud.android.lib.resources.governance.HoldLabelInfo +import com.nextcloud.android.lib.resources.governance.LabelType +import com.nextcloud.android.lib.resources.governance.RemoveLabelRemoteOperation +import com.nextcloud.android.lib.resources.governance.RetentionLabelInfo +import com.nextcloud.android.lib.resources.governance.SensitivityLabelInfo +import com.nextcloud.android.lib.resources.governance.SetLabelRemoteOperation +import com.nextcloud.client.account.User +import com.nextcloud.client.network.ClientFactory +import com.nextcloud.common.NextcloudClient +import com.nextcloud.ui.fileInfo.model.CurrentEntityLabels +import com.nextcloud.ui.fileInfo.model.GovernanceLabel +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class FileInfoRepository @Inject constructor(private val clientFactory: ClientFactory) { + + private val clientMutex = Mutex() + private var client: NextcloudClient? = null + + suspend fun fetchSensitivityLabels(file: OCFile, user: User): List = + execute(user, emptyList(), "Could not fetch available sensitivity labels") { client -> + val result = GetAvailableSensitivityLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) + if (result.isSuccess) result.resultData.orEmpty().map { it.toGovernanceLabel() } else emptyList() + } + + suspend fun fetchRetentionLabels(file: OCFile, user: User): List = + execute(user, emptyList(), "Could not fetch available retention labels") { client -> + val result = GetAvailableRetentionLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) + if (result.isSuccess) result.resultData.orEmpty().map { it.toGovernanceLabel() } else emptyList() + } + + suspend fun fetchHoldLabels(file: OCFile, user: User): List = + execute(user, emptyList(), "Could not fetch available hold labels") { client -> + val result = GetAvailableHoldLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) + if (result.isSuccess) result.resultData.orEmpty().map { it.toGovernanceLabel() } else emptyList() + } + + suspend fun fetchEntityLabels(file: OCFile, user: User): CurrentEntityLabels = + execute(user, CurrentEntityLabels(), "Could not fetch entity labels") { client -> + val result = GetEntityLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId.toString()).execute(client) + val labels = if (result.isSuccess) result.resultData else null + CurrentEntityLabels( + sensitivityId = labels?.sensitivity?.id ?: "", + retentionIds = labels?.retention?.map { it.id }?.toSet() ?: emptySet(), + holdIds = labels?.hold?.map { it.id }?.toSet() ?: emptySet() + ) + } + + suspend fun setLabel(file: OCFile, user: User, labelType: LabelType, labelId: String): Boolean = + execute(user, false, "Could not set label") { client -> + SetLabelRemoteOperation(ENTITY_TYPE_FILES, file.localId, labelType, labelId).execute(client).isSuccess + } + + suspend fun removeLabel(file: OCFile, user: User, labelType: LabelType, labelId: String): Boolean = + execute(user, false, "Could not remove label") { client -> + RemoveLabelRemoteOperation(ENTITY_TYPE_FILES, file.localId, labelType, labelId).execute(client).isSuccess + } + + @Suppress("TooGenericExceptionCaught") + private suspend fun execute(user: User, default: T, errorMessage: String, block: (NextcloudClient) -> T): T = + withContext(Dispatchers.IO) { + val client = client(user) ?: return@withContext default + try { + block(client) + } catch (e: Exception) { + Log_OC.e(TAG, errorMessage, e) + default + } + } + + private suspend fun client(user: User): NextcloudClient? = clientMutex.withLock { + client ?: runCatching { clientFactory.createNextcloudClient(user) }.getOrNull()?.also { client = it } + } + + private fun SensitivityLabelInfo.toGovernanceLabel() = GovernanceLabel(id, name, color) + + private fun RetentionLabelInfo.toGovernanceLabel() = GovernanceLabel(id, name, color) + + private fun HoldLabelInfo.toGovernanceLabel() = GovernanceLabel(id, name, color) + + companion object { + private val TAG = FileInfoRepository::class.java.simpleName + private const val ENTITY_TYPE_FILES = "FILES" + } +} diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt index dfbf476c3b16..c91965f7debd 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ @@ -9,29 +9,16 @@ package com.nextcloud.ui.fileInfo import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.nextcloud.android.lib.resources.governance.EntityLabels -import com.nextcloud.android.lib.resources.governance.GetAvailableHoldLabelsRemoteOperation -import com.nextcloud.android.lib.resources.governance.GetAvailableRetentionLabelsRemoteOperation -import com.nextcloud.android.lib.resources.governance.GetAvailableSensitivityLabelsRemoteOperation -import com.nextcloud.android.lib.resources.governance.GetEntityLabelsRemoteOperation -import com.nextcloud.android.lib.resources.governance.HoldLabelInfo import com.nextcloud.android.lib.resources.governance.LabelType -import com.nextcloud.android.lib.resources.governance.RemoveLabelRemoteOperation -import com.nextcloud.android.lib.resources.governance.RetentionLabelInfo -import com.nextcloud.android.lib.resources.governance.SensitivityLabelInfo -import com.nextcloud.android.lib.resources.governance.SetLabelRemoteOperation import com.nextcloud.client.account.User -import com.nextcloud.client.network.ClientFactory import com.nextcloud.ui.fileInfo.model.GovernanceLabel import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.lib.common.utils.Log_OC -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject -class FileInfoViewModel @Inject constructor(private val clientFactory: ClientFactory) : ViewModel() { +class FileInfoViewModel @Inject constructor(private val repository: FileInfoRepository) : ViewModel() { private val _sensitivityLabels = MutableStateFlow?>(null) val sensitivityLabels: StateFlow?> = _sensitivityLabels @@ -54,46 +41,43 @@ class FileInfoViewModel @Inject constructor(private val clientFactory: ClientFac private var cachedFile: OCFile? = null private var cachedUser: User? = null - fun load(file: OCFile, user: User) { + fun init(file: OCFile, user: User) { cachedFile = file cachedUser = user - viewModelScope.launch(Dispatchers.IO) { - _sensitivityLabels.value = fetchSensitivityLabels(file, user) + viewModelScope.launch { + _sensitivityLabels.value = repository.fetchSensitivityLabels(file, user) } - viewModelScope.launch(Dispatchers.IO) { - _retentionLabels.value = fetchRetentionLabels(file, user) + viewModelScope.launch { + _retentionLabels.value = repository.fetchRetentionLabels(file, user) } - viewModelScope.launch(Dispatchers.IO) { - _holdLabels.value = fetchHoldLabels(file, user) + viewModelScope.launch { + _holdLabels.value = repository.fetchHoldLabels(file, user) } - viewModelScope.launch(Dispatchers.IO) { - val entityLabels = fetchEntityLabels(file, user) - _currentSensitivityLabelId.value = entityLabels?.sensitivity?.id ?: "" - _currentRetentionLabelIds.value = entityLabels?.retention?.map { it.id }?.toSet() ?: emptySet() - _currentHoldLabelIds.value = entityLabels?.hold?.map { it.id }?.toSet() ?: emptySet() + viewModelScope.launch { + val entityLabels = repository.fetchEntityLabels(file, user) + _currentSensitivityLabelId.value = entityLabels.sensitivityId + _currentRetentionLabelIds.value = entityLabels.retentionIds + _currentHoldLabelIds.value = entityLabels.holdIds } } fun setSensitivityLabel(labelId: String) { val file = cachedFile ?: return val user = cachedUser ?: return - viewModelScope.launch(Dispatchers.IO) { - val result = applyLabel(file, user, LabelType.SENSITIVITY, labelId) - if (result) { + viewModelScope.launch { + if (repository.setLabel(file, user, LabelType.SENSITIVITY, labelId)) { _currentSensitivityLabelId.value = labelId } } } fun removeSensitivityLabel() { - val labelId = _currentSensitivityLabelId.value?.takeIf { it.isNotEmpty() } ?: return - cachedFile?.let { file -> - cachedUser?.let { user -> - viewModelScope.launch(Dispatchers.IO) { - if (removeLabel(file, user, LabelType.SENSITIVITY, labelId)) { - _currentSensitivityLabelId.value = "" - } - } + val file = cachedFile ?: return + val user = cachedUser ?: return + viewModelScope.launch { + val labelId = _currentSensitivityLabelId.value?.takeIf { it.isNotEmpty() } ?: return@launch + if (repository.removeLabel(file, user, LabelType.SENSITIVITY, labelId)) { + _currentSensitivityLabelId.value = "" } } } @@ -104,9 +88,9 @@ class FileInfoViewModel @Inject constructor(private val clientFactory: ClientFac val currentIds = _currentRetentionLabelIds.value ?: emptySet() val toAdd = newLabelIds - currentIds val toRemove = currentIds - newLabelIds - viewModelScope.launch(Dispatchers.IO) { - toAdd.forEach { applyLabel(file, user, LabelType.RETENTION, it) } - toRemove.forEach { removeLabel(file, user, LabelType.RETENTION, it) } + viewModelScope.launch { + toAdd.forEach { repository.setLabel(file, user, LabelType.RETENTION, it) } + toRemove.forEach { repository.removeLabel(file, user, LabelType.RETENTION, it) } _currentRetentionLabelIds.value = newLabelIds } } @@ -117,79 +101,10 @@ class FileInfoViewModel @Inject constructor(private val clientFactory: ClientFac val currentIds = _currentHoldLabelIds.value ?: emptySet() val toAdd = newLabelIds - currentIds val toRemove = currentIds - newLabelIds - viewModelScope.launch(Dispatchers.IO) { - toAdd.forEach { applyLabel(file, user, LabelType.HOLD, it) } - toRemove.forEach { removeLabel(file, user, LabelType.HOLD, it) } + viewModelScope.launch { + toAdd.forEach { repository.setLabel(file, user, LabelType.HOLD, it) } + toRemove.forEach { repository.removeLabel(file, user, LabelType.HOLD, it) } _currentHoldLabelIds.value = newLabelIds } } - - @Suppress("TooGenericExceptionCaught") - private fun fetchSensitivityLabels(file: OCFile, user: User): List = try { - val client = clientFactory.createNextcloudClient(user) - val result = GetAvailableSensitivityLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) - if (result.isSuccess) result.resultData.orEmpty().map { it.toGovernanceLabel() } else emptyList() - } catch (e: Exception) { - Log_OC.e(TAG, "Could not fetch available sensitivity labels", e) - emptyList() - } - - @Suppress("TooGenericExceptionCaught") - private fun fetchRetentionLabels(file: OCFile, user: User): List = try { - val client = clientFactory.createNextcloudClient(user) - val result = GetAvailableRetentionLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) - if (result.isSuccess) result.resultData.orEmpty().map { it.toGovernanceLabel() } else emptyList() - } catch (e: Exception) { - Log_OC.e(TAG, "Could not fetch available retention labels", e) - emptyList() - } - - @Suppress("TooGenericExceptionCaught") - private fun fetchHoldLabels(file: OCFile, user: User): List = try { - val client = clientFactory.createNextcloudClient(user) - val result = GetAvailableHoldLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) - if (result.isSuccess) result.resultData.orEmpty().map { it.toGovernanceLabel() } else emptyList() - } catch (e: Exception) { - Log_OC.e(TAG, "Could not fetch available hold labels", e) - emptyList() - } - - @Suppress("TooGenericExceptionCaught") - private fun fetchEntityLabels(file: OCFile, user: User): EntityLabels? = try { - val client = clientFactory.createNextcloudClient(user) - val result = GetEntityLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId.toString()).execute(client) - if (result.isSuccess) result.resultData else null - } catch (e: Exception) { - Log_OC.e(TAG, "Could not fetch entity labels", e) - null - } - - @Suppress("TooGenericExceptionCaught") - private fun applyLabel(file: OCFile, user: User, labelType: LabelType, labelId: String): Boolean = try { - val client = clientFactory.createNextcloudClient(user) - SetLabelRemoteOperation(ENTITY_TYPE_FILES, file.localId, labelType, labelId).execute(client).isSuccess - } catch (e: Exception) { - Log_OC.e(TAG, "Could not set label", e) - false - } - - @Suppress("TooGenericExceptionCaught") - private fun removeLabel(file: OCFile, user: User, labelType: LabelType, labelId: String): Boolean = try { - val client = clientFactory.createNextcloudClient(user) - RemoveLabelRemoteOperation(ENTITY_TYPE_FILES, file.localId, labelType, labelId).execute(client).isSuccess - } catch (e: Exception) { - Log_OC.e(TAG, "Could not remove label", e) - false - } - - private fun SensitivityLabelInfo.toGovernanceLabel() = GovernanceLabel(id, name, color) - - private fun RetentionLabelInfo.toGovernanceLabel() = GovernanceLabel(id, name, color) - - private fun HoldLabelInfo.toGovernanceLabel() = GovernanceLabel(id, name, color) - - companion object { - private val TAG = FileInfoViewModel::class.java.simpleName - private const val ENTITY_TYPE_FILES = "FILES" - } } diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/model/CurrentEntityLabels.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/model/CurrentEntityLabels.kt new file mode 100644 index 000000000000..4c0fb3216d3c --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/model/CurrentEntityLabels.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.fileInfo.model + +data class CurrentEntityLabels( + val sensitivityId: String = "", + val retentionIds: Set = emptySet(), + val holdIds: Set = emptySet() +) From b598d87317c38b26cb7e12b476a1702148fbf605 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 15 Jul 2026 11:31:04 +0200 Subject: [PATCH 23/35] remove nullable flow data use sealed interface Signed-off-by: alperozturk96 --- .../ui/fileInfo/FileInfoViewModel.kt | 99 +++++++-------- .../ui/fileInfo/GovernanceDetailInfo.kt | 113 +++++++++--------- .../ui/fileInfo/model/GovernanceUiState.kt | 21 ++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 118 insertions(+), 116 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceUiState.kt diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt index c91965f7debd..a1ad23245211 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt @@ -11,100 +11,85 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.android.lib.resources.governance.LabelType import com.nextcloud.client.account.User -import com.nextcloud.ui.fileInfo.model.GovernanceLabel +import com.nextcloud.ui.fileInfo.model.GovernanceUiState import com.owncloud.android.datamodel.OCFile +import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject class FileInfoViewModel @Inject constructor(private val repository: FileInfoRepository) : ViewModel() { - private val _sensitivityLabels = MutableStateFlow?>(null) - val sensitivityLabels: StateFlow?> = _sensitivityLabels + private val _uiState = MutableStateFlow(GovernanceUiState.Loading) + val uiState: StateFlow = _uiState - private val _retentionLabels = MutableStateFlow?>(null) - val retentionLabels: StateFlow?> = _retentionLabels - - private val _holdLabels = MutableStateFlow?>(null) - val holdLabels: StateFlow?> = _holdLabels - - private val _currentSensitivityLabelId = MutableStateFlow(null) - val currentSensitivityLabelId: StateFlow = _currentSensitivityLabelId - - private val _currentRetentionLabelIds = MutableStateFlow?>(null) - val currentRetentionLabelIds: StateFlow?> = _currentRetentionLabelIds - - private val _currentHoldLabelIds = MutableStateFlow?>(null) - val currentHoldLabelIds: StateFlow?> = _currentHoldLabelIds - - private var cachedFile: OCFile? = null - private var cachedUser: User? = null + private lateinit var file: OCFile + private lateinit var user: User fun init(file: OCFile, user: User) { - cachedFile = file - cachedUser = user - viewModelScope.launch { - _sensitivityLabels.value = repository.fetchSensitivityLabels(file, user) - } - viewModelScope.launch { - _retentionLabels.value = repository.fetchRetentionLabels(file, user) - } - viewModelScope.launch { - _holdLabels.value = repository.fetchHoldLabels(file, user) - } + this.file = file + this.user = user viewModelScope.launch { + val sensitivityLabels = async { repository.fetchSensitivityLabels(file, user) } + val retentionLabels = async { repository.fetchRetentionLabels(file, user) } + val holdLabels = async { repository.fetchHoldLabels(file, user) } val entityLabels = repository.fetchEntityLabels(file, user) - _currentSensitivityLabelId.value = entityLabels.sensitivityId - _currentRetentionLabelIds.value = entityLabels.retentionIds - _currentHoldLabelIds.value = entityLabels.holdIds + _uiState.value = GovernanceUiState.Loaded( + sensitivityLabels = sensitivityLabels.await(), + retentionLabels = retentionLabels.await(), + holdLabels = holdLabels.await(), + currentSensitivityLabelId = entityLabels.sensitivityId, + currentRetentionLabelIds = entityLabels.retentionIds, + currentHoldLabelIds = entityLabels.holdIds + ) } } fun setSensitivityLabel(labelId: String) { - val file = cachedFile ?: return - val user = cachedUser ?: return viewModelScope.launch { if (repository.setLabel(file, user, LabelType.SENSITIVITY, labelId)) { - _currentSensitivityLabelId.value = labelId + updateLoaded { it.copy(currentSensitivityLabelId = labelId) } } } } fun removeSensitivityLabel() { - val file = cachedFile ?: return - val user = cachedUser ?: return viewModelScope.launch { - val labelId = _currentSensitivityLabelId.value?.takeIf { it.isNotEmpty() } ?: return@launch + val labelId = loaded()?.currentSensitivityLabelId?.takeIf { it.isNotEmpty() } ?: return@launch if (repository.removeLabel(file, user, LabelType.SENSITIVITY, labelId)) { - _currentSensitivityLabelId.value = "" + updateLoaded { it.copy(currentSensitivityLabelId = "") } } } } fun updateRetentionLabels(newLabelIds: Set) { - val file = cachedFile ?: return - val user = cachedUser ?: return - val currentIds = _currentRetentionLabelIds.value ?: emptySet() - val toAdd = newLabelIds - currentIds - val toRemove = currentIds - newLabelIds + val currentIds = loaded()?.currentRetentionLabelIds ?: return viewModelScope.launch { - toAdd.forEach { repository.setLabel(file, user, LabelType.RETENTION, it) } - toRemove.forEach { repository.removeLabel(file, user, LabelType.RETENTION, it) } - _currentRetentionLabelIds.value = newLabelIds + applyDiff(LabelType.RETENTION, currentIds, newLabelIds) + updateLoaded { it.copy(currentRetentionLabelIds = newLabelIds) } } } fun updateHoldLabels(newLabelIds: Set) { - val file = cachedFile ?: return - val user = cachedUser ?: return - val currentIds = _currentHoldLabelIds.value ?: emptySet() - val toAdd = newLabelIds - currentIds - val toRemove = currentIds - newLabelIds + val currentIds = loaded()?.currentHoldLabelIds ?: return viewModelScope.launch { - toAdd.forEach { repository.setLabel(file, user, LabelType.HOLD, it) } - toRemove.forEach { repository.removeLabel(file, user, LabelType.HOLD, it) } - _currentHoldLabelIds.value = newLabelIds + applyDiff(LabelType.HOLD, currentIds, newLabelIds) + updateLoaded { it.copy(currentHoldLabelIds = newLabelIds) } + } + } + + private suspend fun applyDiff(labelType: LabelType, currentIds: Set, newLabelIds: Set) { + (newLabelIds - currentIds).forEach { repository.setLabel(file, user, labelType, it) } + (currentIds - newLabelIds).forEach { repository.removeLabel(file, user, labelType, it) } + } + + private fun loaded(): GovernanceUiState.Loaded? = _uiState.value as? GovernanceUiState.Loaded + + private fun updateLoaded(transform: (GovernanceUiState.Loaded) -> GovernanceUiState.Loaded) { + _uiState.update { state -> + if (state is GovernanceUiState.Loaded) transform(state) else state } } } diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt index 00cba4ed3c24..9f0c98ad39fb 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt @@ -21,11 +21,12 @@ import com.google.android.material.textfield.MaterialAutoCompleteTextView import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import com.nextcloud.ui.fileInfo.model.GovernanceLabel +import com.nextcloud.ui.fileInfo.model.GovernanceUiState import com.owncloud.android.R import com.owncloud.android.databinding.FileInfoFragmentBinding import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.theme.ViewThemeUtils -import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -39,76 +40,70 @@ class GovernanceDetailInfo( fun init() { viewThemeUtils.material.themeCardView(binding.governanceLayout) - collectSensitivityLabels() - collectRetentionLabels() - collectHoldLabels() + fragment.lifecycleScope.launch { + val state = viewModel.uiState.filterIsInstance().first() + setupSensitivityLabels(state.sensitivityLabels, state.currentSensitivityLabelId) + setupRetentionLabels(state.retentionLabels) + setupHoldLabels(state.holdLabels) + } } - private fun collectSensitivityLabels() { - fragment.lifecycleScope.launch { - val labels = viewModel.sensitivityLabels.filterNotNull().first() - if (labels.isEmpty()) { - binding.sensitivityLabel.visibility = View.GONE - return@launch - } - val currentId = viewModel.currentSensitivityLabelId.filterNotNull().first() - val items = listOf(GovernanceLabel("", "", "")) + labels - val initialItem = items.find { it.id == currentId } ?: items.first() - initDropdown( - textInputLayout = binding.sensitivityLabel, - autoComplete = binding.sensitivityLabelAutoComplete, - items = items, - initialItem = initialItem - ) { label -> - if (label.id.isEmpty()) { - viewModel.removeSensitivityLabel() - } else { - viewModel.setSensitivityLabel(label.id) - } + private fun setupSensitivityLabels(labels: List, currentId: String) { + if (labels.isEmpty()) { + binding.sensitivityLabel.visibility = View.GONE + return + } + val noneLabel = GovernanceLabel("", context.getString(R.string.governance_no_label), "") + val items = listOf(noneLabel) + labels + val initialItem = items.find { it.id == currentId } ?: items.first() + initDropdown( + textInputLayout = binding.sensitivityLabel, + autoComplete = binding.sensitivityLabelAutoComplete, + items = items, + initialItem = initialItem + ) { label -> + if (label.id.isEmpty()) { + viewModel.removeSensitivityLabel() + } else { + viewModel.setSensitivityLabel(label.id) } } } - private fun collectRetentionLabels() { - fragment.lifecycleScope.launch { - val labels = viewModel.retentionLabels.filterNotNull().first() - if (labels.isEmpty()) { - binding.fileRetentionLabel.visibility = View.GONE - return@launch - } - val currentIds = viewModel.currentRetentionLabelIds.filterNotNull().first() - setupMultiSelectField( - textInputLayout = binding.fileRetentionLabel, - labelText = binding.fileRetentionLabelText, - items = labels, - title = context.getString(R.string.governance_file_retention_label), - getCurrentIds = { viewModel.currentRetentionLabelIds.value ?: currentIds } - ) { newIds -> - viewModel.updateRetentionLabels(newIds) - } + private fun setupRetentionLabels(labels: List) { + if (labels.isEmpty()) { + binding.fileRetentionLabel.visibility = View.GONE + return + } + setupMultiSelectField( + textInputLayout = binding.fileRetentionLabel, + labelText = binding.fileRetentionLabelText, + items = labels, + title = context.getString(R.string.governance_file_retention_label), + getCurrentIds = { currentLoadedState()?.currentRetentionLabelIds.orEmpty() } + ) { newIds -> + viewModel.updateRetentionLabels(newIds) } } - private fun collectHoldLabels() { - fragment.lifecycleScope.launch { - val labels = viewModel.holdLabels.filterNotNull().first() - if (labels.isEmpty()) { - binding.fileHoldLabel.visibility = View.GONE - return@launch - } - val currentIds = viewModel.currentHoldLabelIds.filterNotNull().first() - setupMultiSelectField( - textInputLayout = binding.fileHoldLabel, - labelText = binding.fileHoldLabelText, - items = labels, - title = context.getString(R.string.governance_legal_hold_label), - getCurrentIds = { viewModel.currentHoldLabelIds.value ?: currentIds } - ) { newIds -> - viewModel.updateHoldLabels(newIds) - } + private fun setupHoldLabels(labels: List) { + if (labels.isEmpty()) { + binding.fileHoldLabel.visibility = View.GONE + return + } + setupMultiSelectField( + textInputLayout = binding.fileHoldLabel, + labelText = binding.fileHoldLabelText, + items = labels, + title = context.getString(R.string.governance_legal_hold_label), + getCurrentIds = { currentLoadedState()?.currentHoldLabelIds.orEmpty() } + ) { newIds -> + viewModel.updateHoldLabels(newIds) } } + private fun currentLoadedState(): GovernanceUiState.Loaded? = viewModel.uiState.value as? GovernanceUiState.Loaded + private fun setupMultiSelectField( textInputLayout: TextInputLayout, labelText: TextInputEditText, diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceUiState.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceUiState.kt new file mode 100644 index 000000000000..0895ef4b0edc --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceUiState.kt @@ -0,0 +1,21 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.fileInfo.model + +sealed interface GovernanceUiState { + data object Loading : GovernanceUiState + + data class Loaded( + val sensitivityLabels: List, + val retentionLabels: List, + val holdLabels: List, + val currentSensitivityLabelId: String, + val currentRetentionLabelIds: Set, + val currentHoldLabelIds: Set + ) : GovernanceUiState +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index accea8cbcee2..007922587212 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1529,6 +1529,7 @@ Sensitivity label File retention Legal hold + None Governance Image details From 518da6f7e558a2cc8b6f13af6daa9302fff2810c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 15 Jul 2026 11:33:17 +0200 Subject: [PATCH 24/35] revert setting.gradle.kts Signed-off-by: alperozturk96 --- settings.gradle.kts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index fce8b85c32d1..651d7a880838 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -44,7 +44,7 @@ dependencyResolutionManagement { /* Needed for local android library -includeBuild("../android-library") { +includeBuild("../android_library") { dependencySubstitution { substitute(module("com.github.nextcloud:android-library")) .using(project(":library")) @@ -52,10 +52,11 @@ includeBuild("../android-library") { } */ + /* Needed for local android common library -includeBuild("../android-common") { +includeBuild("../android_common") { dependencySubstitution { substitute(module("com.github.nextcloud.android-common:core")) .using(project(":core")) From a2dc9cd6da8a3715902656868e3a5426c5bd3e3a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 15 Jul 2026 11:35:23 +0200 Subject: [PATCH 25/35] make file retention positive button filled tonal button Signed-off-by: alperozturk96 --- .../com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt index 9f0c98ad39fb..86300518b3a2 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt @@ -13,9 +13,11 @@ import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.TextView +import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.graphics.toColorInt import androidx.lifecycle.lifecycleScope +import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.MaterialAutoCompleteTextView import com.google.android.material.textfield.TextInputEditText @@ -136,7 +138,7 @@ class GovernanceDetailInfo( val checkedItems = items.map { it.id in currentIds }.toBooleanArray() val newSelection = currentIds.toMutableSet() - MaterialAlertDialogBuilder(context) + val dialog = MaterialAlertDialogBuilder(context) .setTitle(title) .setMultiChoiceItems(itemNames, checkedItems) { _, which, isChecked -> val labelId = items[which].id @@ -145,6 +147,10 @@ class GovernanceDetailInfo( .setPositiveButton(android.R.string.ok) { _, _ -> onConfirm(newSelection) } .setNegativeButton(android.R.string.cancel, null) .show() + + (dialog.getButton(AlertDialog.BUTTON_POSITIVE) as? MaterialButton)?.let { + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(it) + } } private fun updateMultiSelectText( From b604384d7dc7be956faa2b65125f7eb635769d2d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 15 Jul 2026 11:41:58 +0200 Subject: [PATCH 26/35] remove manual migration Signed-off-by: alperozturk96 --- .../client/database/NextcloudDatabase.kt | 4 +- .../database/migrations/Migration100to101.kt | 93 ------------------- 2 files changed, 1 insertion(+), 96 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/client/database/migrations/Migration100to101.kt diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index 788d09995dc7..5991d18eaa65 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -40,7 +40,6 @@ import com.nextcloud.client.database.entity.VirtualEntity import com.nextcloud.client.database.migrations.DatabaseMigrationUtil import com.nextcloud.client.database.migrations.MIGRATION_88_89 import com.nextcloud.client.database.migrations.MIGRATION_97_98 -import com.nextcloud.client.database.migrations.MIGRATION_100_101 import com.nextcloud.client.database.migrations.MIGRATION_99_100 import com.nextcloud.client.database.migrations.Migration67to68 import com.nextcloud.client.database.migrations.RoomMigration @@ -100,7 +99,7 @@ import com.owncloud.android.db.ProviderMeta // manual migration used for 97 to 98 AutoMigration(from = 98, to = 99), // manual migration used for 99 to 100 - // manual migration used for 100 to 101 + AutoMigration(from = 100, to = 101, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 101, to = 102, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class) ], exportSchema = true @@ -142,7 +141,6 @@ abstract class NextcloudDatabase : RoomDatabase() { .addMigrations(MIGRATION_88_89) .addMigrations(MIGRATION_97_98) .addMigrations(MIGRATION_99_100) - .addMigrations(MIGRATION_100_101) .build() } return instance!! diff --git a/app/src/main/java/com/nextcloud/client/database/migrations/Migration100to101.kt b/app/src/main/java/com/nextcloud/client/database/migrations/Migration100to101.kt deleted file mode 100644 index e4b5232859d1..000000000000 --- a/app/src/main/java/com/nextcloud/client/database/migrations/Migration100to101.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.client.database.migrations - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -@Suppress("MagicNumber", "LongMethod") -val MIGRATION_100_101 = object : Migration(100, 101) { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("DROP TABLE IF EXISTS capabilities") - - db.execSQL( - """ - CREATE TABLE IF NOT EXISTS capabilities ( - _id INTEGER PRIMARY KEY AUTOINCREMENT, - account TEXT, - version_mayor INTEGER, - version_minor INTEGER, - version_micro INTEGER, - version_string TEXT, - version_edition TEXT, - extended_support INTEGER, - core_pollinterval INTEGER, - sharing_api_enabled INTEGER, - sharing_public_enabled INTEGER, - sharing_public_password_enforced INTEGER, - sharing_public_ask_for_optional_password INTEGER, - sharing_public_expire_date_enabled INTEGER, - sharing_public_expire_date_days INTEGER, - sharing_public_expire_date_enforced INTEGER, - sharing_public_send_mail INTEGER, - sharing_public_upload INTEGER, - sharing_user_send_mail INTEGER, - sharing_resharing INTEGER, - sharing_federation_outgoing INTEGER, - sharing_federation_incoming INTEGER, - files_bigfilechunking INTEGER, - files_undelete INTEGER, - files_versioning INTEGER, - files_locking_version TEXT, - external_links INTEGER, - server_name TEXT, - server_color TEXT, - server_text_color TEXT, - server_element_color TEXT, - background_url TEXT, - server_slogan TEXT, - server_logo TEXT, - background_default INTEGER, - background_plain INTEGER, - end_to_end_encryption INTEGER, - end_to_end_encryption_keys_exist INTEGER, - end_to_end_encryption_api_version TEXT, - activity INTEGER, - richdocument INTEGER, - recommendation INTEGER, - richdocument_mimetype_list TEXT, - richdocument_optional_mimetype_list TEXT, - richdocument_direct_editing INTEGER, - richdocument_direct_templates INTEGER, - richdocument_product_name TEXT, - direct_editing_etag TEXT, - etag TEXT, - user_status INTEGER, - user_status_supports_emoji INTEGER, - user_status_supports_busy INTEGER, - assistant INTEGER, - groupfolders INTEGER, - drop_account INTEGER, - security_guard INTEGER, - governance INTEGER, - forbidden_filename_characters TEXT, - forbidden_filenames TEXT, - forbidden_filename_extensions TEXT, - forbidden_filename_basenames TEXT, - windows_compatible_filenames INTEGER, - files_download_limit INTEGER, - files_download_limit_default INTEGER, - notes_folder_path TEXT, - default_permissions INTEGER, - has_valid_subscription INTEGER, - client_integration_json TEXT - ) - """.trimIndent() - ) - } -} From c264e9564489a29799b2e4e87ffde57702366cd1 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 15 Jul 2026 11:43:51 +0200 Subject: [PATCH 27/35] remove manual migration Signed-off-by: alperozturk96 --- .../main/java/com/nextcloud/client/database/NextcloudDatabase.kt | 1 + app/src/main/java/com/owncloud/android/db/ProviderMeta.java | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index 5991d18eaa65..c3420e14ef91 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only diff --git a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java index 3471d02d03a9..e609b6f48c89 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2016-2021 Tobias Kaminsky * SPDX-FileCopyrightText: 2016 ownCloud Inc. From d887c43bf779e8e2223025b9ec25845f2ace7d90 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 15 Jul 2026 11:54:35 +0200 Subject: [PATCH 28/35] refactor(file-info): initialize FileInfoViewModel via AssistedInject Signed-off-by: alperozturk96 --- .../nextcloud/client/di/ViewModelModule.kt | 6 ----- .../nextcloud/ui/fileInfo/FileInfoFragment.kt | 16 +++++++++---- .../ui/fileInfo/FileInfoViewModel.kt | 24 +++++++++++++------ 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt b/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt index c0a3c1a85927..eb3e98a6c570 100644 --- a/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt @@ -12,7 +12,6 @@ import androidx.lifecycle.ViewModelProvider import com.nextcloud.client.documentscan.DocumentScanViewModel import com.nextcloud.client.etm.EtmViewModel import com.nextcloud.client.logger.ui.LogsViewModel -import com.nextcloud.ui.fileInfo.FileInfoViewModel import com.nextcloud.ui.fileactions.FileActionsViewModel import com.owncloud.android.ui.preview.pdf.PreviewPdfViewModel import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsViewModel @@ -58,11 +57,6 @@ abstract class ViewModelModule { @ViewModelKey(TrashbinFileActionsViewModel::class) abstract fun trashbinFileActionsViewModel(vm: TrashbinFileActionsViewModel): ViewModel - @Binds - @IntoMap - @ViewModelKey(FileInfoViewModel::class) - abstract fun fileInfoViewModel(vm: FileInfoViewModel): ViewModel - @Binds abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory } diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt index 35f638d5779c..458409d7e0ef 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoFragment.kt @@ -14,10 +14,10 @@ import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.nextcloud.client.account.User import com.nextcloud.client.di.Injectable -import com.nextcloud.client.di.ViewModelFactory import com.owncloud.android.databinding.FileInfoFragmentBinding import com.owncloud.android.datamodel.OCFile import com.owncloud.android.utils.MimeTypeUtil @@ -42,16 +42,20 @@ class FileInfoFragment : lateinit var viewThemeUtils: ViewThemeUtils @Inject - lateinit var vmFactory: ViewModelFactory + lateinit var viewModelFactory: FileInfoViewModel.Factory private lateinit var viewModel: FileInfoViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - viewModel = ViewModelProvider(this, vmFactory)[FileInfoViewModel::class.java] val file = file ?: return val user = user ?: return - viewModel.init(file, user) + viewModel = ViewModelProvider(this, provideViewModelFactory(file, user))[FileInfoViewModel::class.java] + } + + private fun provideViewModelFactory(file: OCFile, user: User) = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = viewModelFactory.create(file, user) as T } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @@ -66,6 +70,10 @@ class FileInfoFragment : } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + if (!::viewModel.isInitialized) { + binding.governanceLayout.visibility = View.GONE + return + } if (CapabilityUtils.getCapability(context).governance.isTrue) { val governanceDetailInfo = GovernanceDetailInfo(binding, viewThemeUtils, this, viewModel) governanceDetailInfo.init() diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt index a1ad23245211..f7260d70671b 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt @@ -13,24 +13,29 @@ import com.nextcloud.android.lib.resources.governance.LabelType import com.nextcloud.client.account.User import com.nextcloud.ui.fileInfo.model.GovernanceUiState import com.owncloud.android.datamodel.OCFile +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -class FileInfoViewModel @Inject constructor(private val repository: FileInfoRepository) : ViewModel() { +class FileInfoViewModel @AssistedInject constructor( + private val repository: FileInfoRepository, + @Assisted private val file: OCFile, + @Assisted private val user: User +) : ViewModel() { private val _uiState = MutableStateFlow(GovernanceUiState.Loading) val uiState: StateFlow = _uiState - private lateinit var file: OCFile - private lateinit var user: User + init { + loadGovernance() + } - fun init(file: OCFile, user: User) { - this.file = file - this.user = user + private fun loadGovernance() { viewModelScope.launch { val sensitivityLabels = async { repository.fetchSensitivityLabels(file, user) } val retentionLabels = async { repository.fetchRetentionLabels(file, user) } @@ -92,4 +97,9 @@ class FileInfoViewModel @Inject constructor(private val repository: FileInfoRepo if (state is GovernanceUiState.Loaded) transform(state) else state } } + + @AssistedFactory + interface Factory { + fun create(file: OCFile, user: User): FileInfoViewModel + } } From 20ff953cdf32029b24b37769d05592da755b0f8e Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Thu, 16 Jul 2026 07:29:29 +0200 Subject: [PATCH 29/35] Sensitivity labels are now an array, but in first iteration only one item is set, thus always use first entry Signed-off-by: tobiasKaminsky --- .../main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt index ecdf5523d03c..d1e0c8d0d970 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt @@ -58,7 +58,7 @@ class FileInfoRepository @Inject constructor(private val clientFactory: ClientFa val result = GetEntityLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId.toString()).execute(client) val labels = if (result.isSuccess) result.resultData else null CurrentEntityLabels( - sensitivityId = labels?.sensitivity?.id ?: "", + sensitivityId = labels?.sensitivity?.map { it.id }?.toSet()?.first() ?: "", retentionIds = labels?.retention?.map { it.id }?.toSet() ?: emptySet(), holdIds = labels?.hold?.map { it.id }?.toSet() ?: emptySet() ) From 47dd8a86db0dffbefbd4bfe00c6155d5e7e97ecb Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Thu, 16 Jul 2026 07:44:35 +0200 Subject: [PATCH 30/35] update lib Signed-off-by: tobiasKaminsky --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0d7512ca0a43..73af35445655 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidCommonLibraryVersion = "0.33.2" androidGifDrawableVersion = "1.2.32" androidImageCropperVersion = "4.7.0" -androidLibraryVersion ="2d47a5a57cef54b581fa5e2cfb10466a0bfd3c55" +androidLibraryVersion ="f8dd47d3aef22c68cad19a915598cda99c71b940" androidOpensslVersion = "3.5.6" androidPluginVersion = "9.2.1" androidsvgVersion = "1.4" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index cc61c0a0d3d1..a6bb7b41e336 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -22237,6 +22237,14 @@ + + + + + + + + From e92ca085ccad8cf8e18333c77820ebbfa91129fc Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Thu, 16 Jul 2026 09:09:32 +0200 Subject: [PATCH 31/35] remove unused files Signed-off-by: tobiasKaminsky --- app/src/main/res/drawable/ic_dashboard_filled.xml | 15 --------------- .../main/res/drawable/ic_dashboard_outlined.xml | 15 --------------- 2 files changed, 30 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_dashboard_filled.xml delete mode 100644 app/src/main/res/drawable/ic_dashboard_outlined.xml diff --git a/app/src/main/res/drawable/ic_dashboard_filled.xml b/app/src/main/res/drawable/ic_dashboard_filled.xml deleted file mode 100644 index 111b1e97f44a..000000000000 --- a/app/src/main/res/drawable/ic_dashboard_filled.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_dashboard_outlined.xml b/app/src/main/res/drawable/ic_dashboard_outlined.xml deleted file mode 100644 index 9d32ead03ef7..000000000000 --- a/app/src/main/res/drawable/ic_dashboard_outlined.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - From 3162c6f0e935ceacc09d3445f715a23cc9bf1638 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Thu, 16 Jul 2026 09:26:25 +0200 Subject: [PATCH 32/35] use one api call for fetching available labels Signed-off-by: tobiasKaminsky --- .../ui/fileInfo/FileInfoRepository.kt | 38 +++++++++---------- .../ui/fileInfo/FileInfoViewModel.kt | 11 +++--- .../ui/fileInfo/model/SelectableLabels.kt | 14 +++++++ gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 8 ++++ 5 files changed, 45 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/ui/fileInfo/model/SelectableLabels.kt diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt index d1e0c8d0d970..e4351c376da3 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt @@ -7,9 +7,7 @@ package com.nextcloud.ui.fileInfo -import com.nextcloud.android.lib.resources.governance.GetAvailableHoldLabelsRemoteOperation -import com.nextcloud.android.lib.resources.governance.GetAvailableRetentionLabelsRemoteOperation -import com.nextcloud.android.lib.resources.governance.GetAvailableSensitivityLabelsRemoteOperation +import com.nextcloud.android.lib.resources.governance.GetAllSelectableLabelsRemoteOperation import com.nextcloud.android.lib.resources.governance.GetEntityLabelsRemoteOperation import com.nextcloud.android.lib.resources.governance.HoldLabelInfo import com.nextcloud.android.lib.resources.governance.LabelType @@ -22,6 +20,7 @@ import com.nextcloud.client.network.ClientFactory import com.nextcloud.common.NextcloudClient import com.nextcloud.ui.fileInfo.model.CurrentEntityLabels import com.nextcloud.ui.fileInfo.model.GovernanceLabel +import com.nextcloud.ui.fileInfo.model.SelectableLabels import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.utils.Log_OC import kotlinx.coroutines.Dispatchers @@ -35,23 +34,22 @@ class FileInfoRepository @Inject constructor(private val clientFactory: ClientFa private val clientMutex = Mutex() private var client: NextcloudClient? = null - suspend fun fetchSensitivityLabels(file: OCFile, user: User): List = - execute(user, emptyList(), "Could not fetch available sensitivity labels") { client -> - val result = GetAvailableSensitivityLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) - if (result.isSuccess) result.resultData.orEmpty().map { it.toGovernanceLabel() } else emptyList() - } - - suspend fun fetchRetentionLabels(file: OCFile, user: User): List = - execute(user, emptyList(), "Could not fetch available retention labels") { client -> - val result = GetAvailableRetentionLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) - if (result.isSuccess) result.resultData.orEmpty().map { it.toGovernanceLabel() } else emptyList() - } - - suspend fun fetchHoldLabels(file: OCFile, user: User): List = - execute(user, emptyList(), "Could not fetch available hold labels") { client -> - val result = GetAvailableHoldLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) - if (result.isSuccess) result.resultData.orEmpty().map { it.toGovernanceLabel() } else emptyList() + suspend fun fetchAllSelectableLabels(file: OCFile, user: User): SelectableLabels { + val empty = SelectableLabels(emptyList(), emptyList(), emptyList()) + return execute(user, empty, "Could not fetch selectable labels") { client -> + val result = GetAllSelectableLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId).execute(client) + if (result.isSuccess) { + val data = result.resultData + SelectableLabels( + sensitivityLabels = data?.sensitivity?.map { it.toGovernanceLabel() } ?: emptyList(), + retentionLabels = data?.retention?.map { it.toGovernanceLabel() } ?: emptyList(), + holdLabels = data?.hold?.map { it.toGovernanceLabel() } ?: emptyList() + ) + } else { + empty + } } + } suspend fun fetchEntityLabels(file: OCFile, user: User): CurrentEntityLabels = execute(user, CurrentEntityLabels(), "Could not fetch entity labels") { client -> @@ -91,9 +89,7 @@ class FileInfoRepository @Inject constructor(private val clientFactory: ClientFa } private fun SensitivityLabelInfo.toGovernanceLabel() = GovernanceLabel(id, name, color) - private fun RetentionLabelInfo.toGovernanceLabel() = GovernanceLabel(id, name, color) - private fun HoldLabelInfo.toGovernanceLabel() = GovernanceLabel(id, name, color) companion object { diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt index f7260d70671b..213abed42ad1 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt @@ -37,14 +37,13 @@ class FileInfoViewModel @AssistedInject constructor( private fun loadGovernance() { viewModelScope.launch { - val sensitivityLabels = async { repository.fetchSensitivityLabels(file, user) } - val retentionLabels = async { repository.fetchRetentionLabels(file, user) } - val holdLabels = async { repository.fetchHoldLabels(file, user) } + val selectableLabels = async { repository.fetchAllSelectableLabels(file, user) } val entityLabels = repository.fetchEntityLabels(file, user) + val labels = selectableLabels.await() _uiState.value = GovernanceUiState.Loaded( - sensitivityLabels = sensitivityLabels.await(), - retentionLabels = retentionLabels.await(), - holdLabels = holdLabels.await(), + sensitivityLabels = labels.sensitivityLabels, + retentionLabels = labels.retentionLabels, + holdLabels = labels.holdLabels, currentSensitivityLabelId = entityLabels.sensitivityId, currentRetentionLabelIds = entityLabels.retentionIds, currentHoldLabelIds = entityLabels.holdIds diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/model/SelectableLabels.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/model/SelectableLabels.kt new file mode 100644 index 000000000000..33026f3ed4eb --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/model/SelectableLabels.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.fileInfo.model + +data class SelectableLabels( + val sensitivityLabels: List, + val retentionLabels: List, + val holdLabels: List +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 73af35445655..c05f19c7ae4a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidCommonLibraryVersion = "0.33.2" androidGifDrawableVersion = "1.2.32" androidImageCropperVersion = "4.7.0" -androidLibraryVersion ="f8dd47d3aef22c68cad19a915598cda99c71b940" +androidLibraryVersion ="bb924d75419a57a2dac04ce5979d83d91ae9db7a" androidOpensslVersion = "3.5.6" androidPluginVersion = "9.2.1" androidsvgVersion = "1.4" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index a6bb7b41e336..bccb8d81cae5 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -21856,6 +21856,14 @@ + + + + + + + + From 383f111c69f73f8e7f7e837c6f5cf1684773d9dd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 16 Jul 2026 13:29:55 +0200 Subject: [PATCH 33/35] update library Signed-off-by: alperozturk96 --- .../java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt | 8 ++++---- .../java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt | 2 +- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 8 ++++++++ 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt index e4351c376da3..0357aaabaa1c 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt @@ -9,12 +9,12 @@ package com.nextcloud.ui.fileInfo import com.nextcloud.android.lib.resources.governance.GetAllSelectableLabelsRemoteOperation import com.nextcloud.android.lib.resources.governance.GetEntityLabelsRemoteOperation -import com.nextcloud.android.lib.resources.governance.HoldLabelInfo -import com.nextcloud.android.lib.resources.governance.LabelType import com.nextcloud.android.lib.resources.governance.RemoveLabelRemoteOperation -import com.nextcloud.android.lib.resources.governance.RetentionLabelInfo -import com.nextcloud.android.lib.resources.governance.SensitivityLabelInfo import com.nextcloud.android.lib.resources.governance.SetLabelRemoteOperation +import com.nextcloud.android.lib.resources.governance.model.HoldLabelInfo +import com.nextcloud.android.lib.resources.governance.model.LabelType +import com.nextcloud.android.lib.resources.governance.model.RetentionLabelInfo +import com.nextcloud.android.lib.resources.governance.model.SensitivityLabelInfo import com.nextcloud.client.account.User import com.nextcloud.client.network.ClientFactory import com.nextcloud.common.NextcloudClient diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt index 213abed42ad1..d30a8b38bdb6 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt @@ -9,7 +9,7 @@ package com.nextcloud.ui.fileInfo import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.nextcloud.android.lib.resources.governance.LabelType +import com.nextcloud.android.lib.resources.governance.model.LabelType import com.nextcloud.client.account.User import com.nextcloud.ui.fileInfo.model.GovernanceUiState import com.owncloud.android.datamodel.OCFile diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c05f19c7ae4a..1f93ea0131c2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidCommonLibraryVersion = "0.33.2" androidGifDrawableVersion = "1.2.32" androidImageCropperVersion = "4.7.0" -androidLibraryVersion ="bb924d75419a57a2dac04ce5979d83d91ae9db7a" +androidLibraryVersion ="a7db4615d7" androidOpensslVersion = "3.5.6" androidPluginVersion = "9.2.1" androidsvgVersion = "1.4" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index bccb8d81cae5..37ddcdbc046c 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -21800,6 +21800,14 @@ + + + + + + + + From 4e89f465c47d9b12435168e9a7fb347da56ae962 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 16 Jul 2026 15:10:33 +0200 Subject: [PATCH 34/35] update library Signed-off-by: alperozturk96 --- .../ui/fileInfo/FileInfoRepository.kt | 27 ++++-- .../ui/fileInfo/FileInfoViewModel.kt | 46 ++++++++-- .../ui/fileInfo/GovernanceDetailInfo.kt | 87 +++++++++++-------- .../ui/fileInfo/model/GovernanceEvent.kt | 12 +++ .../ui/fileInfo/model/LabelOperationResult.kt | 16 ++++ app/src/main/res/values/strings.xml | 1 + gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 8 ++ 8 files changed, 146 insertions(+), 53 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceEvent.kt create mode 100644 app/src/main/java/com/nextcloud/ui/fileInfo/model/LabelOperationResult.kt diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt index 0357aaabaa1c..cb6475a180f9 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoRepository.kt @@ -20,13 +20,16 @@ import com.nextcloud.client.network.ClientFactory import com.nextcloud.common.NextcloudClient import com.nextcloud.ui.fileInfo.model.CurrentEntityLabels import com.nextcloud.ui.fileInfo.model.GovernanceLabel +import com.nextcloud.ui.fileInfo.model.LabelOperationResult import com.nextcloud.ui.fileInfo.model.SelectableLabels import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import org.apache.commons.httpclient.HttpStatus import javax.inject.Inject class FileInfoRepository @Inject constructor(private val clientFactory: ClientFactory) { @@ -56,22 +59,32 @@ class FileInfoRepository @Inject constructor(private val clientFactory: ClientFa val result = GetEntityLabelsRemoteOperation(ENTITY_TYPE_FILES, file.localId.toString()).execute(client) val labels = if (result.isSuccess) result.resultData else null CurrentEntityLabels( - sensitivityId = labels?.sensitivity?.map { it.id }?.toSet()?.first() ?: "", + sensitivityId = labels?.sensitivity?.firstOrNull()?.id ?: "", retentionIds = labels?.retention?.map { it.id }?.toSet() ?: emptySet(), holdIds = labels?.hold?.map { it.id }?.toSet() ?: emptySet() ) } - suspend fun setLabel(file: OCFile, user: User, labelType: LabelType, labelId: String): Boolean = - execute(user, false, "Could not set label") { client -> - SetLabelRemoteOperation(ENTITY_TYPE_FILES, file.localId, labelType, labelId).execute(client).isSuccess + suspend fun setLabel(file: OCFile, user: User, labelType: LabelType, labelId: String): LabelOperationResult = + execute(user, LabelOperationResult.Failure, "Could not set label") { client -> + SetLabelRemoteOperation(ENTITY_TYPE_FILES, file.localId, labelType, labelId) + .execute(client) + .toLabelOperationResult() } - suspend fun removeLabel(file: OCFile, user: User, labelType: LabelType, labelId: String): Boolean = - execute(user, false, "Could not remove label") { client -> - RemoveLabelRemoteOperation(ENTITY_TYPE_FILES, file.localId, labelType, labelId).execute(client).isSuccess + suspend fun removeLabel(file: OCFile, user: User, labelType: LabelType, labelId: String): LabelOperationResult = + execute(user, LabelOperationResult.Failure, "Could not remove label") { client -> + RemoveLabelRemoteOperation(ENTITY_TYPE_FILES, file.localId, labelType, labelId) + .execute(client) + .toLabelOperationResult() } + private fun RemoteOperationResult<*>.toLabelOperationResult(): LabelOperationResult = when { + isSuccess -> LabelOperationResult.Success + httpCode == HttpStatus.SC_FORBIDDEN -> LabelOperationResult.Forbidden + else -> LabelOperationResult.Failure + } + @Suppress("TooGenericExceptionCaught") private suspend fun execute(user: User, default: T, errorMessage: String, block: (NextcloudClient) -> T): T = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt index d30a8b38bdb6..c7b3dbe361de 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/FileInfoViewModel.kt @@ -11,13 +11,17 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.android.lib.resources.governance.model.LabelType import com.nextcloud.client.account.User +import com.nextcloud.ui.fileInfo.model.GovernanceEvent import com.nextcloud.ui.fileInfo.model.GovernanceUiState +import com.nextcloud.ui.fileInfo.model.LabelOperationResult import com.owncloud.android.datamodel.OCFile import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -31,6 +35,9 @@ class FileInfoViewModel @AssistedInject constructor( private val _uiState = MutableStateFlow(GovernanceUiState.Loading) val uiState: StateFlow = _uiState + private val _events = MutableSharedFlow() + val events: SharedFlow = _events + init { loadGovernance() } @@ -53,7 +60,7 @@ class FileInfoViewModel @AssistedInject constructor( fun setSensitivityLabel(labelId: String) { viewModelScope.launch { - if (repository.setLabel(file, user, LabelType.SENSITIVITY, labelId)) { + handleResult(repository.setLabel(file, user, LabelType.SENSITIVITY, labelId)) { updateLoaded { it.copy(currentSensitivityLabelId = labelId) } } } @@ -62,7 +69,7 @@ class FileInfoViewModel @AssistedInject constructor( fun removeSensitivityLabel() { viewModelScope.launch { val labelId = loaded()?.currentSensitivityLabelId?.takeIf { it.isNotEmpty() } ?: return@launch - if (repository.removeLabel(file, user, LabelType.SENSITIVITY, labelId)) { + handleResult(repository.removeLabel(file, user, LabelType.SENSITIVITY, labelId)) { updateLoaded { it.copy(currentSensitivityLabelId = "") } } } @@ -71,22 +78,43 @@ class FileInfoViewModel @AssistedInject constructor( fun updateRetentionLabels(newLabelIds: Set) { val currentIds = loaded()?.currentRetentionLabelIds ?: return viewModelScope.launch { - applyDiff(LabelType.RETENTION, currentIds, newLabelIds) - updateLoaded { it.copy(currentRetentionLabelIds = newLabelIds) } + handleResult(applyDiff(LabelType.RETENTION, currentIds, newLabelIds)) { + updateLoaded { it.copy(currentRetentionLabelIds = newLabelIds) } + } } } fun updateHoldLabels(newLabelIds: Set) { val currentIds = loaded()?.currentHoldLabelIds ?: return viewModelScope.launch { - applyDiff(LabelType.HOLD, currentIds, newLabelIds) - updateLoaded { it.copy(currentHoldLabelIds = newLabelIds) } + handleResult(applyDiff(LabelType.HOLD, currentIds, newLabelIds)) { + updateLoaded { it.copy(currentHoldLabelIds = newLabelIds) } + } + } + } + + private suspend fun applyDiff( + labelType: LabelType, + currentIds: Set, + newLabelIds: Set + ): LabelOperationResult { + val results = + (newLabelIds - currentIds).map { repository.setLabel(file, user, labelType, it) } + + (currentIds - newLabelIds).map { repository.removeLabel(file, user, labelType, it) } + + return when { + results.any { it is LabelOperationResult.Forbidden } -> LabelOperationResult.Forbidden + results.any { it is LabelOperationResult.Failure } -> LabelOperationResult.Failure + else -> LabelOperationResult.Success } } - private suspend fun applyDiff(labelType: LabelType, currentIds: Set, newLabelIds: Set) { - (newLabelIds - currentIds).forEach { repository.setLabel(file, user, labelType, it) } - (currentIds - newLabelIds).forEach { repository.removeLabel(file, user, labelType, it) } + private suspend fun handleResult(result: LabelOperationResult, onSuccess: () -> Unit) { + when (result) { + LabelOperationResult.Success -> onSuccess() + LabelOperationResult.Forbidden -> _events.emit(GovernanceEvent.PermissionDenied) + LabelOperationResult.Failure -> Unit + } } private fun loaded(): GovernanceUiState.Loaded? = _uiState.value as? GovernanceUiState.Loaded diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt index 86300518b3a2..3b8184c7fd0e 100644 --- a/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/GovernanceDetailInfo.kt @@ -22,11 +22,13 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.MaterialAutoCompleteTextView import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout +import com.nextcloud.ui.fileInfo.model.GovernanceEvent import com.nextcloud.ui.fileInfo.model.GovernanceLabel import com.nextcloud.ui.fileInfo.model.GovernanceUiState import com.owncloud.android.R import com.owncloud.android.databinding.FileInfoFragmentBinding import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first @@ -45,18 +47,59 @@ class GovernanceDetailInfo( fragment.lifecycleScope.launch { val state = viewModel.uiState.filterIsInstance().first() setupSensitivityLabels(state.sensitivityLabels, state.currentSensitivityLabelId) - setupRetentionLabels(state.retentionLabels) - setupHoldLabels(state.holdLabels) + setupMultiSelectField( + textInputLayout = binding.fileRetentionLabel, + labelText = binding.fileRetentionLabelText, + items = state.retentionLabels, + title = context.getString(R.string.governance_file_retention_label), + getCurrentIds = { currentLoadedState()?.currentRetentionLabelIds.orEmpty() } + ) { newIds -> viewModel.updateRetentionLabels(newIds) } + setupMultiSelectField( + textInputLayout = binding.fileHoldLabel, + labelText = binding.fileHoldLabelText, + items = state.holdLabels, + title = context.getString(R.string.governance_legal_hold_label), + getCurrentIds = { currentLoadedState()?.currentHoldLabelIds.orEmpty() } + ) { newIds -> viewModel.updateHoldLabels(newIds) } } + observeEvents() } + private fun observeEvents() { + fragment.lifecycleScope.launch { + viewModel.events.collect { event -> + when (event) { + GovernanceEvent.PermissionDenied -> { + DisplayUtils.showSnackMessage(fragment, R.string.governance_permission_denied) + currentLoadedState()?.let { refresh(it) } + } + } + } + } + } + + private fun refresh(state: GovernanceUiState.Loaded) { + if (state.retentionLabels.isNotEmpty()) { + updateMultiSelectText(binding.fileRetentionLabelText, state.retentionLabels, state.currentRetentionLabelIds) + } + if (state.holdLabels.isNotEmpty()) { + updateMultiSelectText(binding.fileHoldLabelText, state.holdLabels, state.currentHoldLabelIds) + } + if (state.sensitivityLabels.isNotEmpty()) { + val items = listOf(noneLabel()) + state.sensitivityLabels + val item = items.find { it.id == state.currentSensitivityLabelId } ?: items.first() + applySelection(binding.sensitivityLabelAutoComplete, item) + } + } + + private fun noneLabel() = GovernanceLabel("", context.getString(R.string.governance_no_label), "") + private fun setupSensitivityLabels(labels: List, currentId: String) { if (labels.isEmpty()) { binding.sensitivityLabel.visibility = View.GONE return } - val noneLabel = GovernanceLabel("", context.getString(R.string.governance_no_label), "") - val items = listOf(noneLabel) + labels + val items = listOf(noneLabel()) + labels val initialItem = items.find { it.id == currentId } ?: items.first() initDropdown( textInputLayout = binding.sensitivityLabel, @@ -72,38 +115,6 @@ class GovernanceDetailInfo( } } - private fun setupRetentionLabels(labels: List) { - if (labels.isEmpty()) { - binding.fileRetentionLabel.visibility = View.GONE - return - } - setupMultiSelectField( - textInputLayout = binding.fileRetentionLabel, - labelText = binding.fileRetentionLabelText, - items = labels, - title = context.getString(R.string.governance_file_retention_label), - getCurrentIds = { currentLoadedState()?.currentRetentionLabelIds.orEmpty() } - ) { newIds -> - viewModel.updateRetentionLabels(newIds) - } - } - - private fun setupHoldLabels(labels: List) { - if (labels.isEmpty()) { - binding.fileHoldLabel.visibility = View.GONE - return - } - setupMultiSelectField( - textInputLayout = binding.fileHoldLabel, - labelText = binding.fileHoldLabelText, - items = labels, - title = context.getString(R.string.governance_legal_hold_label), - getCurrentIds = { currentLoadedState()?.currentHoldLabelIds.orEmpty() } - ) { newIds -> - viewModel.updateHoldLabels(newIds) - } - } - private fun currentLoadedState(): GovernanceUiState.Loaded? = viewModel.uiState.value as? GovernanceUiState.Loaded private fun setupMultiSelectField( @@ -114,6 +125,10 @@ class GovernanceDetailInfo( getCurrentIds: () -> Set, onUpdate: (Set) -> Unit ) { + if (items.isEmpty()) { + textInputLayout.visibility = View.GONE + return + } textInputLayout.visibility = View.VISIBLE viewThemeUtils.material.colorTextInputLayout(textInputLayout) updateMultiSelectText(labelText, items, getCurrentIds()) diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceEvent.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceEvent.kt new file mode 100644 index 000000000000..224709585416 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/model/GovernanceEvent.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.fileInfo.model + +sealed interface GovernanceEvent { + data object PermissionDenied : GovernanceEvent +} diff --git a/app/src/main/java/com/nextcloud/ui/fileInfo/model/LabelOperationResult.kt b/app/src/main/java/com/nextcloud/ui/fileInfo/model/LabelOperationResult.kt new file mode 100644 index 000000000000..828316a68203 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/fileInfo/model/LabelOperationResult.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.fileInfo.model + +sealed interface LabelOperationResult { + data object Success : LabelOperationResult + + data object Forbidden : LabelOperationResult + + data object Failure : LabelOperationResult +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 007922587212..7734d4ce2a34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1530,6 +1530,7 @@ File retention Legal hold None + You do not have permission to change this label Governance Image details diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1f93ea0131c2..ce5b700224db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidCommonLibraryVersion = "0.33.2" androidGifDrawableVersion = "1.2.32" androidImageCropperVersion = "4.7.0" -androidLibraryVersion ="a7db4615d7" +androidLibraryVersion ="25bd90c0f21b9b16acee6a7f433063040c75f30d" androidOpensslVersion = "3.5.6" androidPluginVersion = "9.2.1" androidsvgVersion = "1.4" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 37ddcdbc046c..4a578d42e3fe 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -21064,6 +21064,14 @@ + + + + + + + + From 3646a40a9733df688295a9ffb9779e35bb2b2aa0 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 16 Jul 2026 15:14:44 +0200 Subject: [PATCH 35/35] update spotbugs for AssistedFactory Signed-off-by: alperozturk96 --- scripts/analysis/spotbugs-filter.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/analysis/spotbugs-filter.xml b/scripts/analysis/spotbugs-filter.xml index 9f1f687618cc..27f8ae23acd7 100644 --- a/scripts/analysis/spotbugs-filter.xml +++ b/scripts/analysis/spotbugs-filter.xml @@ -40,6 +40,11 @@ + + + + +