Skip to content

Commit 105daab

Browse files
committed
convert java to kotlin
Signed-off-by: alperozturk96 <alper_ozturk@proton.me>
1 parent 3e51d8b commit 105daab

13 files changed

Lines changed: 680 additions & 688 deletions

app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java

Lines changed: 0 additions & 616 deletions
This file was deleted.
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.owncloud.android.ui.adapter.localFileList
8+
9+
import android.annotation.SuppressLint
10+
import android.app.Activity
11+
import android.view.LayoutInflater
12+
import android.view.ViewGroup
13+
import androidx.annotation.VisibleForTesting
14+
import androidx.recyclerview.widget.RecyclerView
15+
import com.nextcloud.client.preferences.AppPreferences
16+
import com.nextcloud.utils.FileHelper
17+
import com.owncloud.android.R
18+
import com.owncloud.android.lib.common.utils.Log_OC
19+
import com.owncloud.android.ui.adapter.FilterableListAdapter
20+
import com.owncloud.android.ui.adapter.storagePermissionBanner.StoragePermissionBannerViewHolder
21+
import com.owncloud.android.ui.interfaces.LocalFileListFragmentInterface
22+
import com.owncloud.android.utils.FileSortOrder
23+
import com.owncloud.android.utils.MimeTypeUtil
24+
import com.owncloud.android.utils.PermissionUtil
25+
import com.owncloud.android.utils.theme.ViewThemeUtils
26+
import kotlinx.coroutines.CoroutineScope
27+
import kotlinx.coroutines.Dispatchers
28+
import kotlinx.coroutines.ExperimentalCoroutinesApi
29+
import kotlinx.coroutines.SupervisorJob
30+
import kotlinx.coroutines.cancel
31+
import kotlinx.coroutines.launch
32+
import kotlinx.coroutines.withContext
33+
import java.io.File
34+
import java.util.Locale
35+
36+
@Suppress("LongParameterList", "TooManyFunctions")
37+
class LocalFileListAdapter(
38+
localFolderPickerMode: Boolean,
39+
private val fragmentInterface: LocalFileListFragmentInterface,
40+
private val preferences: AppPreferences,
41+
private val activity: Activity,
42+
viewThemeUtils: ViewThemeUtils,
43+
private val isWithinEncryptedFolder: Boolean
44+
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(),
45+
FilterableListAdapter {
46+
47+
private var files: MutableList<File> = mutableListOf()
48+
private var allFiles: MutableList<File> = mutableListOf()
49+
private val checkedFiles: MutableSet<File> = linkedSetOf()
50+
private var currentOffset = 0
51+
52+
var gridView: Boolean = false
53+
54+
private val itemBinder = LocalFileListItemBinder(
55+
activity,
56+
viewThemeUtils,
57+
fragmentInterface,
58+
localFolderPickerMode,
59+
isWithinEncryptedFolder,
60+
::isCheckedFile
61+
)
62+
63+
@OptIn(ExperimentalCoroutinesApi::class)
64+
private val backgroundScope = CoroutineScope(SupervisorJob() + Dispatchers.IO.limitedParallelism(1))
65+
66+
init {
67+
setHasStableIds(true)
68+
}
69+
70+
val filesCount: Int
71+
get() = files.size
72+
73+
val checkedFilesPath: Array<String>
74+
get() {
75+
val result = FileHelper.listFilesRecursive(checkedFiles)
76+
Log_OC.d(TAG, "Returning ${result.size} selected files")
77+
return result.toTypedArray()
78+
}
79+
80+
fun isCheckedFile(file: File): Boolean = checkedFiles.contains(file)
81+
82+
fun addCheckedFile(file: File) {
83+
checkedFiles.add(file)
84+
}
85+
86+
fun removeCheckedFile(file: File) {
87+
checkedFiles.remove(file)
88+
}
89+
90+
fun addAllFilesToCheckedFiles() {
91+
if (isWithinEncryptedFolder) {
92+
checkedFiles.addAll(allFiles.filter { it.isFile })
93+
} else {
94+
checkedFiles.addAll(files)
95+
}
96+
}
97+
98+
fun removeAllFilesFromCheckedFiles() {
99+
checkedFiles.clear()
100+
}
101+
102+
fun checkedFilesCount(): Int = checkedFiles.size
103+
104+
fun getItemPosition(file: File): Int = files.indexOf(file)
105+
106+
private fun shouldShowHeader(): Boolean = !PermissionUtil.checkStoragePermission(activity)
107+
108+
private val headerOffset: Int
109+
get() = if (shouldShowHeader()) HEADER_ITEM_COUNT else 0
110+
111+
override fun getItemCount(): Int = files.size + FOOTER_ITEM_COUNT + headerOffset
112+
113+
override fun getItemId(position: Int): Long = getStableItemId(position, headerOffset, files)
114+
115+
override fun getItemViewType(position: Int): Int {
116+
val offset = headerOffset
117+
118+
return when {
119+
offset == HEADER_ITEM_COUNT && position == 0 -> VIEW_TYPE_HEADER
120+
position == files.size + offset -> VIEW_TYPE_FOOTER
121+
MimeTypeUtil.isImageOrVideo(files[position - offset]) -> VIEW_TYPE_IMAGE
122+
else -> VIEW_TYPE_ITEM
123+
}
124+
}
125+
126+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
127+
val inflater = LayoutInflater.from(activity)
128+
129+
return when (viewType) {
130+
VIEW_TYPE_ITEM, VIEW_TYPE_IMAGE -> if (gridView) {
131+
LocalFileListGridItemViewHolder(inflater.inflate(R.layout.grid_item, parent, false))
132+
} else {
133+
LocalFileListItemViewHolder(inflater.inflate(R.layout.list_item, parent, false))
134+
}
135+
136+
VIEW_TYPE_FOOTER -> LocalFileListFooterViewHolder(inflater.inflate(R.layout.list_footer, parent, false))
137+
138+
VIEW_TYPE_HEADER -> StoragePermissionBannerViewHolder(
139+
activity,
140+
inflater.inflate(R.layout.storage_permission_warning_banner, parent, false)
141+
)
142+
143+
else -> throw IllegalArgumentException("Invalid viewType: $viewType")
144+
}
145+
}
146+
147+
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
148+
val offset = headerOffset
149+
val isHeader = offset == HEADER_ITEM_COUNT && position == 0
150+
val isFooter = position == files.size + offset
151+
152+
when {
153+
// the header has no dynamic binding
154+
isHeader -> Unit
155+
156+
isFooter -> (holder as LocalFileListFooterViewHolder).footerText.text = footerText
157+
158+
else -> files.getOrNull(position - offset)?.let { file ->
159+
itemBinder.bind(holder as LocalFileListGridItemViewHolder, file)
160+
}
161+
}
162+
}
163+
164+
/**
165+
* Change the adapted directory for a new one
166+
*
167+
* @param directory New file to adapt. Can be NULL, meaning "no content to adapt".
168+
*/
169+
fun swapDirectory(directory: File?) {
170+
fragmentInterface.setLoading(true)
171+
currentOffset = 0
172+
173+
backgroundScope.launch {
174+
showFirstPage(loadFirstPage(directory))
175+
176+
// load the remaining folders first, then all files
177+
loadRemainingEntries(directory, fetchFolders = true)
178+
179+
currentOffset = 0
180+
loadRemainingEntries(directory, fetchFolders = false)
181+
}
182+
}
183+
184+
private fun loadFirstPage(directory: File?): List<File> {
185+
val firstPage = FileHelper.listDirectoryEntries(directory, currentOffset, PAGE_SIZE, true)
186+
currentOffset += PAGE_SIZE
187+
188+
return if (firstPage.isEmpty()) firstPage else sortAndFilterHiddenEntries(firstPage)
189+
}
190+
191+
private suspend fun loadRemainingEntries(directory: File?, fetchFolders: Boolean) {
192+
while (true) {
193+
val nextPage = FileHelper.listDirectoryEntries(directory, currentOffset, PAGE_SIZE, fetchFolders)
194+
if (nextPage.isEmpty()) {
195+
return
196+
}
197+
198+
currentOffset += PAGE_SIZE
199+
appendPage(sortAndFilterHiddenEntries(nextPage))
200+
}
201+
}
202+
203+
private fun sortAndFilterHiddenEntries(page: List<File>): List<File> {
204+
val visibleEntries = if (preferences.isShowHiddenFilesEnabled) page else page.filterNot { it.isHidden }
205+
val sortOrder = preferences.getSortOrderByType(FileSortOrder.Type.localFileListView)
206+
207+
return sortOrder.sortLocalFiles(visibleEntries.toMutableList())
208+
}
209+
210+
@SuppressLint("NotifyDataSetChanged")
211+
private suspend fun showFirstPage(firstPage: List<File>) = withContext(Dispatchers.Main) {
212+
files = firstPage.toMutableList()
213+
allFiles = firstPage.toMutableList()
214+
215+
notifyDataSetChanged()
216+
fragmentInterface.setLoading(false)
217+
}
218+
219+
private suspend fun appendPage(page: List<File>) = withContext(Dispatchers.Main) {
220+
val startPositionInAdapter = files.size + headerOffset
221+
222+
files.addAll(page)
223+
allFiles.addAll(page)
224+
225+
Log_OC.d(TAG, "appendPage, item size: ${allFiles.size}")
226+
notifyItemRangeInserted(startPositionInAdapter, page.size)
227+
}
228+
229+
@SuppressLint("NotifyDataSetChanged")
230+
fun setSortOrder(sortOrder: FileSortOrder) {
231+
fragmentInterface.setLoading(true)
232+
233+
backgroundScope.launch {
234+
val sortedFiles = sortOrder.sortLocalFiles(files.toMutableList())
235+
236+
withContext(Dispatchers.Main) {
237+
files = sortedFiles.toMutableList()
238+
allFiles = sortedFiles.toMutableList()
239+
240+
notifyDataSetChanged()
241+
fragmentInterface.setLoading(false)
242+
}
243+
}
244+
}
245+
246+
@SuppressLint("NotifyDataSetChanged")
247+
override fun filter(text: String) {
248+
files = if (text.isEmpty()) allFiles else filterByName(allFiles, text).toMutableList()
249+
notifyDataSetChanged()
250+
}
251+
252+
private val footerText: String
253+
get() {
254+
val (filesCount, foldersCount) = countFooterEntries(files)
255+
return generateFooterText(filesCount, foldersCount)
256+
}
257+
258+
private fun generateFooterText(filesCount: Int, foldersCount: Int): String {
259+
val resources = activity.resources
260+
val fileText = resources.getQuantityString(R.plurals.file_list__footer__file, filesCount, filesCount)
261+
val folderText = resources.getQuantityString(R.plurals.file_list__footer__folder, foldersCount, foldersCount)
262+
263+
return when {
264+
filesCount + foldersCount <= 0 -> ""
265+
foldersCount <= 0 -> fileText
266+
filesCount <= 0 -> folderText
267+
else -> "$fileText, $folderText"
268+
}
269+
}
270+
271+
@SuppressLint("NotifyDataSetChanged")
272+
@VisibleForTesting
273+
fun setFiles(newFiles: List<File>) {
274+
files = newFiles.toMutableList()
275+
allFiles = newFiles.toMutableList()
276+
277+
notifyDataSetChanged()
278+
fragmentInterface.setLoading(false)
279+
}
280+
281+
fun cleanup() {
282+
backgroundScope.cancel()
283+
}
284+
285+
companion object {
286+
private val TAG: String = LocalFileListAdapter::class.java.simpleName
287+
288+
private const val VIEW_TYPE_ITEM = 0
289+
private const val VIEW_TYPE_FOOTER = 1
290+
private const val VIEW_TYPE_IMAGE = 2
291+
private const val VIEW_TYPE_HEADER = 3
292+
293+
private const val PAGE_SIZE = 50
294+
private const val HEADER_ITEM_COUNT = 1
295+
private const val FOOTER_ITEM_COUNT = 1
296+
297+
private const val HEADER_ID = Long.MIN_VALUE
298+
private const val FOOTER_ID = Long.MIN_VALUE + 1
299+
300+
@VisibleForTesting
301+
fun getStableItemId(position: Int, headerOffset: Int, files: List<File>): Long {
302+
if (headerOffset == HEADER_ITEM_COUNT && position == 0) {
303+
return HEADER_ID
304+
}
305+
306+
val file = files.getOrNull(position - headerOffset)
307+
return file?.absolutePath?.hashCode()?.toLong() ?: FOOTER_ID
308+
}
309+
310+
@VisibleForTesting
311+
fun filterByName(files: List<File>, text: String): List<File> {
312+
val filterText = text.lowercase(Locale.getDefault())
313+
return files.filter { it.name.lowercase(Locale.getDefault()).contains(filterText) }
314+
}
315+
316+
/**
317+
* Counts the entries shown in the footer: folders are always counted, files only when they are not hidden.
318+
*
319+
* @return amount of files and amount of folders
320+
*/
321+
@VisibleForTesting
322+
fun countFooterEntries(files: List<File>): Pair<Int, Int> {
323+
val (folders, plainFiles) = files.partition { it.isDirectory }
324+
return plainFiles.count { !it.isHidden } to folders.size
325+
}
326+
}
327+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.owncloud.android.ui.adapter.localFileList
8+
9+
import android.view.View
10+
import android.widget.TextView
11+
import androidx.recyclerview.widget.RecyclerView
12+
import com.owncloud.android.R
13+
14+
internal class LocalFileListFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
15+
val footerText: TextView = itemView.findViewById(R.id.footerText)
16+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.owncloud.android.ui.adapter.localFileList
8+
9+
import android.view.View
10+
import android.widget.ImageView
11+
import android.widget.LinearLayout
12+
import android.widget.TextView
13+
import androidx.recyclerview.widget.RecyclerView
14+
import com.owncloud.android.R
15+
16+
internal open class LocalFileListGridItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
17+
val fileName: TextView = itemView.findViewById(R.id.Filename)
18+
val thumbnail: ImageView = itemView.findViewById(R.id.thumbnail)
19+
val checkbox: ImageView = itemView.findViewById(R.id.custom_checkbox)
20+
val itemLayout: LinearLayout = itemView.findViewById(R.id.ListItemLayout)
21+
22+
init {
23+
itemView.findViewById<View>(R.id.sharedIcon).visibility = View.GONE
24+
itemView.findViewById<View>(R.id.favorite_action).visibility = View.GONE
25+
itemView.findViewById<View>(R.id.localFileIndicator).visibility = View.GONE
26+
}
27+
}

0 commit comments

Comments
 (0)