(...);
26 | }
27 | -keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
28 | **[] $VALUES;
29 | public *;
30 | }
31 | -keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder {
32 | *** rewind();
33 | }
--------------------------------------------------------------------------------
/file-picker/src/androidTest/java/ir/one_developer/file_picker/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package ir.one_developer.file_picker
2 |
3 | /**
4 | * Instrumented test, which will execute on an Android device.
5 | *
6 | * See [testing documentation](http://d.android.com/tools/testing).
7 | */
8 | //@RunWith(AndroidJUnit4::class)
9 | class ExampleInstrumentedTest {
10 | // @Test
11 | // fun useAppContext() {
12 | // // Context of the app under test.
13 | // val appContext = InstrumentationRegistry.getInstrumentation().targetContext
14 | // assertEquals("ir.one_developer.file_picker.test", appContext.packageName)
15 | // }
16 | }
--------------------------------------------------------------------------------
/file-picker/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/Const.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | enum class ListDirection : Parcelable {
8 | LTR,
9 | RTL
10 | }
11 |
12 | @Parcelize
13 | enum class FileType : Parcelable {
14 | VIDEO,
15 | IMAGE,
16 | AUDIO,
17 | }
18 |
19 | const val PAGE_SIZE = 10
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/FilePicker.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker
2 |
3 | import android.Manifest
4 | import android.content.DialogInterface
5 | import android.content.res.ColorStateList
6 | import android.graphics.Color
7 | import android.os.Build
8 | import android.os.Bundle
9 | import android.view.LayoutInflater
10 | import android.view.View
11 | import android.view.ViewGroup
12 | import androidx.activity.result.contract.ActivityResultContracts
13 | import androidx.annotation.ColorInt
14 | import androidx.annotation.FloatRange
15 | import androidx.appcompat.app.AppCompatActivity
16 | import androidx.core.view.isVisible
17 | import androidx.fragment.app.FragmentManager
18 | import androidx.paging.LoadState
19 | import androidx.recyclerview.widget.GridLayoutManager
20 | import androidx.recyclerview.widget.RecyclerView
21 | import com.github.file_picker.adapter.ItemAdapter
22 | import com.github.file_picker.data.model.Media
23 | import com.github.file_picker.data.repository.FilesRepository
24 | import com.github.file_picker.extension.hasPermission
25 | import com.github.file_picker.listener.OnItemClickListener
26 | import com.github.file_picker.listener.OnSubmitClickListener
27 | import com.google.android.material.bottomsheet.BottomSheetBehavior
28 | import com.google.android.material.bottomsheet.BottomSheetDialog
29 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment
30 | import ir.one_developer.file_picker.R
31 | import ir.one_developer.file_picker.databinding.FilePickerBinding
32 | import kotlinx.coroutines.CoroutineScope
33 | import kotlinx.coroutines.Dispatchers
34 | import kotlinx.coroutines.launch
35 | import kotlin.properties.Delegates
36 |
37 | /**
38 | *
39 | * A fragment that shows a list of items as a modal bottom sheet.
40 | *
41 | * You can show this modal bottom sheet from your activity like this:
42 | *
43 | * FilePicker.Builder([Context]).buildAndShow()
44 | *
45 | */
46 | class FilePicker private constructor(
47 | builder: Builder
48 | ) : BottomSheetDialogFragment() {
49 |
50 | // This property is only valid between onCreateView and onDestroyView.
51 | private val binding get() = _binding!!
52 | private var _binding: FilePickerBinding? = null
53 |
54 | private var itemsAdapter: ItemAdapter? = null
55 | private lateinit var repository: FilesRepository
56 |
57 | private var title: String
58 | private var titleTextColor by Delegates.notNull()
59 | private var submitText: String
60 | private var submitTextColor by Delegates.notNull()
61 | private var selectedFiles: List
62 | private var fileType: FileType
63 | private var listDirection: ListDirection
64 | private var limitCount by Delegates.notNull()
65 | private var accentColor by Delegates.notNull()
66 | private var gridSpanCount by Delegates.notNull()
67 | private var cancellable by Delegates.notNull()
68 | private var overlayAlpha by Delegates.notNull()
69 |
70 | private var onItemClickListener: OnItemClickListener?
71 | private var onSubmitClickListener: OnSubmitClickListener?
72 |
73 | init {
74 | this.title = builder.title
75 | this.titleTextColor = builder.titleTextColor
76 | this.submitText = builder.submitText
77 | this.submitTextColor = builder.submitTextColor
78 | this.selectedFiles = builder.selectedFiles
79 | this.fileType = builder.fileType
80 | this.listDirection = builder.listDirection
81 | this.cancellable = builder.cancellable
82 | this.gridSpanCount = builder.gridSpanCount
83 | this.limitCount = builder.limitCount
84 | this.accentColor = builder.accentColor
85 | this.overlayAlpha = builder.overlayAlpha
86 | this.onItemClickListener = builder.onItemClickListener
87 | this.onSubmitClickListener = builder.onSubmitClickListener
88 | }
89 |
90 | private var requestPermission =
91 | registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
92 | if (isGranted) loadFiles()
93 | else dismissAllowingStateLoss()
94 | }
95 |
96 | class Builder(
97 | private val appCompatActivity: AppCompatActivity
98 | ) {
99 | var title: String = DEFAULT_TITLE
100 | private set
101 | var titleTextColor: Int = DEFAULT_TITLE_TEXT_COLOR
102 | private set
103 | var submitText: String = DEFAULT_SUBMIT_TEXT
104 | private set
105 | var submitTextColor: Int = DEFAULT_SUBMIT_TEXT_COLOR
106 | private set
107 | var accentColor: Int = DEFAULT_ACCENT_COLOR
108 | private set
109 | var selectedFiles: List = arrayListOf()
110 | private set
111 | var limitCount: Int = DEFAULT_LIMIT_COUNT
112 | private set
113 | var fileType: FileType = DEFAULT_FILE_TYPE
114 | private set
115 | var gridSpanCount: Int = DEFAULT_SPAN_COUNT
116 | private set
117 | var cancellable: Boolean = DEFAULT_CANCELABLE
118 | private set
119 | var listDirection: ListDirection = DEFAULT_LIST_DIRECTION
120 | private set
121 | var onItemClickListener: OnItemClickListener? = null
122 | private set
123 | var onSubmitClickListener: OnSubmitClickListener? = null
124 | private set
125 | var overlayAlpha: Float = DEFAULT_OVERLAY_ALPHA
126 | private set
127 |
128 | /**
129 | * Set title
130 | *
131 | * @param title
132 | * @return
133 | */
134 | fun setTitle(title: String) = apply { this.title = title }
135 |
136 | /**
137 | * Set title text color
138 | *
139 | * @param color
140 | * @return
141 | */
142 | fun setTitleTextColor(color: Int) = apply { this.titleTextColor = color }
143 |
144 | /**
145 | * Set submit text
146 | *
147 | * @param text
148 | * @return
149 | */
150 | fun setSubmitText(text: String) = apply { this.submitText = text }
151 |
152 | /**
153 | * Set submit text color
154 | *
155 | * @param color
156 | * @return
157 | */
158 | fun setSubmitTextColor(color: Int) = apply { this.submitTextColor = color }
159 |
160 | /**
161 | * Set accent color
162 | *
163 | * @param color
164 | * @return
165 | */
166 | fun setAccentColor(@ColorInt color: Int) = apply { this.accentColor = color }
167 |
168 | /**
169 | * Set selected files, for show as selected style in list
170 | *
171 | * @param selectedFiles
172 | * @return
173 | */
174 | fun setSelectedFiles(selectedFiles: List) =
175 | apply { this.selectedFiles = selectedFiles }
176 |
177 | /**
178 | * Set limit item selection
179 | *
180 | * @param limit the limit item can select
181 | * @return
182 | */
183 | fun setLimitItemSelection(limit: Int) = apply { this.limitCount = limit }
184 |
185 | /**
186 | * Set file type
187 | *
188 | * @param fileType the [FileType]
189 | * @return
190 | */
191 | fun setFileType(fileType: FileType) = apply { this.fileType = fileType }
192 |
193 | /**
194 | * Set grid span count
195 | *
196 | * @param spanCount the list span count
197 | * @return
198 | */
199 | fun setGridSpanCount(spanCount: Int) = apply { this.gridSpanCount = spanCount }
200 |
201 | /**
202 | * Set cancellable
203 | *
204 | * @param cancellable
205 | * @return
206 | */
207 | fun setCancellable(cancellable: Boolean) = apply { this.cancellable = cancellable }
208 |
209 | /**
210 | * Set list direction
211 | *
212 | * @param listDirection [ListDirection]
213 | * @return
214 | */
215 | fun setListDirection(listDirection: ListDirection) =
216 | apply { this.listDirection = listDirection }
217 |
218 | /**
219 | * Set on submit click listener
220 | *
221 | * @param onSubmitClickListener
222 | * @return
223 | */
224 | fun setOnSubmitClickListener(
225 | onSubmitClickListener: OnSubmitClickListener?
226 | ) = apply { this.onSubmitClickListener = onSubmitClickListener }
227 |
228 | /**
229 | * Set on item click listener
230 | *
231 | * @param onItemClickListener
232 | * @return
233 | */
234 | fun setOnItemClickListener(
235 | onItemClickListener: OnItemClickListener?
236 | ) = apply { this.onItemClickListener = onItemClickListener }
237 |
238 | /**
239 | * Set overlay alpha
240 | *
241 | * @param alpha
242 | */
243 | fun setOverlayAlpha(
244 | alpha: Float
245 | ) = apply { this.overlayAlpha = alpha }
246 |
247 | /**
248 | * Build file picker instance
249 | */
250 | fun build() = FilePicker(this)
251 |
252 | /**
253 | * Build file picker and show it
254 | */
255 | fun buildAndShow() = build().show(
256 | appCompatActivity.supportFragmentManager,
257 | "file.picker"
258 | )
259 | }
260 |
261 | override fun onCreate(savedInstanceState: Bundle?) {
262 | super.onCreate(savedInstanceState)
263 | setStyle(STYLE_NORMAL, R.style.BottomSheetDialog)
264 | }
265 |
266 | override fun onCreateView(
267 | inflater: LayoutInflater,
268 | container: ViewGroup?,
269 | savedInstanceState: Bundle?
270 | ): View {
271 | _binding = FilePickerBinding.inflate(inflater, container, false)
272 | return binding.root
273 | }
274 |
275 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
276 | repository = FilesRepository(requireActivity().application)
277 | setCancellableDialog(cancellable)
278 | setupViews()
279 | }
280 |
281 | override fun onStart() {
282 | super.onStart()
283 | val readStoragePermission = getRequiredPermissionByType()
284 | if (!hasPermission(readStoragePermission)) {
285 | requestPermission(readStoragePermission)
286 | return
287 | }
288 | loadFiles()
289 | }
290 |
291 | /**
292 | * Get required permission by file type
293 | *
294 | * @return the permission
295 | */
296 | private fun getRequiredPermissionByType() : String {
297 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
298 | return when(fileType) {
299 | FileType.AUDIO -> Manifest.permission.READ_MEDIA_AUDIO
300 | FileType.VIDEO -> Manifest.permission.READ_MEDIA_VIDEO
301 | FileType.IMAGE -> Manifest.permission.READ_MEDIA_IMAGES
302 | }
303 | }
304 | return Manifest.permission.READ_EXTERNAL_STORAGE
305 | }
306 |
307 | override fun show(manager: FragmentManager, tag: String?) {
308 | if (isShown) return
309 | super.show(manager, tag)
310 | isShown = true
311 | }
312 |
313 | override fun onDismiss(dialog: DialogInterface) {
314 | isShown = false
315 | super.onDismiss(dialog)
316 | }
317 |
318 | override fun onDestroyView() {
319 | super.onDestroyView()
320 | _binding = null
321 | requestPermission.unregister()
322 | }
323 |
324 | /**
325 | * Request permission
326 | *
327 | * @param permission
328 | */
329 | private fun requestPermission(permission: String) = requestPermission.launch(permission)
330 |
331 | /**
332 | * Set cancellable dialog
333 | *
334 | * @param cancellable
335 | */
336 | private fun setCancellableDialog(cancellable: Boolean) {
337 | dialog?.setCancelable(cancellable)
338 | dialog?.setCanceledOnTouchOutside(cancellable)
339 | }
340 |
341 | /**
342 | * Set fixed submit button
343 | *
344 | */
345 | private fun setFixedSubmitButton() {
346 | val behavior: BottomSheetBehavior<*> = (dialog as BottomSheetDialog).behavior
347 | behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
348 | override fun onStateChanged(bottomSheet: View, newState: Int) {}
349 | override fun onSlide(bottomSheet: View, slideOffset: Float) {
350 | val button = binding.buttonContainer
351 | button.y =
352 | ((bottomSheet.parent as View).height - bottomSheet.top - button.height).toFloat()
353 | }
354 | }.apply {
355 | val root = binding.root
356 | root.post { onSlide(root.parent as View, 0f) }
357 | })
358 | }
359 |
360 | /**
361 | * Setup views
362 | */
363 | private fun setupViews() = binding.apply {
364 | changeSubmitButtonState()
365 | setupRecyclerView(rvFiles)
366 | setFixedSubmitButton()
367 | updateSelectedCount()
368 |
369 | cardLine.setCardBackgroundColor(ColorStateList.valueOf(accentColor))
370 | progress.indeterminateTintList = ColorStateList.valueOf(accentColor)
371 | tvTitle.apply {
372 | text = title
373 | setTextColor(titleTextColor)
374 | }
375 | btnSubmit.apply {
376 | text = submitText
377 | setOnClickListener {
378 | submitList()
379 | dismissAllowingStateLoss()
380 | }
381 | }
382 | }
383 |
384 | /**
385 | * Show selected count
386 | *
387 | */
388 | private fun updateSelectedCount() {
389 | val selectedCount = getSelectedItems()?.size ?: 0
390 | binding.tvTitle.text = "$title ($selectedCount/$limitCount)"
391 | }
392 |
393 | /**
394 | * Setup recycler view
395 | *
396 | * @param recyclerView
397 | */
398 | private fun setupRecyclerView(recyclerView: RecyclerView) {
399 | itemsAdapter = ItemAdapter(
400 | accentColor = accentColor,
401 | overlayAlpha = overlayAlpha,
402 | limitSelectionCount = limitCount,
403 | listener = { itemPosition ->
404 | setupOnItemClickListener(itemPosition)
405 | updateSelectedCount()
406 | changeSubmitButtonState()
407 | }
408 | )
409 | recyclerView.apply {
410 | layoutDirection = when (listDirection) {
411 | ListDirection.LTR -> RecyclerView.LAYOUT_DIRECTION_LTR
412 | ListDirection.RTL -> RecyclerView.LAYOUT_DIRECTION_RTL
413 | }
414 | layoutManager = GridLayoutManager(requireContext(), gridSpanCount)
415 | adapter = itemsAdapter
416 | }
417 | }
418 |
419 | /**
420 | * Setup on item click listener
421 | *
422 | * @param position
423 | */
424 | private fun setupOnItemClickListener(position: Int) {
425 | if (onItemClickListener == null) return
426 | if (itemsAdapter == null) return
427 | val media = itemsAdapter?.snapshot()?.items?.get(position) ?: return
428 | onItemClickListener?.onClick(media, position, itemsAdapter!!)
429 | }
430 |
431 | /**
432 | * change submit button state
433 | * if has selected item change to enable otherwise disable it
434 | */
435 | private fun changeSubmitButtonState() = binding.btnSubmit.apply {
436 | isEnabled = hasSelectedItem()
437 | if (isEnabled) {
438 | setTextColor(submitTextColor)
439 | setBackgroundColor(accentColor)
440 | return@apply
441 | }
442 | setTextColor(Color.GRAY)
443 | setBackgroundColor(Color.LTGRAY)
444 | }
445 |
446 | /**
447 | * Load files
448 | */
449 | private fun loadFiles() = CoroutineScope(Dispatchers.IO).launch {
450 | itemsAdapter?.addLoadStateListener { state ->
451 | binding.progress.isVisible = state.source.refresh is LoadState.Loading
452 | if (state.source.refresh is LoadState.NotLoading) {
453 | selectedFiles.forEach { media ->
454 | itemsAdapter?.snapshot()?.items?.find { it.id == media.id }?.let {
455 | it.isSelected = media.isSelected
456 | it.order = media.order
457 | }
458 | }
459 | updateSelectedCount()
460 | setFixedSubmitButton()
461 | changeSubmitButtonState()
462 | }
463 | }
464 | repository.getFiles(fileType = fileType).collect { pagingData ->
465 | itemsAdapter?.submitData(pagingData)
466 | }
467 | }
468 |
469 | /**
470 | * Submit list
471 | */
472 | private fun submitList() = getSelectedItems()?.let {
473 | onSubmitClickListener?.onClick(it)
474 | }
475 |
476 | /**
477 | * Get selected items
478 | *
479 | * @return
480 | */
481 | private fun getSelectedItems(): List? =
482 | itemsAdapter?.snapshot()?.items?.filter { it.isSelected }?.sortedBy { it.order }
483 |
484 | /**
485 | * Has selected item
486 | *
487 | * @return
488 | */
489 | private fun hasSelectedItem(): Boolean = !getSelectedItems().isNullOrEmpty()
490 |
491 | companion object {
492 | // Defaults
493 | const val DEFAULT_SPAN_COUNT = 2
494 | const val DEFAULT_LIMIT_COUNT = 1
495 | const val DEFAULT_CANCELABLE = true
496 | val DEFAULT_FILE_TYPE = FileType.IMAGE
497 | val DEFAULT_LIST_DIRECTION = ListDirection.LTR
498 |
499 | const val DEFAULT_ACCENT_COLOR = Color.BLACK
500 | const val DEFAULT_TITLE = "Choose File"
501 | const val DEFAULT_TITLE_TEXT_COLOR = DEFAULT_ACCENT_COLOR
502 |
503 | @FloatRange(from = 0.0, to = 1.0)
504 | const val DEFAULT_OVERLAY_ALPHA = 0.5F
505 |
506 | const val DEFAULT_SUBMIT_TEXT = "Submit"
507 | const val DEFAULT_SUBMIT_TEXT_COLOR = Color.WHITE
508 |
509 | private var isShown: Boolean = false
510 | }
511 |
512 | }
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/adapter/FilePickerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.adapter
2 |
3 | interface FilePickerAdapter {
4 | fun setSelected(position: Int)
5 | }
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/adapter/ItemAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.paging.PagingDataAdapter
6 | import androidx.recyclerview.widget.DiffUtil
7 | import androidx.recyclerview.widget.ListAdapter
8 | import com.github.file_picker.FilePicker
9 | import com.github.file_picker.extension.isValidPosition
10 | import com.github.file_picker.data.model.Media
11 | import ir.one_developer.file_picker.databinding.ItemLayoutBinding
12 | import java.util.Locale.filter
13 |
14 | internal class ItemAdapter(
15 | private var accentColor: Int = FilePicker.DEFAULT_ACCENT_COLOR,
16 | private var overlayAlpha: Float = FilePicker.DEFAULT_OVERLAY_ALPHA,
17 | private var limitSelectionCount: Int = FilePicker.DEFAULT_LIMIT_COUNT,
18 | private var listener: ((Int) -> Unit)? = null
19 | ) : PagingDataAdapter(COMPARATOR), FilePickerAdapter {
20 |
21 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemVH(
22 | listener = listener,
23 | accentColor = accentColor,
24 | overlayAlpha =overlayAlpha,
25 | limitSelectionCount = limitSelectionCount,
26 | binding = ItemLayoutBinding.inflate(
27 | LayoutInflater.from(parent.context),
28 | parent,
29 | false
30 | )
31 | )
32 |
33 | override fun onBindViewHolder(holder: ItemVH, position: Int) {
34 | getItem(position)?.let { holder.bind(it) }
35 | }
36 |
37 | /**
38 | * Set selected
39 | *
40 | * @param position the selected item position
41 | */
42 | override fun setSelected(position: Int) {
43 | if (limitSelectionCount > 1) {
44 | val item = getItem(position) ?: return
45 | val selectedItems = snapshot().items.filter { it.isSelected && it.id != item.id }
46 | val selectedItemCount = selectedItems.size
47 |
48 | if (item.isSelected) {
49 | item.isSelected = false
50 | notifyItemChanged(position)
51 | selectedItems.forEach { media ->
52 | if (media.order > item.order) {
53 | media.order--
54 | notifyItemChanged(snapshot().items.indexOf(media))
55 | }
56 | }
57 | return
58 | }
59 |
60 | if (selectedItemCount < limitSelectionCount) {
61 | item.isSelected = true
62 | item.order = selectedItemCount + 1
63 | notifyItemChanged(position)
64 | }
65 | return
66 | }
67 |
68 | if (!snapshot().items.isValidPosition(lastSelectedPosition)) {
69 | lastSelectedPosition = position
70 | }
71 | getItem(lastSelectedPosition)?.isSelected = false
72 | notifyItemChanged(lastSelectedPosition)
73 | lastSelectedPosition = position
74 | getItem(lastSelectedPosition)?.isSelected = true
75 | notifyItemChanged(lastSelectedPosition)
76 | }
77 |
78 | companion object {
79 | private var lastSelectedPosition = -1
80 |
81 | private val COMPARATOR = object : DiffUtil.ItemCallback() {
82 | override fun areItemsTheSame(
83 | oldItem: Media,
84 | newItem: Media
85 | ) = oldItem.id == newItem.id
86 |
87 | override fun areContentsTheSame(
88 | oldItem: Media,
89 | newItem: Media
90 | ) = oldItem == newItem
91 | }
92 | }
93 |
94 | }
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/adapter/ItemVH.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.adapter
2 |
3 | import android.graphics.drawable.Drawable
4 | import androidx.core.view.isVisible
5 | import androidx.recyclerview.widget.RecyclerView
6 | import com.bumptech.glide.Glide
7 | import com.bumptech.glide.load.DataSource
8 | import com.bumptech.glide.load.engine.DiskCacheStrategy
9 | import com.bumptech.glide.load.engine.GlideException
10 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
11 | import com.bumptech.glide.request.RequestListener
12 | import com.bumptech.glide.request.target.Target
13 | import com.github.file_picker.FileType
14 | import com.github.file_picker.data.model.Media
15 | import com.github.file_picker.extension.getMusicCoverArt
16 | import com.github.file_picker.extension.lastPathTitle
17 | import com.github.file_picker.extension.size
18 | import ir.one_developer.file_picker.R
19 | import ir.one_developer.file_picker.databinding.ItemLayoutBinding
20 |
21 | internal class ItemVH(
22 | private val listener: ((Int) -> Unit)?,
23 | private val binding: ItemLayoutBinding,
24 | private val accentColor: Int,
25 | private val overlayAlpha: Float,
26 | private val limitSelectionCount: Int
27 | ) : RecyclerView.ViewHolder(binding.root) {
28 |
29 | init {
30 | binding.apply {
31 | frameChecked.setBackgroundColor(accentColor)
32 | frameChecked.alpha = overlayAlpha
33 | card.setOnClickListener {
34 | listener?.invoke(bindingAdapterPosition)
35 | }
36 | }
37 | }
38 |
39 | fun bind(item: Media) = binding.apply {
40 | cardErrorState.isVisible = false
41 | frameChecked.isVisible = item.isSelected
42 | cardOrder.isVisible = item.isSelected && limitSelectionCount > 1
43 | ivChecked.isVisible = item.isSelected && limitSelectionCount == 1
44 |
45 | tvOrder.text = "${item.order}"
46 | tvFileSize.text = item.file.size()
47 |
48 | val previewImage: Any? = when (item.type) {
49 | FileType.AUDIO -> {
50 | tvPath.text = item.file.name
51 | ivMediaIcon.setImageResource(R.drawable.ic_audiotrack)
52 | item.file.getMusicCoverArt()
53 | }
54 | FileType.IMAGE -> {
55 | tvPath.text = item.file.lastPathTitle()
56 | ivMediaIcon.setImageResource(R.drawable.ic_image)
57 | item.file
58 | }
59 | FileType.VIDEO -> {
60 | tvPath.text = item.file.lastPathTitle()
61 | ivMediaIcon.setImageResource(R.drawable.ic_play)
62 | item.file
63 | }
64 | }
65 |
66 | Glide.with(ivImage)
67 | .load(previewImage)
68 | .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
69 | .transition(DrawableTransitionOptions.withCrossFade())
70 | .listener(object : RequestListener {
71 | override fun onLoadFailed(
72 | e: GlideException?,
73 | model: Any?,
74 | target: Target?,
75 | isFirstResource: Boolean
76 | ): Boolean {
77 | setErrorState(item.type)
78 | return false
79 | }
80 |
81 | override fun onResourceReady(
82 | resource: Drawable?,
83 | model: Any?,
84 | target: Target?,
85 | dataSource: DataSource?,
86 | isFirstResource: Boolean
87 | ): Boolean = false
88 | })
89 | .into(ivImage)
90 | }
91 |
92 | private fun setErrorState(type: FileType) = binding.apply {
93 | cardErrorState.isVisible = true
94 | when (type) {
95 | FileType.VIDEO -> Glide.with(ivErrorIcon).load(R.drawable.ic_play).into(ivErrorIcon)
96 | FileType.IMAGE -> Glide.with(ivErrorIcon).load(R.drawable.ic_image).into(ivErrorIcon)
97 | FileType.AUDIO -> Glide.with(ivErrorIcon).load(R.drawable.ic_audiotrack)
98 | .into(ivErrorIcon)
99 | }
100 | }
101 |
102 | }
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/data/model/Media.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.data.model
2 |
3 | import android.os.Parcelable
4 | import com.github.file_picker.FileType
5 | import kotlinx.parcelize.Parcelize
6 | import java.io.File
7 |
8 | @Parcelize
9 | data class Media(
10 | val file: File,
11 | val type: FileType,
12 | var order: Int = 0,
13 | var isSelected: Boolean = false,
14 | val id: Int = file.path.hashCode()
15 | ) : Parcelable
16 |
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/data/repository/FilesPagingSource.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.data.repository
2 |
3 | import android.app.Application
4 | import androidx.paging.PagingSource
5 | import androidx.paging.PagingState
6 | import com.github.file_picker.FileType
7 | import com.github.file_picker.data.model.Media
8 | import com.github.file_picker.extension.getStorageFiles
9 |
10 | internal class FilesPagingSource(
11 | private val application: Application,
12 | private val fileType: FileType,
13 | ) : PagingSource() {
14 |
15 | // the initial load size for the first page may be different from the requested size
16 | private var initialLoadSize: Int = 0
17 |
18 | override fun getRefreshKey(state: PagingState): Int? =
19 | state.anchorPosition?.let { anchorPosition ->
20 | val anchorPage = state.closestPageToPosition(anchorPosition)
21 | anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
22 | }
23 |
24 | override suspend fun load(params: LoadParams): LoadResult {
25 | return try {
26 | // Start refresh at page 1 if undefined.
27 | val nextPageNumber = params.key ?: 1
28 |
29 | if (params.key == null) initialLoadSize = params.loadSize
30 |
31 | // work out the offset into the database to retrieve records from the page number,
32 | // allow for a different load size for the first page
33 | val offsetCalc = {
34 | if (nextPageNumber == 2)
35 | initialLoadSize
36 | else
37 | ((nextPageNumber - 1) * params.loadSize) + (initialLoadSize - params.loadSize)
38 | }
39 |
40 | val offset = offsetCalc.invoke()
41 |
42 | val files = application.getStorageFiles(
43 | fileType = fileType,
44 | limit = params.loadSize,
45 | offset = offset
46 | )
47 | val count = files.size
48 |
49 | LoadResult.Page(
50 | data = files,
51 | prevKey = null,
52 | nextKey = if (count < params.loadSize) null else nextPageNumber + 1
53 | )
54 | } catch (e: Exception) {
55 | LoadResult.Error(e)
56 | }
57 | }
58 |
59 | }
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/data/repository/FilesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.data.repository
2 |
3 | import android.app.Application
4 | import androidx.paging.Pager
5 | import androidx.paging.PagingConfig
6 | import com.github.file_picker.FileType
7 | import com.github.file_picker.PAGE_SIZE
8 |
9 | class FilesRepository(
10 | private val application: Application
11 | ) {
12 |
13 | fun getFiles(
14 | fileType: FileType = FileType.IMAGE,
15 | ) = Pager(
16 | PagingConfig(
17 | pageSize = PAGE_SIZE,
18 | enablePlaceholders = true,
19 | initialLoadSize = PAGE_SIZE,
20 | )
21 | ) {
22 | FilesPagingSource(
23 | application = application,
24 | fileType = fileType
25 | )
26 | }.flow
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/extension/ActivityExt.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.extension
2 |
3 | import androidx.annotation.FloatRange
4 | import androidx.appcompat.app.AppCompatActivity
5 | import com.github.file_picker.FilePicker
6 | import com.github.file_picker.FileType
7 | import com.github.file_picker.ListDirection
8 | import com.github.file_picker.listener.OnItemClickListener
9 | import com.github.file_picker.listener.OnSubmitClickListener
10 | import com.github.file_picker.data.model.Media
11 |
12 | /**
13 | * Show file picker
14 | *
15 | * @param title
16 | * @param titleTextColor
17 | * @param submitText
18 | * @param submitTextColor
19 | * @param accentColor
20 | * @param fileType
21 | * @param listDirection
22 | * @param cancellable
23 | * @param gridSpanCount
24 | * @param limitItemSelection
25 | * @param selectedFiles
26 | * @param onSubmitClickListener
27 | * @param onItemClickListener
28 | */
29 | fun AppCompatActivity.showFilePicker(
30 | title: String = FilePicker.DEFAULT_TITLE,
31 | titleTextColor: Int = FilePicker.DEFAULT_TITLE_TEXT_COLOR,
32 | submitText: String = FilePicker.DEFAULT_SUBMIT_TEXT,
33 | submitTextColor: Int = FilePicker.DEFAULT_SUBMIT_TEXT_COLOR,
34 | accentColor: Int = FilePicker.DEFAULT_ACCENT_COLOR,
35 | fileType: FileType = FilePicker.DEFAULT_FILE_TYPE,
36 | listDirection: ListDirection = FilePicker.DEFAULT_LIST_DIRECTION,
37 | cancellable: Boolean = FilePicker.DEFAULT_CANCELABLE,
38 | gridSpanCount: Int = FilePicker.DEFAULT_SPAN_COUNT,
39 | limitItemSelection: Int = FilePicker.DEFAULT_LIMIT_COUNT,
40 | @FloatRange(from = 0.0, to = 1.0)
41 | overlayAlpha: Float = FilePicker.DEFAULT_OVERLAY_ALPHA,
42 | selectedFiles: ArrayList = arrayListOf(),
43 | onSubmitClickListener: OnSubmitClickListener? = null,
44 | onItemClickListener: OnItemClickListener? = null,
45 | ) = FilePicker.Builder(this)
46 | .setTitle(title)
47 | .setTitleTextColor(titleTextColor)
48 | .setSubmitText(submitText)
49 | .setSubmitTextColor(submitTextColor)
50 | .setAccentColor(accentColor)
51 | .setFileType(fileType)
52 | .setListDirection(listDirection)
53 | .setCancellable(cancellable)
54 | .setGridSpanCount(gridSpanCount)
55 | .setLimitItemSelection(limitItemSelection)
56 | .setSelectedFiles(selectedFiles)
57 | .setOverlayAlpha(overlayAlpha)
58 | .setOnSubmitClickListener(onSubmitClickListener)
59 | .setOnItemClickListener(onItemClickListener)
60 | .buildAndShow()
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/extension/CollectionExt.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.extension
2 |
3 | /**
4 | * Is valid position in list
5 | *
6 | * @param position
7 | * @return
8 | */
9 | internal fun List<*>.isValidPosition(position: Int): Boolean {
10 | return if (isNotEmpty()) position in 0 until size else position >= 0
11 | }
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/extension/ContextExt.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.extension
2 |
3 | import android.content.ContentResolver
4 | import android.content.Context
5 | import android.content.pm.PackageManager
6 | import android.os.Build
7 | import android.os.Bundle
8 | import android.provider.MediaStore
9 | import androidx.core.app.ActivityCompat
10 | import com.github.file_picker.FileType
11 | import com.github.file_picker.PAGE_SIZE
12 | import com.github.file_picker.data.model.Media
13 | import java.io.File
14 |
15 | /**
16 | * check has runtime permission
17 | *
18 | * @param permission permission name ex: Manifest.permission.READ_EXTERNAL_STORAGE
19 | * @return if has permission return true otherwise false
20 | */
21 | internal fun Context.hasPermission(
22 | permission: String
23 | ): Boolean = ActivityCompat.checkSelfPermission(
24 | this,
25 | permission
26 | ) == PackageManager.PERMISSION_GRANTED
27 |
28 | /**
29 | * Get storage files path
30 | *
31 | * @return list of file path, ex: /storage/0/emulated/download/image.jpg
32 | */
33 | internal fun Context.getStorageFiles(
34 | fileType: FileType = FileType.IMAGE,
35 | limit: Int = PAGE_SIZE,
36 | offset: Int = 0
37 | ): List {
38 |
39 | val resolver = applicationContext.contentResolver
40 |
41 | val media = when (fileType) {
42 | FileType.VIDEO -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
43 | FileType.IMAGE -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
44 | FileType.AUDIO -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
45 | }
46 |
47 | val projection = arrayOf(MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns._ID)
48 | val modified = MediaStore.Files.FileColumns.DATE_MODIFIED
49 |
50 | val cursor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
51 | val sortArgs = arrayOf(modified)
52 | val bundle = Bundle().apply {
53 | putInt(ContentResolver.QUERY_ARG_LIMIT, limit)
54 | putInt(ContentResolver.QUERY_ARG_OFFSET, offset)
55 | putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, sortArgs)
56 | putInt(
57 | ContentResolver.QUERY_ARG_SORT_DIRECTION,
58 | ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
59 | )
60 | }
61 | resolver.query(
62 | media,
63 | projection,
64 | bundle,
65 | null
66 | )
67 | } else resolver.query(
68 | media,
69 | projection,
70 | null,
71 | null,
72 | "$modified DESC LIMIT $limit OFFSET $offset",
73 | )
74 |
75 | //Total number of images
76 | val count = cursor?.count ?: return emptyList()
77 |
78 | //Create an array to store path to all the images
79 | val files = arrayListOf()
80 |
81 | for (i in 0 until count) {
82 | cursor.moveToPosition(i)
83 | val dataColumnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
84 | //Store the path of the image
85 | val file = File(cursor.getString(dataColumnIndex))
86 | if (file.size > 0.0) {
87 | files.add(Media(file = file, type = fileType))
88 | }
89 | }
90 |
91 | // The cursor should be freed up after use with close()
92 | cursor.close()
93 | return files
94 | }
95 |
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/extension/FileExt.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.extension
2 |
3 | import android.graphics.Bitmap
4 | import android.graphics.BitmapFactory
5 | import android.media.MediaMetadataRetriever
6 | import java.io.File
7 |
8 | internal val File.size get() = if (!exists()) 0.0 else length().toDouble()
9 | internal val File.sizeInKb get() = size / 1024
10 | internal val File.sizeInMb get() = sizeInKb / 1024
11 | internal val File.sizeInGb get() = sizeInMb / 1024
12 | internal val File.sizeInTb get() = sizeInGb / 1024
13 |
14 | /**
15 | * Format file size
16 | *
17 | * @return string ex: 2.35 MB
18 | */
19 | internal fun File.size(): String = when {
20 | sizeInGb > 1024 -> "${sizeInTb.roundTo()} TB"
21 | sizeInMb > 1024 -> "${sizeInGb.roundTo()} GB"
22 | sizeInKb > 1024 -> "${sizeInMb.roundTo()} MB"
23 | size > 1024 -> "${sizeInKb.roundTo()} KB"
24 | else -> "${size.roundTo()} Bytes"
25 | }
26 |
27 | /**
28 | * Path name
29 | *
30 | * @return
31 | */
32 | internal fun File.lastPathTitle(): CharSequence {
33 | val paths = path.split("/")
34 | val titleIndex = paths.lastIndex - 1
35 | if (titleIndex >= 0) return paths[titleIndex]
36 | return ""
37 | }
38 |
39 | /**
40 | * Get music cover art
41 | *
42 | * @return
43 | */
44 | internal fun File.getMusicCoverArt(): Bitmap? = try {
45 | val mData = MediaMetadataRetriever()
46 | mData.setDataSource(path)
47 | val art = mData.embeddedPicture
48 | BitmapFactory.decodeByteArray(art, 0, art?.size ?: 0)
49 | } catch (e: Exception) {
50 | null
51 | }
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/extension/FragmentExt.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.extension
2 |
3 | import androidx.annotation.FloatRange
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.fragment.app.Fragment
6 | import com.github.file_picker.FilePicker
7 | import com.github.file_picker.FileType
8 | import com.github.file_picker.ListDirection
9 | import com.github.file_picker.data.model.Media
10 | import com.github.file_picker.listener.OnItemClickListener
11 | import com.github.file_picker.listener.OnSubmitClickListener
12 |
13 | /**
14 | * check has runtime permission
15 | *
16 | * @param permission permission name ex: Manifest.permission.READ_EXTERNAL_STORAGE
17 | * @return if has permission return true otherwise false
18 | */
19 | internal fun Fragment.hasPermission(
20 | permission: String
21 | ): Boolean = requireContext().hasPermission(permission)
22 |
23 | /**
24 | * Get storage files path
25 | *
26 | * @return list of file path, ex: /storage/0/emulated/download/image.jpg
27 | */
28 | internal fun Fragment.getStorageFiles(
29 | fileType: FileType = FileType.IMAGE
30 | ) = requireContext().getStorageFiles(fileType = fileType)
31 |
32 | /**
33 | * Show file picker
34 | *
35 | * @param title
36 | * @param titleTextColor
37 | * @param submitText
38 | * @param submitTextColor
39 | * @param accentColor
40 | * @param fileType
41 | * @param listDirection
42 | * @param cancellable
43 | * @param gridSpanCount
44 | * @param limitItemSelection
45 | * @param selectedFiles
46 | * @param onSubmitClickListener
47 | * @param onItemClickListener
48 | */
49 | fun Fragment.showFilePicker(
50 | title: String = FilePicker.DEFAULT_TITLE,
51 | titleTextColor: Int = FilePicker.DEFAULT_TITLE_TEXT_COLOR,
52 | submitText: String = FilePicker.DEFAULT_SUBMIT_TEXT,
53 | submitTextColor: Int = FilePicker.DEFAULT_SUBMIT_TEXT_COLOR,
54 | accentColor: Int = FilePicker.DEFAULT_ACCENT_COLOR,
55 | fileType: FileType = FilePicker.DEFAULT_FILE_TYPE,
56 | listDirection: ListDirection = FilePicker.DEFAULT_LIST_DIRECTION,
57 | cancellable: Boolean = FilePicker.DEFAULT_CANCELABLE,
58 | gridSpanCount: Int = FilePicker.DEFAULT_SPAN_COUNT,
59 | limitItemSelection: Int = FilePicker.DEFAULT_LIMIT_COUNT,
60 | selectedFiles: ArrayList = arrayListOf(),
61 | @FloatRange(from = 0.0, to = 1.0)
62 | overlayAlpha: Float = FilePicker.DEFAULT_OVERLAY_ALPHA,
63 | onSubmitClickListener: OnSubmitClickListener? = null,
64 | onItemClickListener: OnItemClickListener? = null,
65 | ) {
66 | if (requireActivity() !is AppCompatActivity) {
67 | throw IllegalAccessException("Fragment host must be extend AppCompatActivity")
68 | }
69 | (requireActivity() as AppCompatActivity).showFilePicker(
70 | title = title,
71 | titleTextColor = titleTextColor,
72 | submitText = submitText,
73 | submitTextColor = submitTextColor,
74 | accentColor = accentColor,
75 | fileType = fileType,
76 | listDirection = listDirection,
77 | cancellable = cancellable,
78 | gridSpanCount = gridSpanCount,
79 | limitItemSelection = limitItemSelection,
80 | selectedFiles = selectedFiles,
81 | overlayAlpha = overlayAlpha,
82 | onSubmitClickListener = onSubmitClickListener,
83 | onItemClickListener = onItemClickListener,
84 | )
85 | }
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/extension/NumberExt.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.extension
2 |
3 | import java.util.*
4 |
5 | /**
6 | * Round to
7 | *
8 | * @param numFractionDigits
9 | */
10 | internal fun Number.roundTo(
11 | numFractionDigits: Int = 2
12 | ) = "%.${numFractionDigits}f".format(this, Locale.ENGLISH)
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/extension/StringExt.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.extension
2 |
3 | /**
4 | * check path is video
5 | * ex : https://example.com/video.mp4
6 | * @return boolean
7 | */
8 | internal fun String?.isVideo(): Boolean {
9 |
10 | if (this == null) return false
11 |
12 | val formats = listOf(
13 | ".mp4",
14 | ".m4b",
15 | ".m4v",
16 | ".m4a",
17 | ".f4a",
18 | ".f4b",
19 | ".mov",
20 | ".3gp",
21 | ".3gp2",
22 | ".3g2",
23 | ".3gpp",
24 | ".3gpp2",
25 | ".wmv",
26 | ".wma",
27 | ".FLV",
28 | ".AVI"
29 | )
30 |
31 | val searched = formats.find {
32 | this.endsWith(it, ignoreCase = true)
33 | }
34 |
35 | return !searched.isNullOrBlank()
36 | }
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/listener/OnItemClickListener.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.listener
2 |
3 | import com.github.file_picker.adapter.FilePickerAdapter
4 | import com.github.file_picker.data.model.Media
5 |
6 | interface OnItemClickListener {
7 | fun onClick(media: Media, position: Int, adapter: FilePickerAdapter)
8 | }
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/listener/OnSubmitClickListener.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.listener
2 |
3 | import com.github.file_picker.data.model.Media
4 |
5 | interface OnSubmitClickListener {
6 | fun onClick(files: List)
7 | }
--------------------------------------------------------------------------------
/file-picker/src/main/java/com/github/file_picker/view/SquareCardView.kt:
--------------------------------------------------------------------------------
1 | package com.github.file_picker.view
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import com.google.android.material.card.MaterialCardView
6 |
7 | internal class SquareCardView : MaterialCardView {
8 |
9 | constructor(context: Context?) : super(context)
10 |
11 | constructor(context: Context?, attributes: AttributeSet) : super(
12 | context,
13 | attributes
14 | )
15 |
16 | constructor(context: Context?, attributes: AttributeSet, defStyleAttr: Int) : super(
17 | context,
18 | attributes,
19 | defStyleAttr
20 | )
21 |
22 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
23 | super.onMeasure(widthMeasureSpec, widthMeasureSpec)
24 | }
25 |
26 | }
--------------------------------------------------------------------------------
/file-picker/src/main/res/drawable/ic_audiotrack.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/file-picker/src/main/res/drawable/ic_check.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/file-picker/src/main/res/drawable/ic_image.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/file-picker/src/main/res/drawable/ic_play.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/file-picker/src/main/res/layout/file_picker.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
20 |
21 |
30 |
31 |
35 |
36 |
51 |
52 |
60 |
61 |
62 |
63 |
71 |
72 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/file-picker/src/main/res/layout/item_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
19 |
27 |
28 |
29 |
39 |
40 |
47 |
48 |
49 |
50 |
51 |
57 |
58 |
59 |
68 |
69 |
81 |
82 |
83 |
84 |
85 |
95 |
96 |
97 |
104 |
105 |
117 |
118 |
119 |
120 |
121 |
126 |
127 |
134 |
135 |
147 |
148 |
149 |
150 |
158 |
159 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
--------------------------------------------------------------------------------
/file-picker/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 8dp
5 |
--------------------------------------------------------------------------------
/file-picker/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
11 |
12 |
19 |
20 |
--------------------------------------------------------------------------------
/file-picker/src/test/java/ir/one_developer/file_picker/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package ir.one_developer.file_picker
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Feb 24 18:26:20 IRST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk11
3 | before_install:
4 | - ./scripts/prepareJitpackEnvironment.sh
--------------------------------------------------------------------------------
/screenshots/audio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/screenshots/audio.png
--------------------------------------------------------------------------------
/screenshots/image-2col-full.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/screenshots/image-2col-full.jpg
--------------------------------------------------------------------------------
/screenshots/image-3col.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/screenshots/image-3col.jpg
--------------------------------------------------------------------------------
/screenshots/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/screenshots/image.png
--------------------------------------------------------------------------------
/screenshots/video-2col-full.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/screenshots/video-2col-full.jpg
--------------------------------------------------------------------------------
/screenshots/video.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/screenshots/video.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | gradlePluginPortal()
12 | maven { url 'https://jitpack.io' }
13 | google()
14 | mavenCentral()
15 | }
16 | }
17 | rootProject.name = "FilePickerLibrary"
18 | include ':app'
19 | include ':file-picker'
20 |
--------------------------------------------------------------------------------