diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 61a9130..b589d56 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
new file mode 100644
index 0000000..0c0c338
--- /dev/null
+++ b/.idea/deploymentTargetDropDown.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index a5f05cd..e34606c 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -21,5 +21,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..2b8a50f
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index d5d35ec..dc5c3c6 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,16 @@
-
+
+
+
+
+
diff --git a/UnicornFilePicker/build.gradle b/UnicornFilePicker/build.gradle
index e97a0e1..eb23f65 100644
--- a/UnicornFilePicker/build.gradle
+++ b/UnicornFilePicker/build.gradle
@@ -1,10 +1,10 @@
plugins {
id 'com.android.library'
}
+apply plugin: 'kotlin-android'
android {
- compileSdkVersion 30
- buildToolsVersion "30.0.2"
+ compileSdk 33
viewBinding{
enabled true
}
@@ -13,12 +13,13 @@ android {
defaultConfig {
minSdkVersion 21
- targetSdkVersion 30
+ targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
+ vectorDrawables.useSupportLibrary = true
}
buildTypes {
@@ -28,9 +29,13 @@ android {
}
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
sourceSets {
main {
res {
@@ -42,10 +47,17 @@ android {
dependencies {
- implementation 'androidx.appcompat:appcompat:1.2.0'
- implementation 'com.google.android.material:material:1.2.1'
- implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
- testImplementation 'junit:junit:4.+'
- androidTestImplementation 'androidx.test.ext:junit:1.1.2'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.9.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.documentfile:documentfile:1.0.1'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ implementation "androidx.core:core-ktx:1.10.1"
+ //noinspection GradleDependency
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
+repositories {
+ mavenCentral()
}
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/AndroidManifest.xml b/UnicornFilePicker/src/main/AndroidManifest.xml
index 1a0707f..bf563a9 100644
--- a/UnicornFilePicker/src/main/AndroidManifest.xml
+++ b/UnicornFilePicker/src/main/AndroidManifest.xml
@@ -1,12 +1,15 @@
-
+
-
+
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/ConfigBuilder.java b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/ConfigBuilder.java
index 82deec3..d339b39 100644
--- a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/ConfigBuilder.java
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/ConfigBuilder.java
@@ -24,7 +24,7 @@ public final class ConfigBuilder {
private boolean showOnlyDir = false;
@StyleRes
- private int themeId = R.style.UnicornFilePicker_Default;
+ private int themeId = R.style.UnicornFilePicker_Default_AppTheme;
private final UnicornFilePicker unicornFilePicker;
private ArrayList extensionFilters;
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/adapters/StorageAdapter.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/adapters/StorageAdapter.kt
new file mode 100644
index 0000000..7184e65
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/adapters/StorageAdapter.kt
@@ -0,0 +1,53 @@
+package abhishekti7.unicorn.filepicker.adapters
+
+import abhishekti7.unicorn.filepicker.R
+import abhishekti7.unicorn.filepicker.storage.StorageDirectoryParcelable
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.ImageView
+
+import android.widget.TextView
+import java.lang.IllegalArgumentException
+
+
+class StorageAdapter(context: Context,private val items : List) : ArrayAdapter(context, R.layout.unicorn_storage_picker_item, items ) {
+ private var inflater : LayoutInflater? = null
+
+
+ override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View? {
+ return rowView(convertView, position)
+ }
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ return rowView(convertView, position) ?: throw IllegalArgumentException("Row view is null")
+ }
+
+ private fun rowView(convertView: View?, position: Int): View? {
+ val rowItem: StorageDirectoryParcelable? = getItem(position)
+ val holder: ViewHolder
+ var rowView : View? = convertView
+ if (rowView == null) {
+ holder = ViewHolder()
+ inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
+ rowView = inflater?.inflate(R.layout.unicorn_storage_picker_item, null)
+ rowView?.let{
+ holder.txtTitle = rowView.findViewById(R.id.title) as TextView
+ holder.icon = rowView.findViewById(R.id.icon) as ImageView
+ rowView.tag = holder
+ }
+ } else {
+ holder = rowView.tag as ViewHolder
+ }
+ holder.icon?.setImageResource(rowItem?.iconRes ?: -1)
+ holder.txtTitle?.text = rowItem?.name ?: ""
+ return rowView
+ }
+
+ private class ViewHolder {
+ var txtTitle: TextView? = null
+ var icon: ImageView? = null
+ }
+}
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/ExternalSdCardOperation.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/ExternalSdCardOperation.kt
new file mode 100644
index 0000000..462a3c1
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/ExternalSdCardOperation.kt
@@ -0,0 +1,172 @@
+package abhishekti7.unicorn.filepicker.filesystem
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.preference.PreferenceManager
+import android.util.Log
+import androidx.documentfile.provider.DocumentFile
+import java.io.File
+import java.io.IOException
+import java.util.ArrayList
+
+object ExternalSdCardOperation {
+ val LOG = "ExternalSdCardOperation"
+
+ /**
+ * Get a DocumentFile corresponding to the given file (for writing on ExtSdCard on Android 5). If
+ * the file is not existing, it is created.
+ *
+ * @param file The file.
+ * @param isDirectory flag indicating if the file should be a directory.
+ * @return The DocumentFile
+ */
+ @JvmStatic
+ fun getDocumentFile(
+ file: File,
+ isDirectory: Boolean,
+ context: Context
+ ): DocumentFile? {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) return DocumentFile.fromFile(file)
+ val baseFolder = getExtSdCardFolder(file, context)
+ var originalDirectory = false
+ if (baseFolder == null) {
+ return null
+ }
+ var relativePath: String? = null
+ try {
+ val fullPath = file.canonicalPath
+ if (baseFolder != fullPath) {
+ relativePath = fullPath.substring(baseFolder.length + 1)
+ } else {
+ originalDirectory = true
+ }
+ } catch (e: IOException) {
+ return null
+ }
+
+ val preferenceUri = PreferenceManager.getDefaultSharedPreferences(context)
+ .getString(PreferencesConstants.PREFERENCE_URI, null)
+ var treeUri: Uri? = null
+ if (preferenceUri != null) {
+ treeUri = Uri.parse(preferenceUri)
+ }
+ if (treeUri == null) {
+ return null
+ }
+
+ // start with root of SD card and then parse through document tree.
+ var document = DocumentFile.fromTreeUri(context, treeUri)
+ if (originalDirectory || relativePath == null) {
+ return document
+ }
+
+ val parts = relativePath.split("/").toTypedArray()
+ for (i in parts.indices) {
+ if (document == null) {
+ return null
+ }
+
+ var nextDocument = document.findFile(parts[i])
+ if (nextDocument == null) {
+ nextDocument = if (i < parts.size - 1 || isDirectory) {
+ document.createDirectory(parts[i])
+ } else {
+ document.createFile("image", parts[i])
+ }
+ }
+ document = nextDocument
+ }
+
+ return document
+ }
+
+ /**
+ * Get a list of external SD card paths. (Kitkat or higher.)
+ *
+ * @return A list of external SD card paths.
+ */
+ @JvmStatic
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ private fun getExtSdCardPaths(context: Context): Array {
+ val paths: MutableList = ArrayList()
+ for (file in context.getExternalFilesDirs("external")) {
+ if (file != null && file != context.getExternalFilesDir("external")) {
+ val index = file.absolutePath.lastIndexOf("/Android/data")
+ if (index < 0) {
+ Log.w(LOG, "Unexpected external file dir: " + file.absolutePath)
+ } else {
+ var path = file.absolutePath.substring(0, index)
+ try {
+ path = File(path).canonicalPath
+ } catch (e: IOException) {
+ // Keep non-canonical path.
+ }
+ paths.add(path)
+ }
+ }
+ }
+ if (paths.isEmpty()) paths.add("/storage/sdcard1")
+ return paths.toTypedArray()
+ }
+
+ @JvmStatic
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ fun getExtSdCardPathsForActivity(context: Context): Array {
+ val paths: MutableList = ArrayList()
+ for (file in context.getExternalFilesDirs("external")) {
+ if (file != null) {
+ val index = file.absolutePath.lastIndexOf("/Android/data")
+ if (index < 0) {
+ Log.w(LOG, "Unexpected external file dir: " + file.absolutePath)
+ } else {
+ var path = file.absolutePath.substring(0, index)
+ try {
+ path = File(path).canonicalPath
+ } catch (e: IOException) {
+ // Keep non-canonical path.
+ }
+ paths.add(path)
+ }
+ }
+ }
+ if (paths.isEmpty()) paths.add("/storage/sdcard1")
+ return paths.toTypedArray()
+ }
+
+ /**
+ * Determine the main folder of the external SD card containing the given file.
+ *
+ * @param file the file.
+ * @return The main folder of the external SD card containing this file, if the file is on an SD
+ * card. Otherwise, null is returned.
+ */
+ @JvmStatic
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ public fun getExtSdCardFolder(file: File, context: Context): String? {
+ val extSdPaths = getExtSdCardPaths(context)
+ try {
+ for (i in extSdPaths.indices) {
+ if (file.canonicalPath.startsWith(extSdPaths[i])) {
+ return extSdPaths[i]
+ }
+ }
+ } catch (e: IOException) {
+ return null
+ }
+ return null
+ }
+
+ /**
+ * Determine if a file is on external sd card. (Kitkat or higher.)
+ *
+ * @param file The file.
+ * @return true if on external sd card.
+ */
+ @JvmStatic
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ fun isOnExtSdCard(file: File, c: Context): Boolean {
+ return getExtSdCardFolder(file, c) != null
+ }
+}
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/FileUtils.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/FileUtils.kt
new file mode 100644
index 0000000..04a36fa
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/FileUtils.kt
@@ -0,0 +1,12 @@
+package abhishekti7.unicorn.filepicker.filesystem
+
+import java.io.File
+/** Functions that deal with files */
+object FileUtils {
+
+ fun canListFiles(f: File): Boolean {
+ return f.canRead() && f.isDirectory
+ }
+
+
+}
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/OTGUtil.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/OTGUtil.kt
new file mode 100644
index 0000000..f1292aa
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/OTGUtil.kt
@@ -0,0 +1,19 @@
+package abhishekti7.unicorn.filepicker.filesystem
+
+import android.content.Context
+import android.hardware.usb.UsbConstants
+import android.hardware.usb.UsbManager
+import android.os.Build
+import android.provider.DocumentsContract
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.documentfile.provider.DocumentFile
+
+object OTGUtil {
+
+ private val TAG = OTGUtil::class.java.simpleName
+ const val PREFIX_OTG = "otg:/"
+ const val PREFIX_MEDIA_REMOVABLE = "/mnt/media_rw"
+
+
+}
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/PreferencesConstants.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/PreferencesConstants.kt
new file mode 100644
index 0000000..df05d8e
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/PreferencesConstants.kt
@@ -0,0 +1,87 @@
+package abhishekti7.unicorn.filepicker.filesystem
+
+object PreferencesConstants {
+ // START fragments
+ const val FRAGMENT_THEME = "theme"
+ const val FRAGMENT_COLORS = "colors"
+ const val FRAGMENT_FOLDERS = "sidebar_folders"
+ const val FRAGMENT_QUICKACCESSES = "sidebar_quickaccess"
+ const val FRAGMENT_ADVANCED_SEARCH = "advancedsearch"
+ const val FRAGMENT_ABOUT = "about"
+ const val FRAGMENT_FEEDBACK = "feedback"
+
+ // END fragments
+ // START preferences.xml constants
+ const val PREFERENCE_INTELLI_HIDE_TOOLBAR = "intelliHideToolbar"
+ const val PREFERENCE_SHOW_FILE_SIZE = "showFileSize"
+ const val PREFERENCE_SHOW_PERMISSIONS = "showPermissions"
+ const val PREFERENCE_SHOW_DIVIDERS = "showDividers"
+ const val PREFERENCE_SHOW_HEADERS = "showHeaders"
+ const val PREFERENCE_SHOW_GOBACK_BUTTON = "goBack_checkbox"
+ const val PREFERENCE_SHOW_SIDEBAR_FOLDERS = "sidebar_folders_enable"
+ const val PREFERENCE_SHOW_SIDEBAR_QUICKACCESSES = "sidebar_quickaccess_enable"
+ const val PREFERENCE_ENABLE_MARQUEE_FILENAME = "enableMarqueeFilename"
+ const val PREFERENCE_ROOT_LEGACY_LISTING = "legacyListing"
+ const val PREFERENCE_DRAG_AND_DROP_PREFERENCE = "dragAndDropPreference"
+ const val PREFERENCE_DRAG_AND_DROP_REMEMBERED = "dragOperationRemembered"
+ const val PREFERENCE_CLEAR_OPEN_FILE = "clear_open_file"
+ const val PREFERENCE_BOOKMARKS_ADDED = "books_added"
+ const val PREFERENCE_TEXTEDITOR_NEWSTACK = "texteditor_newstack"
+ const val PREFERENCE_SHOW_HIDDENFILES = "showHidden"
+ const val PREFERENCE_SHOW_LAST_MODIFIED = "showLastModified"
+ const val PREFERENCE_USE_CIRCULAR_IMAGES = "circularimages"
+ const val PREFERENCE_ROOTMODE = "rootmode"
+ const val PREFERENCE_CHANGEPATHS = "typeablepaths"
+ const val PREFERENCE_GRID_COLUMNS = "columns"
+ const val PREFERENCE_SHOW_THUMB = "showThumbs"
+ const val PREFERENCE_CRYPT_MASTER_PASSWORD = "crypt_password"
+ const val PREFERENCE_CRYPT_FINGERPRINT = "crypt_fingerprint"
+ const val PREFERENCE_CRYPT_WARNING_REMEMBER = "crypt_remember"
+ const val ENCRYPT_PASSWORD_FINGERPRINT = "fingerprint"
+ const val ENCRYPT_PASSWORD_MASTER = "master"
+ const val PREFERENCE_CRYPT_MASTER_PASSWORD_DEFAULT = ""
+ const val PREFERENCE_CRYPT_FINGERPRINT_DEFAULT = false
+ const val PREFERENCE_CRYPT_WARNING_REMEMBER_DEFAULT = false
+ const val PREFERENCE_ZIP_EXTRACT_PATH = "extractpath"
+
+ // END preferences.xml constants
+ // START color_prefs.xml constants
+ const val PREFERENCE_SKIN = "skin"
+ const val PREFERENCE_SKIN_TWO = "skin_two"
+ const val PREFERENCE_ACCENT = "accent_skin"
+ const val PREFERENCE_ICON_SKIN = "icon_skin"
+ const val PREFERENCE_CURRENT_TAB = "current_tab"
+ const val PREFERENCE_COLORIZE_ICONS = "coloriseIcons"
+ const val PREFERENCE_COLORED_NAVIGATION = "colorednavigation"
+ const val PREFERENCE_RANDOM_COLOR = "random_checkbox"
+
+ // END color_prefs.xml constants
+ // START folders_prefs.xml constants
+ const val PREFERENCE_SHORTCUT = "add_shortcut"
+
+ // END folders_prefs.xml constants
+ // START random preferences
+ const val PREFERENCE_DIRECTORY_SORT_MODE = "dirontop"
+ const val PREFERENCE_DRAWER_HEADER_PATH = "drawer_header_path"
+ const val PREFERENCE_URI = "URI"
+ const val PREFERENCE_HIDEMODE = "hidemode"
+ const val PREFERENCE_VIEW = "view"
+ const val PREFERENCE_NEED_TO_SET_HOME = "needtosethome"
+
+ /** The value is an int with values RANDOM_INDEX, CUSTOM_INDEX, NO_DATA or [0, ...] */
+ const val PREFERENCE_COLOR_CONFIG = "color config"
+
+ // END random preferences
+ // START sort preferences
+ const val PREFERENCE_SORTBY_ONLY_THIS = "sortby_only_this"
+ const val PREFERENCE_APPLIST_SORTBY = "AppsListFragment.sortBy"
+ const val PREFERENCE_APPLIST_ISASCENDING = "AppsListFragment.isAscending"
+
+ // END sort preferences
+ // drag and drop preferences
+ const val PREFERENCE_DRAG_DEFAULT = 0
+ const val PREFERENCE_DRAG_TO_SELECT = 1
+ const val PREFERENCE_DRAG_TO_MOVE_COPY = 2
+ const val PREFERENCE_DRAG_REMEMBER_COPY = "copy"
+ const val PREFERENCE_DRAG_REMEMBER_MOVE = "move"
+}
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/SingletonUsbOtg.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/SingletonUsbOtg.kt
new file mode 100644
index 0000000..0b07929
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/SingletonUsbOtg.kt
@@ -0,0 +1,41 @@
+package abhishekti7.unicorn.filepicker.filesystem
+
+import android.net.Uri
+
+class SingletonUsbOtg private constructor() {
+ private var connectedDevice: UsbOtgRepresentation? = null
+ private var usbOtgRoot: Uri? = null
+ fun setConnectedDevice(connectedDevice: UsbOtgRepresentation?) {
+ this.connectedDevice = connectedDevice
+ }
+
+ val isDeviceConnected: Boolean
+ get() = connectedDevice != null
+
+ fun setUsbOtgRoot(root: Uri?) {
+ checkNotNull(connectedDevice) { "No device connected!" }
+ usbOtgRoot = root
+ }
+
+ fun resetUsbOtgRoot() {
+ connectedDevice = null
+ usbOtgRoot = null
+ }
+
+ fun getUsbOtgRoot(): Uri? {
+ return usbOtgRoot
+ }
+
+ fun checkIfRootIsFromDevice(device: UsbOtgRepresentation): Boolean {
+ return usbOtgRoot != null && connectedDevice.hashCode() === device.hashCode()
+ }
+
+ companion object {
+ var instance: SingletonUsbOtg? = null
+ get() {
+ if (field == null) field = SingletonUsbOtg()
+ return field
+ }
+ private set
+ }
+}
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/UsbOtgRepresentation.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/UsbOtgRepresentation.kt
new file mode 100644
index 0000000..d0c3074
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/filesystem/UsbOtgRepresentation.kt
@@ -0,0 +1,21 @@
+package abhishekti7.unicorn.filepicker.filesystem
+
+class UsbOtgRepresentation(val productID: Int, val vendorID: Int, val serialNumber: String?) {
+ /**
+ * This does not ensure a device is equal to another! This tests parameters to know to a certain
+ * degree of certanty that a device is "similar enough" to another one to be the same one.
+ */
+ override fun equals(obj: Any?): Boolean {
+ if (obj !is UsbOtgRepresentation) return false
+ val other = obj
+ return productID == other.productID && vendorID == other.vendorID && (serialNumber == null && other.serialNumber == null
+ || serialNumber == other.serialNumber)
+ }
+
+ override fun hashCode(): Int {
+ var result = productID
+ result = 37 * result + vendorID
+ result = 37 * result + (serialNumber?.hashCode() ?: 0)
+ return result
+ }
+}
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/storage/StorageDirectoryParcelable.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/storage/StorageDirectoryParcelable.kt
new file mode 100644
index 0000000..8da6054
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/storage/StorageDirectoryParcelable.kt
@@ -0,0 +1,51 @@
+package abhishekti7.unicorn.filepicker.storage
+
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.annotation.DrawableRes
+
+/** Identifies a mounted volume */
+class StorageDirectoryParcelable : Parcelable {
+ val path: String
+ val name: String
+
+ @DrawableRes
+ val iconRes: Int
+
+ constructor(path: String, name: String, iconRes: Int) {
+ this.path = path
+ this.name = name
+ this.iconRes = iconRes
+ }
+
+ constructor(im: Parcel) {
+ path = im.readString()!!
+ name = im.readString()!!
+ iconRes = im.readInt()
+ }
+
+ override fun toString(): String {
+ return "StorageDirectory(path=$path, name=$name, icon=$iconRes)"
+ }
+
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ override fun writeToParcel(parcel: Parcel, i: Int) {
+ parcel.writeString(path)
+ parcel.writeString(name)
+ parcel.writeInt(iconRes)
+ }
+
+
+ companion object CREATOR : Parcelable.Creator {
+ override fun createFromParcel(parcel: Parcel): StorageDirectoryParcelable {
+ return StorageDirectoryParcelable(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+}
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/storage/StorageNaming.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/storage/StorageNaming.kt
new file mode 100644
index 0000000..70bf7ae
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/storage/StorageNaming.kt
@@ -0,0 +1,26 @@
+package abhishekti7.unicorn.filepicker.storage
+
+import androidx.annotation.IntDef
+import java.io.File
+
+object StorageNaming {
+ const val STORAGE_INTERNAL = 0
+ const val STORAGE_SD_CARD = 1
+ const val ROOT = 2
+ const val NOT_KNOWN = 3
+
+ /** Retrofit of [android.os.storage.StorageVolume.getDescription] to older apis */
+ @DeviceDescription
+ fun getDeviceDescriptionLegacy(file: File): Int {
+ val path = file.path
+ return when (path) {
+ "/storage/emulated/legacy", "/storage/emulated/0", "/mnt/sdcard" -> STORAGE_INTERNAL
+ "/storage/sdcard", "/storage/sdcard1" -> STORAGE_SD_CARD
+ "/" -> ROOT
+ else -> NOT_KNOWN
+ }
+ }
+
+ @IntDef(STORAGE_INTERNAL, STORAGE_SD_CARD, ROOT, NOT_KNOWN)
+ annotation class DeviceDescription
+}
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/storage/StorageNamingHelper.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/storage/StorageNamingHelper.kt
new file mode 100644
index 0000000..ce0276d
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/storage/StorageNamingHelper.kt
@@ -0,0 +1,21 @@
+package abhishekti7.unicorn.filepicker.storage
+
+import abhishekti7.unicorn.filepicker.R
+import android.content.Context
+import java.io.File
+
+object StorageNamingHelper {
+ fun getNameForDeviceDescription(
+ context: Context,
+ file: File,
+ @StorageNaming.DeviceDescription deviceDescription: Int
+ ): String {
+ return when (deviceDescription) {
+ StorageNaming.STORAGE_INTERNAL -> context.getString(R.string.unicorn_storage_internal)
+ StorageNaming.STORAGE_SD_CARD -> context.getString(R.string.unicorn_storage_sd_card)
+ StorageNaming.ROOT -> context.getString(R.string.unicorn_root_directory)
+ StorageNaming.NOT_KNOWN -> file.name
+ else -> file.name
+ }
+ }
+}
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/storage/StorageUtils.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/storage/StorageUtils.kt
new file mode 100644
index 0000000..91ff37a
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/storage/StorageUtils.kt
@@ -0,0 +1,220 @@
+package abhishekti7.unicorn.filepicker.storage
+
+import abhishekti7.unicorn.filepicker.R
+import abhishekti7.unicorn.filepicker.filesystem.ExternalSdCardOperation
+import abhishekti7.unicorn.filepicker.filesystem.FileUtils
+import abhishekti7.unicorn.filepicker.filesystem.OTGUtil
+import abhishekti7.unicorn.filepicker.filesystem.SingletonUsbOtg
+import abhishekti7.unicorn.filepicker.utils.Utils
+import android.Manifest
+import android.annotation.TargetApi
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build.VERSION
+import android.os.Build.VERSION_CODES
+import android.os.Environment
+import android.os.storage.StorageManager
+import android.os.storage.StorageVolume
+import android.text.TextUtils
+import android.util.Log
+import androidx.annotation.DrawableRes
+import androidx.core.app.ActivityCompat
+import java.io.*
+import java.lang.Exception
+import java.lang.NumberFormatException
+import java.util.*
+import java.util.regex.Pattern
+
+object StorageUtils {
+ private const val TAG = "StorageUtils"//flags//mount point
+ private const val INTERNAL_SHARED_STORAGE = "Internal shared storage"
+ private const val DEFAULT_FALLBACK_STORAGE_PATH = "/storage/sdcard0"
+ val DIR_SEPARATOR = Pattern.compile("/")
+ /** @return paths to all available volumes in the system (include emulated)
+ */
+ @JvmStatic
+ @Synchronized
+ fun getStorageDirectories(context : Context): ArrayList? {
+ val volumes: ArrayList
+ volumes = if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ getStorageDirectoriesNew(context)
+ } else {
+ getStorageDirectoriesLegacy(context)
+ }
+ if (isRootExplorer()) {
+ volumes.add(
+ StorageDirectoryParcelable(
+ "/",
+ context.getResources().getString(R.string.unicorn_root_directory),
+ -1
+ )
+ )
+ }
+ return volumes
+ }
+
+ private fun isRootExplorer(): Boolean {
+ return false
+ }
+
+ /**
+ * @return All available storage volumes (including internal storage, SD-Cards and USB devices)
+ */
+ @TargetApi(VERSION_CODES.N)
+ @Synchronized
+ fun getStorageDirectoriesNew(context : Context): ArrayList {
+ // Final set of paths
+ val volumes: ArrayList = ArrayList()
+ val sm: StorageManager = context.getSystemService(StorageManager::class.java)
+ for (volume in sm.storageVolumes) {
+ if (!volume.state.equals(Environment.MEDIA_MOUNTED, ignoreCase = true)
+ && !volume.state.equals(Environment.MEDIA_MOUNTED_READ_ONLY, ignoreCase = true)
+ ) {
+ continue
+ }
+ val path: File? = Utils.getVolumeDirectory(volume)
+ var name = volume.getDescription(context)
+ if (INTERNAL_SHARED_STORAGE.equals(name, ignoreCase = true)) {
+ name = context.getString(R.string.unicorn_storage_internal)
+ }
+ var icon: Int = -1
+ if (!volume.isRemovable) {
+ icon = R.drawable.ic_baseline_smartphone_24
+ } else {
+ // HACK: There is no reliable way to distinguish USB and SD external storage
+ // However it is often enough to check for "USB" String
+ if (name.uppercase().contains("USB") || path?.path?.uppercase()?.contains("USB") == true) {
+ icon = R.drawable.ic_baseline_usb_24
+ } else {
+ icon = R.drawable.ic_baseline_sd_storage_24
+ }
+ }
+ volumes.add(StorageDirectoryParcelable(path?.path ?: "", name, icon))
+ }
+ return volumes
+ }
+
+ /**
+ * Returns all available SD-Cards in the system (include emulated)
+ *
+ *
+ * Warning: Hack! Based on Android source code of version 4.3 (API 18) Because there was no
+ * standard way to get it before android N
+ *
+ * @return All available SD-Cards in the system (include emulated)
+ */
+ @Synchronized
+ fun getStorageDirectoriesLegacy(context : Context): ArrayList {
+ val rv: MutableList = ArrayList()
+
+ // Primary physical SD-CARD (not emulated)
+ val rawExternalStorage = System.getenv("EXTERNAL_STORAGE")
+ // All Secondary SD-CARDs (all exclude primary) separated by ":"
+ val rawSecondaryStoragesStr = System.getenv("SECONDARY_STORAGE")
+ // Primary emulated SD-CARD
+ val rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET")
+ if (TextUtils.isEmpty(rawEmulatedStorageTarget)) {
+ // Device has physical external storage; use plain paths.
+ if (TextUtils.isEmpty(rawExternalStorage)) {
+ // EXTERNAL_STORAGE undefined; falling back to default.
+ // Check for actual existence of the directory before adding to list
+ if (File(DEFAULT_FALLBACK_STORAGE_PATH).exists()) {
+ rv.add(DEFAULT_FALLBACK_STORAGE_PATH)
+ } else {
+ // We know nothing else, use Environment's fallback
+ rv.add(Environment.getExternalStorageDirectory().absolutePath)
+ }
+ } else {
+ rv.add(rawExternalStorage)
+ }
+ } else {
+ // Device has emulated storage; external storage paths should have
+ // userId burned into them.
+ val rawUserId: String
+ if (VERSION.SDK_INT < VERSION_CODES.JELLY_BEAN_MR1) {
+ rawUserId = ""
+ } else {
+ val path = Environment.getExternalStorageDirectory().absolutePath
+ val folders: Array = DIR_SEPARATOR.split(path)
+ val lastFolder = folders[folders.size - 1]
+ var isDigit = false
+ try {
+ Integer.valueOf(lastFolder)
+ isDigit = true
+ } catch (ignored: NumberFormatException) {
+ }
+ rawUserId = if (isDigit) lastFolder else ""
+ }
+ // /storage/emulated/0[1,2,...]
+ if (TextUtils.isEmpty(rawUserId)) {
+ rv.add(rawEmulatedStorageTarget)
+ } else {
+ rv.add(rawEmulatedStorageTarget + File.separator + rawUserId)
+ }
+ }
+ // Add all secondary storages
+ if (!TextUtils.isEmpty(rawSecondaryStoragesStr)) {
+ // All Secondary SD-CARDs splited into array
+ val rawSecondaryStorages =
+ rawSecondaryStoragesStr.split(File.pathSeparator).toTypedArray()
+ Collections.addAll(rv, *rawSecondaryStorages)
+ }
+ if (VERSION.SDK_INT >= VERSION_CODES.M && checkStoragePermission(context)) rv.clear()
+ if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
+ val strings: Array = ExternalSdCardOperation.getExtSdCardPathsForActivity(context)
+ for (s in strings) {
+ val f = File(s)
+ if (!rv.contains(s) && FileUtils.canListFiles(f)) rv.add(s)
+ }
+ }
+ val usb: File? = getUsbDrive()
+ if (usb != null && !rv.contains(usb.path)) rv.add(usb.path)
+ if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
+ if (SingletonUsbOtg.instance?.isDeviceConnected == true) {
+ rv.add(OTGUtil.PREFIX_OTG.toString() + "/")
+ }
+ }
+
+ // Assign a label and icon to each directory
+ val volumes: ArrayList = ArrayList()
+ for (file in rv) {
+ val f = File(file)
+ @DrawableRes var icon: Int = -1
+ if ("/storage/emulated/legacy" == file || "/storage/emulated/0" == file || "/mnt/sdcard" == file) {
+ icon = R.drawable.ic_baseline_smartphone_24
+ } else if ("/storage/sdcard1" == file) {
+ icon = R.drawable.ic_baseline_sd_storage_24
+ } else if ("/" == file) {
+ // icon = R.drawable.ic_drawer_root_white
+ } else {
+ icon = R.drawable.ic_baseline_sd_storage_24
+ }
+ @StorageNaming.DeviceDescription val deviceDescription: Int =
+ StorageNaming.getDeviceDescriptionLegacy(f)
+ val name: String =
+ StorageNamingHelper.getNameForDeviceDescription(context , f, deviceDescription)
+ volumes.add(StorageDirectoryParcelable(file, name, icon))
+ }
+ return volumes
+ }
+ fun checkStoragePermission(context: Context): Boolean {
+ // Verify that all required contact permissions have been granted.
+ return (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED)
+ }
+
+ fun getUsbDrive(): File? {
+ var parent = File("/storage")
+ try {
+ for (f in parent.listFiles()) if (f.exists() && f.name.toLowerCase()
+ .contains("usb") && f.canExecute()
+ ) return f
+ } catch (e: Exception) {
+ }
+ parent = File("/mnt/sdcard/usbStorage")
+ if (parent.exists() && parent.canExecute()) return parent
+ parent = File("/mnt/sdcard/usb_storage")
+ return if (parent.exists() && parent.canExecute()) parent else null
+ }
+
+}
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/ui/FilePickerActivity.java b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/ui/FilePickerActivity.java
deleted file mode 100644
index 5dbe0be..0000000
--- a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/ui/FilePickerActivity.java
+++ /dev/null
@@ -1,316 +0,0 @@
-package abhishekti7.unicorn.filepicker.ui;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.SearchView;
-import androidx.core.content.ContextCompat;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.os.Environment;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.inputmethod.EditorInfo;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-
-import abhishekti7.unicorn.filepicker.R;
-import abhishekti7.unicorn.filepicker.adapters.DirectoryAdapter;
-import abhishekti7.unicorn.filepicker.adapters.DirectoryStackAdapter;
-import abhishekti7.unicorn.filepicker.databinding.UnicornActivityFilePickerBinding;
-import abhishekti7.unicorn.filepicker.models.Config;
-import abhishekti7.unicorn.filepicker.models.DirectoryModel;
-import abhishekti7.unicorn.filepicker.utils.UnicornSimpleItemDecoration;
-
-/**
- * Created by Abhishek Tiwari on 06-01-2021.
- */
-
-public class FilePickerActivity extends AppCompatActivity {
-
- private static final String TAG = "FilePickerActivity";
- private UnicornActivityFilePickerBinding filePickerBinding;
-
- private File root_dir;
- private ArrayList selected_files;
- private ArrayList arr_dir_stack;
- private ArrayList arr_files;
-
- private DirectoryStackAdapter stackAdapter;
- private DirectoryAdapter directoryAdapter;
-
- private final String[] REQUIRED_PERMISSIONS = new String[]{
- "android.permission.WRITE_EXTERNAL_STORAGE",
- "android.permission.READ_EXTERNAL_STORAGE",
- };
-
- private Config config;
- private ArrayList filters;
-
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- config = Config.getInstance();
- setTheme(config.getThemeId());
- filePickerBinding = UnicornActivityFilePickerBinding.inflate(getLayoutInflater());
- View view = filePickerBinding.getRoot();
- setContentView(view);
-
- initConfig();
- }
-
- private void initConfig() {
- filters = config.getExtensionFilters();
-
-
- setSupportActionBar(filePickerBinding.toolbar);
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
- getSupportActionBar().setDisplayShowTitleEnabled(false);
-
- if (config.getRootDir() != null) {
- root_dir = new File(config.getRootDir());
- } else {
- root_dir = Environment.getExternalStorageDirectory();
- }
- selected_files = new ArrayList<>();
- arr_dir_stack = new ArrayList<>();
- arr_files = new ArrayList<>();
-
- setUpDirectoryStackView();
- setUpFilesView();
-
- if (allPermissionsGranted()) {
- fetchDirectory(new DirectoryModel(
- true,
- root_dir.getAbsolutePath(),
- root_dir.getName(),
- root_dir.lastModified(),
- root_dir.listFiles() == null ? 0 : root_dir.listFiles().length
- ));
- } else {
- Log.e(TAG, "Storage permissions not granted. You have to implement it before starting the file picker");
- finish();
- }
-
- filePickerBinding.fabSelect.setOnClickListener((v)->{
- Intent intent = new Intent();
- if(config.showOnlyDirectory()){
- selected_files.clear();
- selected_files.add(arr_dir_stack.get(arr_dir_stack.size()-1).getPath());
- }
- intent.putStringArrayListExtra("filePaths", selected_files);
- setResult(config.getReqCode(), intent);
- setResult(RESULT_OK, intent);
- finish();
- });
-
- TypedValue typedValue = new TypedValue();
- Resources.Theme theme = getTheme();
- theme.resolveAttribute(R.attr.unicorn_fabColor, typedValue, true);
- if(typedValue.data!=0){
- filePickerBinding.fabSelect.setBackgroundTintList(ColorStateList.valueOf(typedValue.data));
- }else{
- filePickerBinding.fabSelect.setBackgroundTintList(ColorStateList.valueOf(getResources().getColor(R.color.unicorn_colorAccent)));
- }
-
- }
-
- private void setUpFilesView() {
- LinearLayoutManager layoutManager = new LinearLayoutManager(FilePickerActivity.this);
- filePickerBinding.rvFiles.setLayoutManager(layoutManager);
- directoryAdapter = new DirectoryAdapter(FilePickerActivity.this, arr_files, false, new DirectoryAdapter.onFilesClickListener() {
- @Override
- public void onClicked(DirectoryModel model) {
- fetchDirectory(model);
- }
-
- @Override
- public void onFileSelected(DirectoryModel fileModel) {
- if(config.isSelectMultiple()){
- if(selected_files.contains(fileModel.getPath())){
- selected_files.remove(fileModel.getPath());
- }else{
- selected_files.add(fileModel.getPath());
- }
- }else{
- selected_files.clear();
- selected_files.add(fileModel.getPath());
- }
- }
- });
- filePickerBinding.rvFiles.setAdapter(directoryAdapter);
- directoryAdapter.notifyDataSetChanged();
- if(config.addItemDivider()){
- filePickerBinding.rvFiles.addItemDecoration(new UnicornSimpleItemDecoration(FilePickerActivity.this));
- }
- }
-
- private void setUpDirectoryStackView() {
- LinearLayoutManager layoutManager = new LinearLayoutManager(FilePickerActivity.this, RecyclerView.HORIZONTAL, false);
- filePickerBinding.rvDirPath.setLayoutManager(layoutManager);
- stackAdapter = new DirectoryStackAdapter(FilePickerActivity.this, arr_dir_stack, model -> {
- Log.e(TAG, model.toString());
- arr_dir_stack = new ArrayList<>(arr_dir_stack.subList(0, arr_dir_stack.indexOf(model) + 1));
- setUpDirectoryStackView();
- fetchDirectory(arr_dir_stack.remove(arr_dir_stack.size() - 1));
- });
-
- filePickerBinding.rvDirPath.setAdapter(stackAdapter);
- stackAdapter.notifyDataSetChanged();
- }
-
- /**
- * Fetches list of files in a folder and filters files if filter present
- */
- private void fetchDirectory(DirectoryModel model) {
- filePickerBinding.rlProgress.setVisibility(View.VISIBLE);
- selected_files.clear();
-
- arr_files.clear();
- File dir = new File(model.getPath());
- File[] files_list = dir.listFiles();
- if (files_list != null) {
- for (File file : files_list) {
- DirectoryModel directoryModel = new DirectoryModel();
- directoryModel.setDirectory(file.isDirectory());
- directoryModel.setName(file.getName());
- directoryModel.setPath(file.getAbsolutePath());
- directoryModel.setLast_modif_time(file.lastModified());
-
- if (config.showHidden() || (!config.showHidden() && !file.isHidden())) {
- if (file.isDirectory()) {
- if (file.listFiles() != null)
- directoryModel.setNum_files(file.listFiles().length);
- arr_files.add(directoryModel);
- } else {
- if(!config.showOnlyDirectory()){
- // Filter out files if filters specified
- if(filters!=null){
- try {
- // Extract the file extension
- String fileName = file.getName();
- String extension = fileName.substring(fileName.lastIndexOf("."));
- for (String filter : filters) {
- if (extension.toLowerCase().contains(filter)) {
- arr_files.add(directoryModel);
- }
- }
- } catch (Exception e) {
-// Log.e(TAG, "Encountered a file without an extension: ", e);
- }
- }else{
- arr_files.add(directoryModel);
- }
- }
- }
- }
-
- }
- Collections.sort(arr_files, new CustomFileComparator());
-
- arr_dir_stack.add(model);
- filePickerBinding.rvDirPath.scrollToPosition(arr_dir_stack.size() - 1);
- filePickerBinding.toolbar.setTitle(model.getName());
- }
- if (arr_files.size() == 0) {
- filePickerBinding.rlNoFiles.setVisibility(View.VISIBLE);
- } else {
- filePickerBinding.rlNoFiles.setVisibility(View.GONE);
- }
- filePickerBinding.rlProgress.setVisibility(View.GONE);
- stackAdapter.notifyDataSetChanged();
- directoryAdapter.notifyDataSetChanged();
- }
-
- // Custom Comparator to sort the list of files in lexicographical order
- public static class CustomFileComparator implements Comparator {
- @Override
- public int compare(DirectoryModel o1, DirectoryModel o2) {
- if (o1.isDirectory() && o2.isDirectory()) {
- return o1.getName().toLowerCase().compareTo(o2.getName().toLowerCase());
- } else if (o1.isDirectory() && !o2.isDirectory()) {
- return -1;
- } else if (!o1.isDirectory() && o2.isDirectory()) {
- return 1;
- } else {
- return o1.getName().toLowerCase().compareTo(o2.getName().toLowerCase());
- }
- }
- }
-
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- MenuInflater menuInflater = getMenuInflater();
- menuInflater.inflate(R.menu.unicorn_menu_file_picker, menu);
-
- MenuItem item_search = menu.findItem(R.id.action_search);
-
- SearchView searchView = (SearchView) item_search.getActionView();
- searchView.setImeOptions(EditorInfo.IME_ACTION_DONE);
-
- searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
- @Override
- public boolean onQueryTextSubmit(String query) {
- return false;
- }
-
- @Override
- public boolean onQueryTextChange(String newText) {
- directoryAdapter.getFilter().filter(newText);
- return false;
- }
- });
- return true;
- }
-
- /**
- * This method checks whether STORAGE permissions are granted or not
- */
- private boolean allPermissionsGranted() {
- for (String permission : REQUIRED_PERMISSIONS) {
- if (ContextCompat.checkSelfPermission(FilePickerActivity.this, permission) != PackageManager.PERMISSION_GRANTED) {
- return false;
- }
- }
- return true;
- }
-
- @Override
- public void onBackPressed() {
- if (arr_dir_stack.size() > 1) {
- // pop off top value and display
- arr_dir_stack.remove(arr_dir_stack.size() - 1);
- DirectoryModel model = arr_dir_stack.remove(arr_dir_stack.size() - 1);
- fetchDirectory(model);
- } else {
- // Nothing left in stack so exit
- Intent intent = new Intent();
- setResult(config.getReqCode(), intent);
- setResult(RESULT_CANCELED, intent);
- finish();
- }
- }
-
- @Override
- public boolean onOptionsItemSelected(@NonNull MenuItem item) {
- int id = item.getItemId();
- if (id == android.R.id.home) {
- onBackPressed();
- }
- return true;
- }
-}
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/ui/FilePickerActivity.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/ui/FilePickerActivity.kt
new file mode 100644
index 0000000..8475f35
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/ui/FilePickerActivity.kt
@@ -0,0 +1,338 @@
+package abhishekti7.unicorn.filepicker.ui
+
+import abhishekti7.unicorn.filepicker.R
+import abhishekti7.unicorn.filepicker.adapters.DirectoryAdapter
+import abhishekti7.unicorn.filepicker.adapters.DirectoryAdapter.onFilesClickListener
+import abhishekti7.unicorn.filepicker.adapters.DirectoryStackAdapter
+import abhishekti7.unicorn.filepicker.adapters.StorageAdapter
+import abhishekti7.unicorn.filepicker.databinding.UnicornActivityFilePickerBinding
+import abhishekti7.unicorn.filepicker.models.Config
+import abhishekti7.unicorn.filepicker.models.DirectoryModel
+import abhishekti7.unicorn.filepicker.storage.StorageDirectoryParcelable
+import abhishekti7.unicorn.filepicker.storage.StorageUtils.getStorageDirectories
+import abhishekti7.unicorn.filepicker.utils.UnicornSimpleItemDecoration
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.ColorStateList
+import android.os.Build
+import android.os.Bundle
+import android.os.Environment
+import android.util.Log
+import android.util.TypedValue
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.widget.AdapterView
+import android.widget.AdapterView.OnItemSelectedListener
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.SearchView
+import androidx.appcompat.widget.SearchView.SearchAutoComplete
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import java.io.File
+import java.util.Collections
+import java.util.Locale
+
+/**
+ * Created by Abhishek Tiwari on 06-01-2021.
+ */
+class FilePickerActivity : AppCompatActivity() {
+ private lateinit var filePickerBinding: UnicornActivityFilePickerBinding
+ private lateinit var root_dir: File
+ private var selected_files: ArrayList = arrayListOf()
+ private var arr_dir_stack: ArrayList = arrayListOf()
+ private var arr_files: ArrayList = arrayListOf()
+ private lateinit var stackAdapter: DirectoryStackAdapter
+ private lateinit var directoryAdapter: DirectoryAdapter
+ private val REQUIRED_PERMISSIONS = arrayOf(
+ "android.permission.WRITE_EXTERNAL_STORAGE",
+ "android.permission.READ_EXTERNAL_STORAGE"
+ )
+ private lateinit var config: Config
+ private var filters: ArrayList = arrayListOf()
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ config = Config.getInstance()
+ setTheme(config.getThemeId())
+ filePickerBinding = UnicornActivityFilePickerBinding.inflate(
+ layoutInflater
+ )
+ val view: View = filePickerBinding.root
+ setContentView(view)
+ setupAvailableStorage()
+ initConfig()
+ }
+
+ private fun setupAvailableStorage() {
+ val storageDirectoryParcelableList = getStorageDirectories(this)
+ ?: return
+ val adapter = StorageAdapter(this, storageDirectoryParcelableList)
+ filePickerBinding.rvStoragePath.adapter = adapter
+ filePickerBinding.rvStoragePath.onItemSelectedListener = object : OnItemSelectedListener {
+ override fun onItemSelected(
+ parent: AdapterView<*>,
+ view: View,
+ position: Int,
+ id: Long
+ ) {
+ val storage = parent.adapter.getItem(position) as StorageDirectoryParcelable
+ config.rootDir = storage.path
+ initConfig()
+ }
+
+ override fun onNothingSelected(parent: AdapterView<*>?) {}
+ }
+ }
+
+ private fun initConfig() {
+ filters = config.extensionFilters
+ setSupportActionBar(filePickerBinding.toolbar)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ supportActionBar?.setDisplayShowTitleEnabled(false)
+ root_dir = if (config.rootDir != null) {
+ File(config.rootDir)
+ } else {
+ Environment.getExternalStorageDirectory()
+ }
+ selected_files = ArrayList()
+ arr_dir_stack = ArrayList()
+ arr_files = ArrayList()
+ setUpDirectoryStackView()
+ setUpFilesView()
+ if (allPermissionsGranted()) {
+ fetchDirectory(
+ DirectoryModel(
+ true,
+ root_dir.absolutePath,
+ root_dir.name,
+ root_dir.lastModified(),
+ if (root_dir.listFiles() == null) 0 else root_dir.listFiles().size
+ )
+ )
+ } else {
+ Log.e(
+ TAG,
+ "Storage permissions not granted. You have to implement it before starting the file picker"
+ )
+ finish()
+ }
+ filePickerBinding.fabSelect.setOnClickListener { v: View? ->
+ val intent = Intent()
+ if (config.showOnlyDirectory()) {
+ selected_files.clear()
+ selected_files.add(arr_dir_stack[arr_dir_stack.size - 1].path)
+ }
+ intent.putStringArrayListExtra("filePaths", selected_files)
+ setResult(config.reqCode, intent)
+ setResult(RESULT_OK, intent)
+ finish()
+ }
+ val typedValue = TypedValue()
+ val theme = theme
+ theme.resolveAttribute(R.attr.unicorn_fabColor, typedValue, true)
+ if (typedValue.data != 0) {
+ filePickerBinding.fabSelect.backgroundTintList =
+ ColorStateList.valueOf(typedValue.data)
+ } else {
+ filePickerBinding.fabSelect.backgroundTintList =
+ ColorStateList.valueOf(resources.getColor(R.color.unicorn_md_theme_tertiary))
+ }
+ }
+
+ private fun setUpFilesView() {
+ val layoutManager = LinearLayoutManager(this@FilePickerActivity)
+ filePickerBinding.rvFiles.layoutManager = layoutManager
+ directoryAdapter = DirectoryAdapter(
+ this@FilePickerActivity,
+ arr_files,
+ false,
+ object : onFilesClickListener {
+ override fun onClicked(model: DirectoryModel) {
+ fetchDirectory(model)
+ }
+
+ override fun onFileSelected(fileModel: DirectoryModel) {
+ if (config.isSelectMultiple) {
+ if (selected_files.contains(fileModel.path)) {
+ selected_files.remove(fileModel.path)
+ } else {
+ selected_files.add(fileModel.path)
+ }
+ } else {
+ selected_files.clear()
+ selected_files.add(fileModel.path)
+ }
+ }
+ })
+ filePickerBinding.rvFiles.adapter = directoryAdapter
+ directoryAdapter.notifyDataSetChanged()
+ if (config.addItemDivider()) {
+ filePickerBinding.rvFiles.addItemDecoration(UnicornSimpleItemDecoration(this@FilePickerActivity))
+ }
+ }
+
+ private fun setUpDirectoryStackView() {
+ val layoutManager =
+ LinearLayoutManager(this@FilePickerActivity, RecyclerView.HORIZONTAL, false)
+ filePickerBinding.rvDirPath.layoutManager = layoutManager
+ stackAdapter =
+ DirectoryStackAdapter(this@FilePickerActivity, arr_dir_stack) { model: DirectoryModel ->
+ Log.e(TAG, model.toString())
+ arr_dir_stack =
+ ArrayList(arr_dir_stack.subList(0, arr_dir_stack.indexOf(model) + 1))
+ setUpDirectoryStackView()
+ fetchDirectory(arr_dir_stack.removeAt(arr_dir_stack.size - 1))
+ }
+ filePickerBinding.rvDirPath.adapter = stackAdapter
+ stackAdapter.notifyDataSetChanged()
+ }
+
+ /**
+ * Fetches list of files in a folder and filters files if filter present
+ */
+ private fun fetchDirectory(model: DirectoryModel) {
+ filePickerBinding.rlProgress.visibility = View.VISIBLE
+ selected_files.clear()
+ arr_files.clear()
+ val dir = File(model.path)
+ val files_list = dir.listFiles()
+ if (files_list != null) {
+ for (file in files_list) {
+ val directoryModel = DirectoryModel()
+ directoryModel.isDirectory = file.isDirectory
+ directoryModel.name = file.name
+ directoryModel.path = file.absolutePath
+ directoryModel.last_modif_time = file.lastModified()
+ if (config.showHidden() || !config.showHidden() && !file.isHidden) {
+ if (file.isDirectory) {
+ if (file.listFiles() != null) directoryModel.num_files =
+ file.listFiles().size
+ arr_files.add(directoryModel)
+ } else {
+ if (!config.showOnlyDirectory()) {
+ // Filter out files if filters specified
+ if (filters != null) {
+ try {
+ // Extract the file extension
+ val fileName = file.name
+ val extension = fileName.substring(fileName.lastIndexOf("."))
+ for (filter in filters) {
+ if (extension.lowercase(Locale.getDefault())
+ .contains(filter)
+ ) {
+ arr_files.add(directoryModel)
+ }
+ }
+ } catch (e: Exception) {
+// Log.e(TAG, "Encountered a file without an extension: ", e);
+ }
+ } else {
+ arr_files.add(directoryModel)
+ }
+ }
+ }
+ }
+ }
+ Collections.sort(arr_files, CustomFileComparator())
+ arr_dir_stack.add(model)
+ filePickerBinding.rvDirPath.scrollToPosition(arr_dir_stack.size - 1)
+ filePickerBinding.toolbar.title = model.name
+ }
+ if (arr_files.size == 0) {
+ filePickerBinding.rlNoFiles.visibility = View.VISIBLE
+ } else {
+ filePickerBinding.rlNoFiles.visibility = View.GONE
+ }
+ filePickerBinding.rlProgress.visibility = View.GONE
+ stackAdapter.notifyDataSetChanged()
+ directoryAdapter.notifyDataSetChanged()
+ }
+
+ // Custom Comparator to sort the list of files in lexicographical order
+ class CustomFileComparator : Comparator {
+ override fun compare(o1: DirectoryModel, o2: DirectoryModel): Int {
+ return if (o1.isDirectory && o2.isDirectory) {
+ o1.name.lowercase(Locale.getDefault())
+ .compareTo(o2.name.lowercase(Locale.getDefault()))
+ } else if (o1.isDirectory && !o2.isDirectory) {
+ -1
+ } else if (!o1.isDirectory && o2.isDirectory) {
+ 1
+ } else {
+ o1.name.lowercase(Locale.getDefault())
+ .compareTo(o2.name.lowercase(Locale.getDefault()))
+ }
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ val menuInflater = menuInflater
+ menuInflater.inflate(R.menu.unicorn_menu_file_picker, menu)
+ val item_search = menu.findItem(R.id.action_search)
+ val searchView = item_search.actionView as SearchView
+ val searchText = searchView.findViewById(R.id.search_src_text)
+ searchText.setHintTextColor(resources.getColor(R.color.unicorn_md_theme_outline))
+ searchText.setTextColor(resources.getColor(R.color.unicorn_md_theme_onSurface))
+ searchView.imeOptions = EditorInfo.IME_ACTION_DONE
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String): Boolean {
+ return false
+ }
+
+ override fun onQueryTextChange(newText: String): Boolean {
+ directoryAdapter.filter.filter(newText)
+ return false
+ }
+ })
+ return true
+ }
+
+ /**
+ * This method checks whether STORAGE permissions are granted or not
+ */
+ private fun allPermissionsGranted(): Boolean {
+ var storagePermissionGranted = true
+ for (permission in REQUIRED_PERMISSIONS) {
+ storagePermissionGranted = storagePermissionGranted && ContextCompat.checkSelfPermission(
+ this@FilePickerActivity,
+ permission
+ ) != PackageManager.PERMISSION_GRANTED
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
+ return false
+ } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R && !storagePermissionGranted) {
+ return false
+ }
+ return true
+ }
+
+ override fun onBackPressed() {
+ if (arr_dir_stack.size > 1) {
+ // pop off top value and display
+ arr_dir_stack.removeAt(arr_dir_stack.size - 1)
+ val model = arr_dir_stack.removeAt(arr_dir_stack.size - 1)
+ fetchDirectory(model)
+ } else {
+ // Nothing left in stack so exit
+ val intent = Intent()
+ setResult(config.reqCode, intent)
+ setResult(RESULT_CANCELED, intent)
+ finish()
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ val id = item.itemId
+ if (id == android.R.id.home) {
+ onBackPressed()
+ }
+ return true
+ }
+
+ companion object {
+ private const val TAG = "FilePickerActivity"
+ }
+}
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/utils/DialogUtil.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/utils/DialogUtil.kt
new file mode 100644
index 0000000..f367e1e
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/utils/DialogUtil.kt
@@ -0,0 +1,31 @@
+package abhishekti7.unicorn.filepicker.utils
+
+import android.app.Activity
+import androidx.annotation.StringRes
+import androidx.appcompat.app.AlertDialog
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+
+object DialogUtil {
+ fun showBasicDialog(
+ activity: Activity,
+ @StringRes title: Int,
+ @StringRes message: Int,
+ @StringRes postiveText: Int,
+ @StringRes negativeText: Int,
+ onPositiveAction : () ->Unit
+ ): MaterialAlertDialogBuilder {
+ val dialogBuilder: MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(activity)
+ .setTitle(activity.resources.getString(title))
+ .setMessage(activity.resources.getString(message))
+ .setPositiveButton(activity.resources.getString(postiveText)
+ ) { dialog, p1 ->
+ onPositiveAction()
+ dialog.dismiss()
+ }
+ .setNegativeButton(activity.resources.getString(negativeText)){
+ dialog, p1 ->
+ dialog.dismiss()
+ }
+ return dialogBuilder
+ }
+}
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/utils/PermissionHelper.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/utils/PermissionHelper.kt
new file mode 100644
index 0000000..c9b2723
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/utils/PermissionHelper.kt
@@ -0,0 +1,51 @@
+package abhishekti7.unicorn.filepicker.utils
+
+import abhishekti7.unicorn.filepicker.R
+import android.app.Activity
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.Settings
+import android.util.Log
+import android.widget.Toast
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+object PermissionHelper {
+ private val TAG = PermissionHelper::class.java.simpleName
+ /**
+ * Request all files access on android 11+
+ *
+ * @param onPermissionGranted permission granted callback
+ */
+ fun requestAllFilesAccess(activity: Activity) {
+ activity?.apply {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
+ val materialDialog: MaterialAlertDialogBuilder = DialogUtil.showBasicDialog(
+ activity = activity,
+ title = R.string.unicorn_grant_permission,
+ message = R.string.unicorn_grant_all_files_permission,
+ postiveText = R.string.unicorn_grant,
+ negativeText = R.string.unicorn_cancel,
+ ){
+ try {
+ val intent =
+ Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
+ .setData(Uri.parse("package:" + activity.packageName))
+ activity.startActivity(intent)
+ } catch (e: java.lang.Exception) {
+ Log.e(
+ TAG,
+ "Failed to initial activity to grant all files access",
+ e
+ )
+ Toast.makeText(activity, activity.resources.getText(
+ R.string.unicorn_error_permission_not_granted), Toast.LENGTH_LONG).show()
+ }
+ }
+ materialDialog.setCancelable(false)
+ materialDialog.show()
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/utils/Utils.java b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/utils/Utils.java
deleted file mode 100644
index 82859aa..0000000
--- a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/utils/Utils.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package abhishekti7.unicorn.filepicker.utils;
-
-import java.util.Calendar;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Created by Abhishek Tiwari on 07-01-2021.
- */
-public class Utils {
- public static Map mapOfMonths = new HashMap() {{
- put(1, "Jan");
- put(2, "Feb");
- put(3, "Mar");
- put(4, "Apr");
- put(5, "May");
- put(6, "Jun");
- put(7, "Jul");
- put(8, "Aug");
- put(9, "Sep");
- put(10, "Oct");
- put(11, "Nov");
- put(12, "Dec");
- }};
- public static String longToReadableDate(long time) {
- Calendar calendar = Calendar.getInstance();
- calendar.setTimeInMillis(time);
-
- return mapOfMonths.get(calendar.get(Calendar.MONTH) + 1) + " " +
- calendar.get(Calendar.DAY_OF_MONTH) + ", " + calendar.get(Calendar.YEAR);
- }
-}
diff --git a/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/utils/Utils.kt b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/utils/Utils.kt
new file mode 100644
index 0000000..dc9cec0
--- /dev/null
+++ b/UnicornFilePicker/src/main/java/abhishekti7/unicorn/filepicker/utils/Utils.kt
@@ -0,0 +1,52 @@
+package abhishekti7.unicorn.filepicker.utils
+
+import android.annotation.TargetApi
+import android.os.Build
+import android.os.storage.StorageVolume
+import java.io.File
+import java.lang.Exception
+import java.lang.RuntimeException
+import java.util.*
+
+/**
+ * Created by Abhishek Tiwari on 07-01-2021.
+ */
+object Utils {
+ var mapOfMonths: HashMap = object : HashMap() {
+ init {
+ put(1, "Jan")
+ put(2, "Feb")
+ put(3, "Mar")
+ put(4, "Apr")
+ put(5, "May")
+ put(6, "Jun")
+ put(7, "Jul")
+ put(8, "Aug")
+ put(9, "Sep")
+ put(10, "Oct")
+ put(11, "Nov")
+ put(12, "Dec")
+ }
+ }
+
+ @JvmStatic
+ fun longToReadableDate(time: Long): String {
+ val calendar = Calendar.getInstance()
+ calendar.timeInMillis = time
+ return mapOfMonths[calendar[Calendar.MONTH] + 1].toString() + " " +
+ calendar[Calendar.DAY_OF_MONTH] + ", " + calendar[Calendar.YEAR]
+ }
+
+ @JvmStatic
+ @TargetApi(Build.VERSION_CODES.N)
+ fun getVolumeDirectory(volume: StorageVolume?): File? {
+ return try {
+ val f = StorageVolume::class.java.getDeclaredField("mPath")
+ f.isAccessible = true
+ f[volume] as File
+ } catch (e: Exception) {
+ // This shouldn't fail, as mPath has been there in every version
+ throw RuntimeException(e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/res/drawable/ic_baseline_sd_storage_24.xml b/UnicornFilePicker/src/main/res/drawable/ic_baseline_sd_storage_24.xml
new file mode 100644
index 0000000..84f6ea6
--- /dev/null
+++ b/UnicornFilePicker/src/main/res/drawable/ic_baseline_sd_storage_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/UnicornFilePicker/src/main/res/drawable/ic_baseline_smartphone_24.xml b/UnicornFilePicker/src/main/res/drawable/ic_baseline_smartphone_24.xml
new file mode 100644
index 0000000..bd22f2e
--- /dev/null
+++ b/UnicornFilePicker/src/main/res/drawable/ic_baseline_smartphone_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/UnicornFilePicker/src/main/res/drawable/ic_baseline_usb_24.xml b/UnicornFilePicker/src/main/res/drawable/ic_baseline_usb_24.xml
new file mode 100644
index 0000000..a783dd7
--- /dev/null
+++ b/UnicornFilePicker/src/main/res/drawable/ic_baseline_usb_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/UnicornFilePicker/src/main/res/drawable/ic_outline_phone_iphone_24.xml b/UnicornFilePicker/src/main/res/drawable/ic_outline_phone_iphone_24.xml
new file mode 100644
index 0000000..23ef826
--- /dev/null
+++ b/UnicornFilePicker/src/main/res/drawable/ic_outline_phone_iphone_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/UnicornFilePicker/src/main/res/drawable/drawable/unicorn_ic_arrow_right.xml b/UnicornFilePicker/src/main/res/drawable/unicorn_ic_arrow_right.xml
similarity index 100%
rename from UnicornFilePicker/src/main/res/drawable/drawable/unicorn_ic_arrow_right.xml
rename to UnicornFilePicker/src/main/res/drawable/unicorn_ic_arrow_right.xml
diff --git a/UnicornFilePicker/src/main/res/drawable/drawable/unicorn_ic_arrow_solid_right.xml b/UnicornFilePicker/src/main/res/drawable/unicorn_ic_arrow_solid_right.xml
similarity index 100%
rename from UnicornFilePicker/src/main/res/drawable/drawable/unicorn_ic_arrow_solid_right.xml
rename to UnicornFilePicker/src/main/res/drawable/unicorn_ic_arrow_solid_right.xml
diff --git a/UnicornFilePicker/src/main/res/drawable/drawable/unicorn_ic_done.xml b/UnicornFilePicker/src/main/res/drawable/unicorn_ic_done.xml
similarity index 100%
rename from UnicornFilePicker/src/main/res/drawable/drawable/unicorn_ic_done.xml
rename to UnicornFilePicker/src/main/res/drawable/unicorn_ic_done.xml
diff --git a/UnicornFilePicker/src/main/res/drawable/drawable/unicorn_ic_folder.xml b/UnicornFilePicker/src/main/res/drawable/unicorn_ic_folder.xml
similarity index 100%
rename from UnicornFilePicker/src/main/res/drawable/drawable/unicorn_ic_folder.xml
rename to UnicornFilePicker/src/main/res/drawable/unicorn_ic_folder.xml
diff --git a/UnicornFilePicker/src/main/res/drawable/drawable/unicorn_ic_search.xml b/UnicornFilePicker/src/main/res/drawable/unicorn_ic_search.xml
similarity index 100%
rename from UnicornFilePicker/src/main/res/drawable/drawable/unicorn_ic_search.xml
rename to UnicornFilePicker/src/main/res/drawable/unicorn_ic_search.xml
diff --git a/UnicornFilePicker/src/main/res/layout/unicorn_activity_file_picker.xml b/UnicornFilePicker/src/main/res/layout/unicorn_activity_file_picker.xml
index bebc980..9ec81b5 100644
--- a/UnicornFilePicker/src/main/res/layout/unicorn_activity_file_picker.xml
+++ b/UnicornFilePicker/src/main/res/layout/unicorn_activity_file_picker.xml
@@ -2,40 +2,51 @@
-
+ android:layout_height="wrap_content"
+ app:subtitleTextColor="@color/unicorn_md_theme_onSurface"
+ app:titleTextColor="@color/unicorn_md_theme_onSurface"/>
+
+
+ app:layout_constraintTop_toBottomOf="@+id/rv_storage_path" />
+ android:progressTint="@color/unicorn_md_theme_primary"/>
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/res/layout/unicorn_item_layout_directory.xml b/UnicornFilePicker/src/main/res/layout/unicorn_item_layout_directory.xml
index 4086b31..f02fa6a 100644
--- a/UnicornFilePicker/src/main/res/layout/unicorn_item_layout_directory.xml
+++ b/UnicornFilePicker/src/main/res/layout/unicorn_item_layout_directory.xml
@@ -6,9 +6,8 @@
android:layout_height="wrap_content"
android:minHeight="70dp">
-
@@ -38,27 +37,27 @@
android:id="@+id/tv_num_files"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textColor="?attr/unicorn.file.subHeading"
android:textSize="12sp"
+ android:textColor="@color/unicorn_md_theme_onSurface"
android:layout_below="@id/tv_folder_name"/>
-
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/res/layout/unicorn_item_layout_directory_stack.xml b/UnicornFilePicker/src/main/res/layout/unicorn_item_layout_directory_stack.xml
index dc7aed9..3b3e9f7 100644
--- a/UnicornFilePicker/src/main/res/layout/unicorn_item_layout_directory_stack.xml
+++ b/UnicornFilePicker/src/main/res/layout/unicorn_item_layout_directory_stack.xml
@@ -8,16 +8,16 @@
android:id="@+id/tv_dir_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textSize="12sp"
- android:textColor="?attr/unicorn.primaryTextColor"
android:layout_centerVertical="true"
- android:paddingVertical="2dp"/>
-
+
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/res/layout/unicorn_item_layout_files.xml b/UnicornFilePicker/src/main/res/layout/unicorn_item_layout_files.xml
index 58bb452..dcd8b2e 100644
--- a/UnicornFilePicker/src/main/res/layout/unicorn_item_layout_files.xml
+++ b/UnicornFilePicker/src/main/res/layout/unicorn_item_layout_files.xml
@@ -7,15 +7,15 @@
android:layout_height="wrap_content"
android:minHeight="70dp">
-
diff --git a/UnicornFilePicker/src/main/res/layout/unicorn_storage_picker_item.xml b/UnicornFilePicker/src/main/res/layout/unicorn_storage_picker_item.xml
new file mode 100644
index 0000000..28f94ba
--- /dev/null
+++ b/UnicornFilePicker/src/main/res/layout/unicorn_storage_picker_item.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/res/menu/unicorn_menu_file_picker.xml b/UnicornFilePicker/src/main/res/menu/unicorn_menu_file_picker.xml
index 2c9d35d..d57a748 100644
--- a/UnicornFilePicker/src/main/res/menu/unicorn_menu_file_picker.xml
+++ b/UnicornFilePicker/src/main/res/menu/unicorn_menu_file_picker.xml
@@ -7,5 +7,6 @@
android:title="@string/unicorn_search"
android:icon="@drawable/unicorn_ic_search"
app:showAsAction="always"
+ app:iconTint="@color/unicorn_md_theme_onSurface"
app:actionViewClass="androidx.appcompat.widget.SearchView"/>
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/res/values-night/colors.xml b/UnicornFilePicker/src/main/res/values-night/colors.xml
new file mode 100644
index 0000000..fb2b714
--- /dev/null
+++ b/UnicornFilePicker/src/main/res/values-night/colors.xml
@@ -0,0 +1,33 @@
+
+
+ #BAC3FF
+ #001E91
+ #002ECA
+ #DEE0FF
+ #C3C5DD
+ #2D2F42
+ #434659
+ #E0E1F9
+ #E6BAD7
+ #44263D
+ #5D3C55
+ #FFD7F1
+ #FFB4AB
+ #93000A
+ #690005
+ #FFDAD6
+ #1B1B1F
+ #E4E1E6
+ #1B1B1F
+ #E4E1E6
+ #46464F
+ #C7C5D0
+ #90909A
+ #1B1B1F
+ #E4E1E6
+ #2A4AE8
+ #000000
+ #BAC3FF
+ #46464F
+ #000000
+
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/res/values/colors.xml b/UnicornFilePicker/src/main/res/values/colors.xml
index 8cd8a67..89f0ec5 100644
--- a/UnicornFilePicker/src/main/res/values/colors.xml
+++ b/UnicornFilePicker/src/main/res/values/colors.xml
@@ -1,15 +1,33 @@
-
- #ffa502
- #000
- #000
-
- #fff
- #000
- #B3000000
- #B3FFFFFF
- #353535
- #E5E7E9
-
+ #2A4AE8
+ #FFFFFF
+ #DEE0FF
+ #00105C
+ #5B5D72
+ #FFFFFF
+ #E0E1F9
+ #181A2C
+ #77536D
+ #FFFFFF
+ #FFD7F1
+ #2D1228
+ #BA1A1A
+ #FFDAD6
+ #FFFFFF
+ #410002
+ #FEFBFF
+ #1B1B1F
+ #FEFBFF
+ #1B1B1F
+ #E3E1EC
+ #46464F
+ #767680
+ #F3F0F4
+ #303034
+ #BAC3FF
+ #000000
+ #2A4AE8
+ #C7C5D0
+ #000000
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/res/values/strings.xml b/UnicornFilePicker/src/main/res/values/strings.xml
index 7ddf356..986fd26 100644
--- a/UnicornFilePicker/src/main/res/values/strings.xml
+++ b/UnicornFilePicker/src/main/res/values/strings.xml
@@ -1,4 +1,12 @@
Search
No files
+ Root
+ Internal
+ SD Card
+ Our app needs permission to access all the files on your phone in order to load them. Once you press the "Grant" button, please select the option that says "Allow access to manage all files" on the next screen. This will enable the app to effectively manage and display all the files stored on your device.
+ Grant permission
+ Grant
+ Permission not granted
+ Cancel
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/res/values/style.xml b/UnicornFilePicker/src/main/res/values/style.xml
deleted file mode 100644
index e7c1279..0000000
--- a/UnicornFilePicker/src/main/res/values/style.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/UnicornFilePicker/src/main/res/values/themes.xml b/UnicornFilePicker/src/main/res/values/themes.xml
new file mode 100644
index 0000000..6791968
--- /dev/null
+++ b/UnicornFilePicker/src/main/res/values/themes.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
diff --git a/app/build.gradle b/app/build.gradle
index 4619cf4..e2ae965 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,10 +1,10 @@
plugins {
id 'com.android.application'
}
+apply plugin: 'kotlin-android'
android {
- compileSdkVersion 30
- buildToolsVersion "30.0.2"
+ compileSdk 34
viewBinding{
enabled true
@@ -13,7 +13,7 @@ android {
defaultConfig {
applicationId "dev.abhishekti7.filepickerexample"
minSdkVersion 21
- targetSdkVersion 30
+ targetSdk 34
versionCode 1
versionName "1.0"
@@ -26,19 +26,29 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
- implementation 'androidx.appcompat:appcompat:1.2.0'
- implementation 'com.google.android.material:material:1.2.1'
- implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.9.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation project(path: ':UnicornFilePicker')
- testImplementation 'junit:junit:4.+'
- androidTestImplementation 'androidx.test.ext:junit:1.1.2'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ implementation "androidx.core:core-ktx:1.10.1"
+ //noinspection GradleDependency
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
+repositories {
+ mavenCentral()
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4d6ffdb..7d10e1e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,8 +12,8 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
- android:theme="@style/Theme.FilePickerExample">
-
+ android:theme="@style/UnicornFilePicker.Default.AppTheme">
+
diff --git a/app/src/main/java/dev/abhishekti7/filepickerexample/MainActivity.java b/app/src/main/java/dev/abhishekti7/filepickerexample/MainActivity.java
index 81cd11f..34d6639 100644
--- a/app/src/main/java/dev/abhishekti7/filepickerexample/MainActivity.java
+++ b/app/src/main/java/dev/abhishekti7/filepickerexample/MainActivity.java
@@ -10,14 +10,28 @@
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
+import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
import abhishekti7.unicorn.filepicker.UnicornFilePicker;
+import abhishekti7.unicorn.filepicker.storage.StorageUtils;
import abhishekti7.unicorn.filepicker.utils.Constants;
+import abhishekti7.unicorn.filepicker.utils.PermissionHelper;
import dev.abhishekti7.filepickerexample.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
@@ -29,7 +43,23 @@ public class MainActivity extends AppCompatActivity {
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.READ_EXTERNAL_STORAGE",
};
-
+ public static final String SD_CARD = "sdCard";
+ public static final String EXTERNAL_SD_CARD = "externalSdCard";
+ private static final String ENV_SECONDARY_STORAGE = "SECONDARY_STORAGE";
+ public static Map getAllStorageLocations() {
+ Map storageLocations = new HashMap(10);
+ File sdCard = Environment.getExternalStorageDirectory();
+ storageLocations.put(SD_CARD, sdCard);
+ final String rawSecondaryStorage = System.getenv(ENV_SECONDARY_STORAGE);
+ if (!TextUtils.isEmpty(rawSecondaryStorage)) {
+ String[] externalCards = rawSecondaryStorage.split(":");
+ for (int i = 0; i < externalCards.length; i++) {
+ String path = externalCards[i];
+ storageLocations.put(EXTERNAL_SD_CARD + String.format(i == 0 ? "" : "_%d", i), new File(path));
+ }
+ }
+ return storageLocations;
+ }
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -37,6 +67,8 @@ protected void onCreate(Bundle savedInstanceState) {
View view = mainBinding.getRoot();
setContentView(view);
+
+ PermissionHelper.INSTANCE.requestAllFilesAccess(this);
if (allPermissionsGranted()) {
Toast.makeText(MainActivity.this, "Permissions granted by the user.", Toast.LENGTH_SHORT).show();
} else {
@@ -44,6 +76,16 @@ protected void onCreate(Bundle savedInstanceState) {
}
+ Log.e(TAG, "GetAllLocations " + StorageUtils.INSTANCE.getStorageDirectories(this));
+ File[] f = ContextCompat.getExternalFilesDirs(getApplicationContext(),null);
+ for (int i=0;i< f.length;i++)
+ {
+ String path = f[i].getParent().replace("/Android/data/","").replace(getPackageName(),"");
+ Log.d("DIRS",path); //sdcard and internal and usb
+ }
+
+
+
mainBinding.btnFilesDark.setOnClickListener((v)->{
UnicornFilePicker.from(MainActivity.this)
.addConfigBuilder()
@@ -53,7 +95,7 @@ protected void onCreate(Bundle savedInstanceState) {
.showHiddenFiles(false)
.setFilters(new String[]{"pdf", "png", "jpg", "jpeg"})
.addItemDivider(true)
- .theme(R.style.UnicornFilePicker_Dracula)
+ .theme(R.style.UnicornFilePicker_Default_AppTheme)
.build()
.forResult(Constants.REQ_UNICORN_FILE);
});
@@ -66,7 +108,7 @@ protected void onCreate(Bundle savedInstanceState) {
.showHiddenFiles(false)
.setFilters(new String[]{"pdf", "png", "jpg", "jpeg"})
.addItemDivider(true)
- .theme(R.style.UnicornFilePicker_Default)
+ .theme(R.style.UnicornFilePicker_Default_AppTheme)
.build()
.forResult(Constants.REQ_UNICORN_FILE);
});
@@ -79,7 +121,7 @@ protected void onCreate(Bundle savedInstanceState) {
.showHiddenFiles(false)
.setFilters(new String[]{"pdf", "png", "jpg", "jpeg"})
.addItemDivider(true)
- .theme(R.style.Theme_CustomUnicorn)
+ .theme(R.style.UnicornFilePicker_Default_AppTheme)
.build()
.forResult(Constants.REQ_UNICORN_FILE);
});
@@ -110,6 +152,7 @@ private boolean allPermissionsGranted() {
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
Toast.makeText(MainActivity.this, "Permissions granted by the user.", Toast.LENGTH_SHORT).show();
@@ -118,4 +161,5 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis
}
}
}
+
}
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1cce44b..78a2e33 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,4 @@
FilePickerExample
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index c9ca90d..799cec9 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,6 +1,6 @@
-
-
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index c8d7712..4f443ce 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,11 +1,16 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
+ ext {
+ agp_version = '7.4.2'
+ }
+ ext.kotlin_version = '1.8.0'
repositories {
google()
jcenter()
}
dependencies {
- classpath "com.android.tools.build:gradle:4.1.0"
+ classpath "com.android.tools.build:gradle:$agp_version"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 66c0c24..80cfd82 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip