Till now you have added functionality to just read and open files in the .In this tutorial you will add the features to create/delete files and folders in the files system.
Adding menu options to create
We will add the options to create a file/folder in the menu options of our toolbar. Below is how the application will look once the options are added.
Go ahead and create a new menu file named main_menu.xml.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:context=".main.MainActivity"> <item android:id="@+id/subMenu" android:icon="@drawable/ic_folder_grey_24dp" android:title="Options" app:showAsAction="always" tools:ignore="AlwaysShowAction"> <menu> <item android:id="@+id/menuNewFile" android:icon="@drawable/ic_insert_drive_file_grey_24dp" android:title="New File" app:showAsAction="never" /> <item android:id="@+id/menuNewFolder" android:icon="@drawable/ic_create_new_folder_grey_24dp" android:title="New Folder" app:showAsAction="never" /> </menu> </item> </menu> |
You can import the necessary icons used in the menu by right clicking on res -> New -> Image Asset. Or you can use any icons that you would like.
Inflate the menu in MainActivity.kt.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class MainActivity : AppCompatActivity(), FilesListFragment.OnItemClickListener { ... override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.main_menu, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem?): Boolean { return super.onOptionsItemSelected(item) } } |
As we have added two options in the menu, one for creating folder and one for file, lets check if the users presses any one of them.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class MainActivity : AppCompatActivity(), FilesListFragment.OnItemClickListener { ... override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.main_menu, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem?): Boolean { when (item?.itemId) { R.id.menuNewFile -> createNewFileInCurrentDirectory() R.id.menuNewFolder -> createNewFolderInCurrentDirectory() } return super.onOptionsItemSelected(item) } private fun createNewFileInCurrentDirectory() { } private fun createNewFolderInCurrentDirectory() { } } |
Before actually creating the File/Folder we first need the name of the new File/Folder being created. We will ask the user for this name inside a dialog. We will use the inbuilt BottomSheetDialog to get user input. First lets create a layout for the dialog. Create a layout file named dialog_enter_name.xml.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp"> <TextView android:id="@+id/enterNameTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Enter a name" android:textSize="20sp" /> <EditText android:id="@+id/nameEditText" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="32dp" app:layout_constraintLeft_toLeftOf="parent" android:lines="1" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/enterNameTextView" android:inputType="text" /> <Button android:id="@+id/createButton" style="@style/PrimaryButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="32dp" android:text="Create" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/nameEditText" /> </android.support.constraint.ConstraintLayout> |
First lets complete the process of creating a new File by showing a dialog, getting the input and creating the file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
class MainActivity : AppCompatActivity(), FilesListFragment.OnItemClickListener { ... override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.main_menu, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem?): Boolean { when (item?.itemId) { R.id.menuNewFile -> createNewFileInCurrentDirectory() R.id.menuNewFolder -> createNewFolderInCurrentDirectory() } return super.onOptionsItemSelected(item) } private fun createNewFileInCurrentDirectory() { val bottomSheetDialog = BottomSheetDialog(this) val view = LayoutInflater.from(this).inflate(R.layout.dialog_enter_name, null) view.createButton.setOnClickListener { val fileName = view.nameEditText.text.toString() } bottomSheetDialog.setContentView(view) bottomSheetDialog.show() } private fun createNewFolderInCurrentDirectory() { } } |
Now let’s create a new function in FileUtils.kt named createNewFile that will create a new file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
fun createNewFile(fileName: String, path: String, callback: (result: Boolean, message: String) -> Unit) { val fileAlreadyExists = File(path).listFiles().map { it.name }.contains(fileName) if (fileAlreadyExists) { callback(false, "'${fileName}' already exists.") } else { val file = File(path, fileName) try { val result = file.createNewFile() if (result) { callback(result, "File '${fileName}' created successfully.") } else { callback(result, "Unable to create file '${fileName}'.") } } catch (e: Exception) { callback(false, "Unable to create file. Please try again.") e.printStackTrace() } } } |
The functions takes in file name, path and a callback which will be called on successful/unsuccessful creation of file. We first check if a file with provided name already exists, and if so then we notify using the callback. If not, then we try to create the new file and notify the result using callback. Let’s use this function in the dialog.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class MainActivity : AppCompatActivity(), FilesListFragment.OnItemClickListener { ... override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.main_menu, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem?): Boolean { when (item?.itemId) { R.id.menuNewFile -> createNewFileInCurrentDirectory() R.id.menuNewFolder -> createNewFolderInCurrentDirectory() } return super.onOptionsItemSelected(item) } private fun createNewFileInCurrentDirectory() { val bottomSheetDialog = BottomSheetDialog(this) val view = LayoutInflater.from(this).inflate(R.layout.dialog_enter_name, null) view.createButton.setOnClickListener { val fileName = view.nameEditText.text.toString() if (fileName.isNotEmpty()) { createNewFolder(fileName, backStackManager.top.path) { _, message -> bottomSheetDialog.dismiss() } } } bottomSheetDialog.setContentView(view) bottomSheetDialog.show() } private fun createNewFolderInCurrentDirectory() { } } |
This is how the dialog will look like.
Notifying file create changes
The file will be created successfully. But there is a problem. The current fragment hasn’t been notified that the list of files has been updated (a new file has been created). We can do that by directly accessing the current fragment and calling functions on it. But this is not a good approach. We will use Broadcast Receiver for this. Go ahead an create a new BroadcastReceiver named FileChangeBroadcastReceiver.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class FileChangeBroadcastReceiver(val path: String, val onChange: () -> Unit) : BroadcastReceiver() { companion object { const val EXTRA_PATH = "com.thetechnocafe.gurleensethi.kotlinfileexplorer.fileservice.path" } override fun onReceive(context: Context?, intent: Intent?) { val filePath = intent?.extras?.getString(EXTRA_PATH) if (filePath.equals(path)) { onChange.invoke() } } } |
As constructor arguments this receiver will take a path and a onChange listener. Whenever files at a certain path are changed, a broadcast for that path will be sent, and any receiver listening for that particular path will receive the event and invoke the listener.
Register/Unregister this receiver in FilesListFragment.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
class FilesListFragment : Fragment() { private lateinit var mFileChangeBroadcastReceiver: FileChangeBroadcastReceiver ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val filePath = arguments?.getString(ARG_PATH) if (filePath == null) { Toast.makeText(context, "Path should not be null!", Toast.LENGTH_SHORT).show() return } PATH = filePath mFileChangeBroadcastReceiver = FileChangeBroadcastReceiver(PATH) { updateDate() } } override fun onResume() { super.onResume() context?.registerReceiver(mFileChangeBroadcastReceiver, IntentFilter(getString(R.string.file_change_broadcast))) } override fun onPause() { super.onPause() context?.unregisterReceiver(mFileChangeBroadcastReceiver) } fun updateDate() { val files = getFileModelsFromFiles(getFilesFromPath(PATH)) if (files.isEmpty()) { emptyFolderLayout.visibility = View.VISIBLE } else { emptyFolderLayout.visibility = View.INVISIBLE } mFilesAdapter.updateData(files) } ... } |
Now when there is any change at any path, all we have to do is send a broadcast.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
class MainActivity : AppCompatActivity(), FilesListFragment.OnItemClickListener { ... override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.main_menu, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem?): Boolean { when (item?.itemId) { R.id.menuNewFile -> createNewFileInCurrentDirectory() R.id.menuNewFolder -> createNewFolderInCurrentDirectory() } return super.onOptionsItemSelected(item) } private fun createNewFileInCurrentDirectory() { val bottomSheetDialog = BottomSheetDialog(this) val view = LayoutInflater.from(this).inflate(R.layout.dialog_enter_name, null) view.createButton.setOnClickListener { val fileName = view.nameEditText.text.toString() if (fileName.isNotEmpty()) { createNewFolder(fileName, backStackManager.top.path) { _, message -> bottomSheetDialog.dismiss() updateContentOfCurrentFragment() } } } bottomSheetDialog.setContentView(view) bottomSheetDialog.show() } private fun createNewFolderInCurrentDirectory() { } private fun updateContentOfCurrentFragment() { val broadcastIntent = Intent() broadcastIntent.action = applicationContext.getString(R.string.file_change_broadcast) broadcastIntent.putExtra(FileChangeBroadcastReceiver.EXTRA_PATH, backStackManager.top.path) sendBroadcast(broadcastIntent) } } |
The process of create a new Folder is exactly the same. First create a function named createNewFolder in FileUtils.kt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
fun createNewFolder(folderName: String, path: String, callback: (result: Boolean, message: String) -> Unit) { val folderAlreadyExists = File(path).listFiles().map { it.name }.contains(folderName) if (folderAlreadyExists) { callback(false, "'${folderName}' already exists.") } else { val file = File(path, folderName) try { val result = file.mkdir() if (result) { callback(result, "Folder '${folderName}' created successfully.") } else { callback(result, "Unable to create folder '${folderName}'.") } } catch (e: Exception) { callback(false, "Unable to create folder. Please try again.") e.printStackTrace() } } } |
As you can see this is almost same as the createNewFile function. Now let’s show the dialog and call this function in MainActivity.kt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
class MainActivity : AppCompatActivity(), FilesListFragment.OnItemClickListener { ... override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.main_menu, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem?): Boolean { when (item?.itemId) { R.id.menuNewFile -> createNewFileInCurrentDirectory() R.id.menuNewFolder -> createNewFolderInCurrentDirectory() } return super.onOptionsItemSelected(item) } private fun createNewFileInCurrentDirectory() { val bottomSheetDialog = BottomSheetDialog(this) val view = LayoutInflater.from(this).inflate(R.layout.dialog_enter_name, null) view.createButton.setOnClickListener { val fileName = view.nameEditText.text.toString() if (fileName.isNotEmpty()) { createNewFolder(fileName, backStackManager.top.path) { _, message -> bottomSheetDialog.dismiss() updateContentOfCurrentFragment() } } } bottomSheetDialog.setContentView(view) bottomSheetDialog.show() } private fun createNewFolderInCurrentDirectory() { val bottomSheetDialog = BottomSheetDialog(this) val view = LayoutInflater.from(this).inflate(R.layout.dialog_enter_name, null) view.createButton.setOnClickListener { val fileName = view.nameEditText.text.toString() if (fileName.isNotEmpty()) { createNewFolder(fileName, backStackManager.top.path) { _, message -> bottomSheetDialog.dismiss() coordinatorLayout.createShortSnackbar(message) updateContentOfCurrentFragment() } } } bottomSheetDialog.setContentView(view) bottomSheetDialog.show() } private fun updateContentOfCurrentFragment() { val broadcastIntent = Intent() broadcastIntent.action = applicationContext.getString(R.string.file_change_broadcast) broadcastIntent.putExtra(FileChangeBroadcastReceiver.EXTRA_PATH, backStackManager.top.path) sendBroadcast(broadcastIntent) } } |
Never miss a post from TheTechnoCafe
Deleting Files
When the user long presses on a folder or a file we will show an options dialog which will contain a delete button. So first set up the dialog. Create a new file named FileOptionsDialog.kt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class FileOptionsDialog : BottomSheetDialogFragment() { var onDeleteClickListener: (() -> Unit)? = null companion object { fun build(block: Builder.() -> Unit): FileOptionsDialog = Builder().apply(block).build() } class Builder { fun build(): FileOptionsDialog { val fragment = FileOptionsDialog() val args = Bundle() fragment.arguments = args return fragment } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.dialog_file_options, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initViews() } private fun initViews() { deleteTextView.setOnClickListener { onDeleteClickListener?.invoke() dismiss() } } } |
In this case we will extend BottomSheetDialogFragment. Create a on delete listener and invoke it when the button is clicked. Below is the layout file for the same.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:paddingBottom="24dp"> <TextView android:id="@+id/fileOptionsTextView" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="24dp" android:text="Options" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/deleteTextView" style="@style/FileDialogOption" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:drawableLeft="@drawable/ic_delete_grey_24dp" android:drawableStart="@drawable/ic_delete_grey_24dp" android:text="Delete" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/fileOptionsTextView" /> </android.support.constraint.ConstraintLayout> |
Now let’s open this dialog when user long presses on a file or folder. Do this in onLongClick in the MainActivity.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class MainActivity : AppCompatActivity(), FilesListFragment.OnItemClickListener { companion object { private const val OPTIONS_DIALOG_TAG: String = "com.thetechnocafe.gurleensethi.kotlinfileexplorer.main.options_dialog" } ... override fun onLongClick(fileModel: FileModel) { val optionsDialog = FileOptionsDialog.build {} optionsDialog.onDeleteClickListener = { } optionsDialog.show(supportFragmentManager, OPTIONS_DIALOG_TAG) } } |
Create a function in FileUtils.kt to delete a file/folder from a path.
1 2 3 4 5 6 7 8 |
fun deleteFile(path: String) { val file = File(path) if (file.isDirectory) { file.deleteRecursively() } else { file.delete() } } |
We first check if its a folder then delete recursively, and if its a file then use the delete function.
Note: Android already has a function name deleteFile in the Activity scope so we need to use a named import for this function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import com.thetechnocafe.gurleensethi.kotlinfileexplorer.utils.deleteFile as FileUtilsDeleteFile class MainActivity : AppCompatActivity(), FilesListFragment.OnItemClickListener { companion object { private const val OPTIONS_DIALOG_TAG: String = "com.thetechnocafe.gurleensethi.kotlinfileexplorer.main.options_dialog" } ... override fun onLongClick(fileModel: FileModel) { val optionsDialog = FileOptionsDialog.build {} optionsDialog.onDeleteClickListener = { FileUtilsDeleteFile(fileModel.path) updateContentOfCurrentFragment() } optionsDialog.show(supportFragmentManager, OPTIONS_DIALOG_TAG) } } |
This is how the dialog will look like.
That is it for this tutorial. It was a long time. In the next tutorial, you will see how to copy and move files from one path to another.
<< Previous Tutorial – Part 4 – Adding Breadcrumbs.
1 Comment
Fauzie · May 15, 2019 at 10:56 pm
Please continue this series.