diff --git a/app/build.gradle b/app/build.gradle index f038574..c42034f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,23 +20,28 @@ android { applicationId "ua.leonidius.rtlnotepad" minSdkVersion 17 targetSdkVersion 29 - versionCode 2 + versionCode 3 versionName getVersionName() } buildTypes { release { + resValue "bool", "DEBUG", "false" minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } + debug { + resValue "bool", "DEBUG", "true" + } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "androidx.lifecycle:lifecycle-extensions:2.2.0-beta01" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation project(":navdialogs") implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.chibatching.kotpref:kotpref:2.10.0' } repositories { mavenCentral() diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b4245..21c39e6 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,3 +19,7 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +-assumenosideeffects class kotlin.jvm.internal.Intrinsics { + static void checkParameterIsNotNull(java.lang.Object, java.lang.String); +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 884a936..7b54acd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,41 +1,35 @@ + package="ua.leonidius.rtlnotepad" + android:installLocation="auto"> - + + android:name=".MyApplication" + android:supportsRtl="true" + android:allowBackup="true" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name" + android:theme="@style/Leonidius.Light"> + android:name=".MainActivity" + android:windowSoftInputMode="stateHidden" + android:label="@string/app_name"> - - + + - - + + - - + + - - + \ No newline at end of file diff --git a/app/src/main/java/ua/leonidius/rtlnotepad/EditorFragment.kt b/app/src/main/java/ua/leonidius/rtlnotepad/EditorFragment.kt index 672c0f4..2683a68 100644 --- a/app/src/main/java/ua/leonidius/rtlnotepad/EditorFragment.kt +++ b/app/src/main/java/ua/leonidius/rtlnotepad/EditorFragment.kt @@ -1,30 +1,29 @@ package ua.leonidius.rtlnotepad -import android.Manifest +import android.app.Activity import android.content.Context -import android.content.pm.PackageManager +import android.content.Intent +import android.net.Uri +import android.os.Build import android.os.Bundle import android.text.Editable import android.text.TextWatcher -import android.util.Log import android.view.* import android.widget.EditText import android.widget.Toast -import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment -import ua.leonidius.navdialogs.SaveDialog -import ua.leonidius.rtlnotepad.dialogs.* -import ua.leonidius.rtlnotepad.utils.LastFilesMaster -import ua.leonidius.rtlnotepad.utils.ReadTask -import ua.leonidius.rtlnotepad.utils.WriteTask - -import java.io.File +import ua.leonidius.navdialogs.LegacySaveDialog +import ua.leonidius.rtlnotepad.dialogs.CloseTabDialog +import ua.leonidius.rtlnotepad.dialogs.ConfirmEncodingChangeDialog +import ua.leonidius.rtlnotepad.dialogs.EncodingDialog +import ua.leonidius.rtlnotepad.utils.* class EditorFragment : Fragment() { internal var mTag: String = System.currentTimeMillis().toString() - var file: File? = null + var uri: Uri? = null + private lateinit var tabTitle: String private var currentEncoding = "UTF-8" internal var hasUnsavedChanges = false @@ -49,42 +48,31 @@ class EditorFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { val scrollView = inflater.inflate(R.layout.main, container, false) - editor = scrollView.findViewById(R.id.editor) - editor.textSize = mActivity.pref.getInt(mActivity.PREF_TEXT_SIZE, mActivity.SIZE_MEDIUM).toFloat() - editor.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(p1: CharSequence, p2: Int, p3: Int, p4: Int) {} - override fun onTextChanged(p1: CharSequence, p2: Int, p3: Int, p4: Int) {} - override fun afterTextChanged(p1: Editable) { - if (!ignoreNextTextChange) setTextChanged(true) - else ignoreNextTextChange = false - } - }) + + editor = scrollView.findViewById(R.id.editor).apply { + textSize = Settings.textSize.toFloat() + addTextChangedListener(getTextWatcher()) + } return scrollView } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - if (!initialized) { // Cold start - - val arguments = arguments - if (arguments != null) { - val filePath = arguments.getString(ARGUMENT_FILE_PATH, null) - if (filePath != null) file = File(filePath) - - if (file != null) { - readFile(file!!, currentEncoding) { text -> - if (text == null) - close() // Close if failed to read requested file - else - setTextWithProgressDialog(text) - setTextChanged(false) - } + if (initialized) return + arguments?.getParcelable(ARGUMENT_URI)?.also { + uri = it + tabTitle = getFileName(mActivity, it) ?: getString(R.string.new_document) + ReadTask(mActivity.contentResolver, it, currentEncoding) { text -> + if (text == null) close() + else { + setTextWithProgressDialog(text) + setTextChanged(false) } - - } - initialized = true + }.execute() } + initialized = true + if (uri == null) tabTitle = getString(R.string.new_document) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -92,13 +80,28 @@ class EditorFragment : Fragment() { super.onCreateOptionsMenu(menu, inflater) } + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + uri?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !canWriteFile(mActivity, it)) { + menu.findItem(R.id.options_save).isEnabled = false + menu.findItem(R.id.options_save_as).isEnabled = false + } + } + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.options_save -> { - if (file == null) - openSaveDialog() - else - saveChanges() + if (uri == null) openSaveDialog() + else { + // Saving changes + if (!canWriteFile(mActivity, uri!!)) { + Toast.makeText(mActivity, R.string.file_read_only, Toast.LENGTH_SHORT).show() + return true + } + writeFile(uri!!, currentEncoding) + } return true } R.id.options_save_as -> { @@ -122,73 +125,45 @@ class EditorFragment : Fragment() { * Shows a SaveDialog and writes the text to the selected file. */ private fun openSaveDialog() { - SaveDialog.create { file, encoding -> - writeFile(file, editor.text.toString(), encoding) { success -> - if (success) { - this.file = file - this.currentEncoding = encoding - setTextChanged(false) - val successMessage = resources.getString(R.string.file_save_success, file.name) - Toast.makeText(context, successMessage, Toast.LENGTH_SHORT).show() - LastFilesMaster.add(file) - } else { - Toast.makeText(context, R.string.file_save_error, Toast.LENGTH_SHORT).show() - } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT || Settings.useLegacyDialogs) { + LegacySaveDialog.create(defaultEncoding = currentEncoding, callback = writeFile) + .show(childFragmentManager, "saveDialogLegacy") + } else { + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "text/*" + // TODO: add a dialog to choose file's mime type + putExtra(Intent.EXTRA_TITLE, ".txt") } - }.show(childFragmentManager, "saveDialog") - } - - /** - * Saves changes in the current file. Doesn't check if the file equals null. - */ - private fun saveChanges() { - writeFile(file!!, editor.text.toString(), currentEncoding) { success -> - if (success) { - setTextChanged(false) - val successMessage = resources.getString(R.string.file_save_success, file!!.name) - Toast.makeText(activity, successMessage, Toast.LENGTH_SHORT).show() - } else - Toast.makeText(activity, R.string.file_save_error, Toast.LENGTH_SHORT).show() + startActivityForResult(intent, SAVE_FILE) } } private fun setTextChanged(changed: Boolean) { hasUnsavedChanges = changed - val selectedTab = mActivity.actionBar!!.selectedTab - val name: String = if (file == null) getString(R.string.new_document) else file!!.name - selectedTab.text = if (changed) "$name*" else name + mActivity.actionBar!!.selectedTab.text = if (changed) "$tabTitle*" else tabTitle } private fun setEncoding(newEncoding: String) { - if (file == null) { + if (uri == null) { currentEncoding = newEncoding return } - if (!hasUnsavedChanges) { - readFile(file!!, newEncoding) { result -> - if (result != null) { - editor.setText(result) + val readFileAgain = { + ReadTask(mActivity.contentResolver, uri!!, newEncoding) { + if (it != null) { + setTextWithProgressDialog(it) currentEncoding = newEncoding - } else { - Toast.makeText(activity, R.string.reading_error, Toast.LENGTH_SHORT).show() - } - } - return + } else Toast.makeText(activity, R.string.reading_error, Toast.LENGTH_SHORT).show() + }.execute() } - ConfirmEncodingChangeDialog.create { change -> - if (change) { - readFile(file!!, newEncoding) { result -> - if (result != null) { - editor.setText(result) - currentEncoding = newEncoding - } else { - Toast.makeText(activity, R.string.reading_error, Toast.LENGTH_SHORT).show() - } - } - } - }.show(childFragmentManager, "confirmEncodingChangeDialog") + if (hasUnsavedChanges) { + ConfirmEncodingChangeDialog.create { + if (it) readFileAgain() + }.show(childFragmentManager, "confirmEncodingChangeDialog") + } else readFileAgain() } /** @@ -209,146 +184,109 @@ class EditorFragment : Fragment() { return@create } - if (file != null) { - saveChanges() + if (uri != null) { + writeFile(uri!!, currentEncoding) // Saving changes mActivity.closeTab(selectedTab) return@create } - SaveDialog.create { file, encoding -> - writeFile(file, editor.text.toString(), encoding) { success -> - if (success) { - val successMessage = resources.getString(R.string.file_save_success, file.name) - Toast.makeText(activity, successMessage, Toast.LENGTH_SHORT).show() - LastFilesMaster.add(file) - mActivity.closeTab(selectedTab) - } else { - Toast.makeText(activity, R.string.file_save_error, Toast.LENGTH_SHORT).show() - } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT || Settings.useLegacyDialogs) { + LegacySaveDialog.create(defaultEncoding = currentEncoding, callback = writeFileAndCloseTab) + .show(childFragmentManager, "saveDialogLegacy") + } else { + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "text/*" + putExtra(Intent.EXTRA_TITLE, ".txt") } - }.show(childFragmentManager, "saveDialog") + startActivityForResult(intent, SAVE_FILE_AND_CLOSE) + } }.show(childFragmentManager, "closeTabDialog") } - fun setEditorTextSize(size: Float) { - editor.textSize = size + override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { + if (resultCode != Activity.RESULT_OK) return + resultData?.data?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + takePersistablePermissions(mActivity, it) + } + when (requestCode) { + SAVE_FILE -> writeFile(it, currentEncoding) + SAVE_FILE_AND_CLOSE -> writeFileAndCloseTab(it, currentEncoding) + else -> return@let + } + } } - /** - * Sets a specified text to editor and shows a progress dialog while it is being set. - * @param text Text to set - */ - private fun setTextWithProgressDialog(text: CharSequence?) { - val dialog = LoadingDialog() - dialog.show(childFragmentManager, "loadingDialog") - editor.setText(text) - dialog.dismiss() + private val writeFile: (Uri, String) -> Unit = { uri, encoding -> + WriteTask(mActivity.contentResolver, uri, editor.text.toString(), encoding) { + onFileWritten(uri, encoding, it) + }.execute() } - private lateinit var fileToRead: File - private lateinit var encodingForReading: String - private lateinit var readCallback: (String) -> Unit - - /** - * Asynchronously reads a specified file into a string and returns it via a callback. - * Requests reading permission. Shows a LoadingDialog in the process. - * - * @param file File to read - * @param encoding Encoding to use for decoding of the file - * @param callback Defines what to do with the results of the operation - */ - private fun readFile(file: File, encoding: String, callback: (String) -> Unit) { - if (ContextCompat.checkSelfPermission(activity!!, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - Log.d("RTLnotepad", "No read permission, requesting...") - // saving data to use in onRequestPermissionsResult() - fileToRead = file - encodingForReading = encoding - readCallback = callback - requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), READ_PERMISSION_CODE) - return + private val writeFileAndCloseTab: (Uri, String) -> Unit = { uri, encoding -> + WriteTask(mActivity.contentResolver, uri, editor.text.toString(), encoding) { + onFileWritten(uri, encoding, it) + if (it) with (mActivity) { + closeTab(actionBar!!.selectedTab) + } } - val dialog = LoadingDialog() - dialog.show(childFragmentManager, "loadingDialog") - val task = ReadTask(file, encoding) { result -> - dialog.dismiss() - callback.invoke(result) + } + + private val onFileWritten: (Uri, String, Boolean) -> Unit = { uri, encoding, successfully -> + if (successfully) { + this.uri = uri + this.tabTitle = getFileName(mActivity, uri)!! + this.currentEncoding = encoding + setTextChanged(false) + val successMessage = resources.getString(R.string.file_save_success, tabTitle) + Toast.makeText(context, successMessage, Toast.LENGTH_SHORT).show() + addToLastFiles(uri) + } else { + Toast.makeText(context, R.string.file_save_error, Toast.LENGTH_SHORT).show() } - task.execute() } - private lateinit var fileToWrite: File - private lateinit var textToWrite: String - private lateinit var encodingForWriting: String - private lateinit var writeCallback: (Boolean) -> Unit + fun setEditorTextSize(size: Float) { + editor.textSize = size + } /** - * Asynchronously writes a file to the disk. Returns the status (success/failure) via - * a callback. Requests writing permission. Shows a LoadingDialog in the process. - * - * @param file File to write into - * @param text Text to write into the file - * @param encoding Encoding to use - * @param callback Defines what to do after the operation + * Sets a specified text to editor and shows a progress dialog while it is being set. + * @param text Text to set */ - private fun writeFile(file: File, text: String, encoding: String, callback: (Boolean) -> Unit) { - if (ContextCompat.checkSelfPermission(activity!!, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - Log.d("RTLnotepad", "No write permission, requesting...") - // saving data to use in onRequestPermissionsResult() - fileToWrite = file - textToWrite = text - encodingForWriting = encoding - writeCallback = callback - requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), WRITE_PERMISSION_CODE) - return - } - val dialog = LoadingDialog() - dialog.show(childFragmentManager, "loadingDialog") - val task = WriteTask(file, text, encoding) { success -> - dialog.dismiss() - callback.invoke(success) - } - task.execute() + private fun setTextWithProgressDialog(text: CharSequence) { + /*Log.d("EditorFragment", "Inside setTextWithProgressDialog") + SetTextTask(mActivity, childFragmentManager, editor, text) { + Log.d("EditorFragment", "inside callback") + editor = it.apply { + textSize = Settings.textSize.toFloat() + addTextChangedListener(getTextWatcher()) + } + }.execute() + Log.d("EditorFragment", "New Thread must've been started")*/ + editor.setText(text) // TODO } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - when (requestCode) { - READ_PERMISSION_CODE -> tryReadingFileAgain() - WRITE_PERMISSION_CODE -> tryWritingFileAgain() + private fun getTextWatcher(): TextWatcher { + return object: TextWatcher { + override fun beforeTextChanged(p1: CharSequence, p2: Int, p3: Int, p4: Int) {} + override fun onTextChanged(p1: CharSequence, p2: Int, p3: Int, p4: Int) {} + override fun afterTextChanged(p1: Editable) { + if (!ignoreNextTextChange) setTextChanged(true) + else ignoreNextTextChange = false } - } else { - val dialog = PermissionRequestDialog() - val args = Bundle() - args.putInt(PermissionRequestDialog.TYPE, requestCode) - dialog.arguments = args - dialog.show(childFragmentManager, "permissionRequestDialog") } } - fun tryReadingFileAgain() { - readFile(fileToRead, encodingForReading, readCallback) - } - - fun tryWritingFileAgain() { - writeFile(fileToWrite, textToWrite, encodingForWriting, writeCallback) - } - companion object { - internal const val ARGUMENT_FILE_PATH = "filePath" // will be removed once ViewModel is separated from the fragment - //private val BUNDLE_FILE = "file" - //private val BUNDLE_TAG = "tag" - //private val BUNDLE_CURRENT_ENCODING = "currentEncoding" - //private val BUNDLE_HAS_UNSAVED_CHANGES = "hasUnsavedChanges" + internal const val ARGUMENT_URI = "URI" - const val READ_PERMISSION_CODE = 0 - const val WRITE_PERMISSION_CODE = 1 + // Request codes + const val SAVE_FILE = 1 + const val SAVE_FILE_AND_CLOSE = 2 } -} - -/* TODO: if the process was killed while a dialogFragment was open, dismiss the dialog -* we might pass null to super.onCreate(Bundle savedState) and handle everything ourselves. -* in such a case the dialog fragments will not be retained - */ \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/ua/leonidius/rtlnotepad/MainActivity.kt b/app/src/main/java/ua/leonidius/rtlnotepad/MainActivity.kt index a60ea49..ff55eb4 100644 --- a/app/src/main/java/ua/leonidius/rtlnotepad/MainActivity.kt +++ b/app/src/main/java/ua/leonidius/rtlnotepad/MainActivity.kt @@ -1,136 +1,115 @@ package ua.leonidius.rtlnotepad import android.app.ActionBar -import android.content.Context -import android.content.SharedPreferences +import android.content.Intent import android.net.Uri +import android.os.Build import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.webkit.MimeTypeMap import android.widget.LinearLayout import androidx.fragment.app.FragmentActivity -import ua.leonidius.navdialogs.OpenDialog +import ua.leonidius.navdialogs.LegacyOpenDialog import ua.leonidius.rtlnotepad.dialogs.ExitDialog import ua.leonidius.rtlnotepad.dialogs.LastFilesDialog +import ua.leonidius.rtlnotepad.dialogs.LoadingDialog import ua.leonidius.rtlnotepad.dialogs.WrongFileTypeDialog -import ua.leonidius.rtlnotepad.utils.LastFilesMaster -import java.io.File +import ua.leonidius.rtlnotepad.utils.addToLastFiles +import ua.leonidius.rtlnotepad.utils.getFileName +import ua.leonidius.rtlnotepad.utils.takePersistablePermissions import java.util.* class MainActivity : FragmentActivity() { - //private PlaceholderFragment placeholderFragment; - lateinit var pref: SharedPreferences - - internal val SIZE_SMALL = 14 - internal val SIZE_MEDIUM = 18 - internal val SIZE_LARGE = 22 - internal val PREF_TEXT_SIZE = "textSize" - private val PREF_THEME = "theme" - private val PREF_THEME_LIGHT = "light" - private val PREF_THEME_DARK = "dark" - - private val BUNDLE_SELECTED_TAB_INDEX = "selectedTabIndex" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) instance = this - pref = getPreferences(Context.MODE_PRIVATE) // Applying dark theme if chosen - if (pref.getString(PREF_THEME, PREF_THEME_LIGHT) == PREF_THEME_DARK) { + if (Settings.theme == Settings.PREF_THEME_DARK) { setTheme(R.style.Leonidius_Dark) } setContentView(LinearLayout(this)) // Setting up tab navigation - val actionBar = actionBar actionBar!!.navigationMode = ActionBar.NAVIGATION_MODE_TABS - actionBar.setDisplayShowTitleEnabled(false) - - LastFilesMaster.initSlots(this) + actionBar!!.setDisplayShowTitleEnabled(false) if (savedInstanceState == null) { // Cold start + intent.data?.let { addTab(it) } addPlaceholderFragmentIfNeeded() - // Opening a file from intent - if (intent.data != null) { - val path = intent.data!!.schemeSpecificPart - if (path != null) addTab(File(path)) - } - } else { // Restoring tabs after activity recreation - val tabs = lastCustomNonConfigurationInstance as LinkedHashMap? - if (tabs!!.size != 0) { - val selectedTabIndex = savedInstanceState.getInt(BUNDLE_SELECTED_TAB_INDEX, -1) - for (fragmentTag in tabs.keys) { - val tab = tabs[fragmentTag] - //tab.setTag(getFragmentManager().findFragmentByTag(fragmentTag)); - // we don't change the fragment held in 'tag', because it is not destroyed - getActionBar()!!.addTab(tab) - if (tab!!.position == selectedTabIndex) getActionBar()!!.selectTab(tab) - // they reset fragment tags on recreate? + return + } + + // Restoring tabs after activity recreation + val tabs = lastCustomNonConfigurationInstance as LinkedHashMap + if (tabs.size != 0) { + val selectedTabIndex = savedInstanceState.getInt(BUNDLE_SELECTED_TAB_INDEX, -1) + for (tab in tabs.values) { + with (actionBar!!) { + addTab(tab) + if (tab.position == selectedTabIndex) selectTab(tab) } } } } /** - * Opens a file in a new tab. - * - * @param file File to open + * Opens the file with the given Uri in a new tab */ - private fun addTab(file: File) { - removePlaceholderFragmentIfNeeded() - - // If the file is already opened, switching to the file's tab - // TODO: INSTEAD OF SWITCHING, PROMPT USER TO CHOOSE IF HE WANTS TO ADD A ANOTHER TAB WITH THAT FILE - val fileTab = getFileTab(file) - if (fileTab != null) { - actionBar!!.selectTab(fileTab) + private fun addTab(uri: Uri) { + // Checking if the file is already opened + getFileTab(uri)?.also { + actionBar!!.selectTab(it) return } - // Creating a new tab - val actionBar = actionBar - val tab = actionBar!!.newTab() - tab.text = file.name + removePlaceholderFragmentIfNeeded() + + // Getting the name of the file + val displayName = getFileName(applicationContext, uri) - val fragment = EditorFragment() - val arguments = Bundle() - arguments.putString(EditorFragment.ARGUMENT_FILE_PATH, file.path) - fragment.arguments = arguments - fragment.retainInstance = true - tab.tag = fragment + // Creating a new fragment + val fragment = EditorFragment().apply { + arguments = Bundle().also { it.putParcelable(EditorFragment.ARGUMENT_URI, uri) } + retainInstance = true + } - tab.setTabListener(EditorTabListener()) - actionBar.addTab(tab) + // Creating a new tab + val tab = actionBar!!.newTab().apply { + text = displayName + tag = fragment + setTabListener(EditorTabListener()) + } - // Selecting the tab - actionBar.selectTab(tab) + with (actionBar!!) { + addTab(tab) + selectTab(tab) + } - LastFilesMaster.add(file) + addToLastFiles(uri) } /** * Adds a tab with a blank editor for a new file */ private fun addTab() { - // Detaching noEditorFragment removePlaceholderFragmentIfNeeded() - val actionBar = actionBar - val tab = actionBar!!.newTab() - tab.setText(R.string.new_document) + val fragment = EditorFragment().apply { retainInstance = true } - val fragment = EditorFragment() - fragment.retainInstance = true - tab.tag = fragment - - tab.setTabListener(EditorTabListener()) - actionBar.addTab(tab) + val tab = actionBar!!.newTab().apply { + setText(R.string.new_document) + tag = fragment + setTabListener(EditorTabListener()) + } - // Selecting the tab - actionBar.selectTab(tab) + with (actionBar!!) { + addTab(tab) + selectTab(tab) + } } /** @@ -145,15 +124,18 @@ class MainActivity : FragmentActivity() { } override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.options_new, menu) - when (pref.getString(PREF_THEME, PREF_THEME_LIGHT)) { - PREF_THEME_LIGHT -> menu.findItem(R.id.options_theme_light).isChecked = true - PREF_THEME_DARK -> menu.findItem(R.id.options_theme_dark).isChecked = true + menuInflater.inflate(R.menu.options_main, menu) + when (Settings.theme) { + Settings.PREF_THEME_LIGHT -> menu.findItem(R.id.options_theme_light).isChecked = true + Settings.PREF_THEME_DARK -> menu.findItem(R.id.options_theme_dark).isChecked = true } - when (pref.getInt(PREF_TEXT_SIZE, SIZE_MEDIUM)) { - SIZE_SMALL -> menu.findItem(R.id.options_textSize_small).isChecked = true - SIZE_MEDIUM -> menu.findItem(R.id.options_textSize_medium).isChecked = true - SIZE_LARGE -> menu.findItem(R.id.options_textSize_large).isChecked = true + when (Settings.textSize) { + Settings.SIZE_SMALL -> menu.findItem(R.id.options_textSize_small).isChecked = true + Settings.SIZE_MEDIUM -> menu.findItem(R.id.options_textSize_medium).isChecked = true + Settings.SIZE_LARGE -> menu.findItem(R.id.options_textSize_large).isChecked = true + } + if (Settings.useLegacyDialogs) { + menu.findItem(R.id.options_useLegacyDialogs).isChecked = true } return super.onCreateOptionsMenu(menu) } @@ -161,22 +143,7 @@ class MainActivity : FragmentActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.options_open -> { - // TODO: error when rotating screen on OpenDialog and then - // refusing to rewrite when prompted, and then rotating OpenDialog - lateinit var dialog : OpenDialog - dialog = OpenDialog.create { file : File -> - if (!isText(file)) { - WrongFileTypeDialog.create { - if (it) instance.addTab(file) - else dialog.show(instance.supportFragmentManager, "openDialog") - }.show(instance.supportFragmentManager, "WFTDialog") - } else { - // we use 'instance' because otherwise it adds a new - // fragment to the old activity after orientation change - instance.addTab(file) - } - } - dialog.show(supportFragmentManager, "openDialog") + openFile() return true } R.id.options_new -> { @@ -184,42 +151,89 @@ class MainActivity : FragmentActivity() { return true } R.id.options_theme_light -> { - setThemeNow(PREF_THEME_LIGHT, item) + setThemeNow(Settings.PREF_THEME_LIGHT, item) return true } R.id.options_theme_dark -> { - setThemeNow(PREF_THEME_DARK, item) + setThemeNow(Settings.PREF_THEME_DARK, item) return true } R.id.options_last_files -> { LastFilesDialog.create { - instance.addTab(File(it)) + instance.addTab(it) }.show(supportFragmentManager, "lastFilesDialog") return true } R.id.options_textSize_small -> { - setTextSize(SIZE_SMALL) + setTextSize(Settings.SIZE_SMALL) item.isChecked = true return true } R.id.options_textSize_medium -> { - setTextSize(SIZE_MEDIUM) + setTextSize(Settings.SIZE_MEDIUM) item.isChecked = true return true } R.id.options_textSize_large -> { - setTextSize(SIZE_LARGE) + setTextSize(Settings.SIZE_LARGE) item.isChecked = true return true } - }/*case R.id.options_test: - Intent i = new Intent(); - i.setClass(this, TestingActivity.class); - startActivity(i); - return true;*/ + R.id.options_loadingDialog -> { + LoadingDialog().show(supportFragmentManager, "loadingDialog") + return true + } + R.id.options_useLegacyDialogs -> { + item.isChecked = !item.isChecked + Settings.useLegacyDialogs = item.isChecked + return true + } + } return super.onOptionsItemSelected(item) } + private fun openFile() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT || Settings.useLegacyDialogs) { + lateinit var dialog: LegacyOpenDialog + dialog = LegacyOpenDialog.create { uri -> + if (!isText(uri)) { + WrongFileTypeDialog.create { + if (it) instance.addTab(uri) + else dialog.show(instance.supportFragmentManager, "openDialog") + }.show(instance.supportFragmentManager, "WFTDialog") + } else { + // we use 'instance' because otherwise it adds a new + // fragment to the old activity after orientation change + instance.addTab(uri) + } + } + dialog.show(supportFragmentManager, "openDialog") + return + } + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "*/*" + val mimeTypes = resources.getStringArray(R.array.mimeTypes) + putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) + } + startActivityForResult(intent, PICK_TEXT_FILE) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { + super.onActivityResult(requestCode, resultCode, resultData) + if (resultCode != RESULT_OK) return + when (requestCode) { + PICK_TEXT_FILE -> { + resultData?.data?.also { uri -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + takePersistablePermissions(this, uri) + } + addTab(uri) + } + } + } + } + override fun onBackPressed() { for (i in 0 until actionBar!!.tabCount) { val tab = actionBar!!.getTabAt(i) @@ -237,17 +251,13 @@ class MainActivity : FragmentActivity() { /** * Finds a tab in which the specified file is opened. * - * @param file File which is opened in the tab we are looking for + * @param uriToFind File which is opened in the tab we are looking for * @return ActionBar.Tab in which the specified file is opened, if such a tab exists, null otherwise */ - private fun getFileTab(file: File): ActionBar.Tab? { - // Iterating over tabs + private fun getFileTab(uriToFind: Uri) : ActionBar.Tab? { for (i in 0 until actionBar!!.tabCount) { val tab = actionBar!!.getTabAt(i) - val tabFragment = tab.tag as EditorFragment - if (tabFragment.file != null && tabFragment.file == file) { - return tab - } + if ((tab.tag as EditorFragment).run { uri != null && uri!! == uriToFind }) return tab } return null } @@ -257,15 +267,12 @@ class MainActivity : FragmentActivity() { */ private fun addPlaceholderFragmentIfNeeded() { if (actionBar!!.tabCount == 0) { - var placeholder = supportFragmentManager.findFragmentByTag(PlaceholderFragment.TAG) - if (placeholder == null) { - placeholder = PlaceholderFragment() + val placeholder = supportFragmentManager.findFragmentByTag(PlaceholderFragment.TAG) ?: PlaceholderFragment() + with (supportFragmentManager.beginTransaction()) { + if (!placeholder.isAdded) add(android.R.id.content, placeholder, PlaceholderFragment.TAG) + attach(placeholder) + commitAllowingStateLoss() } - - val sft = supportFragmentManager.beginTransaction() - if (!placeholder.isAdded) sft.add(android.R.id.content, placeholder, PlaceholderFragment.TAG) - sft.attach(placeholder) - sft.commitAllowingStateLoss() } } @@ -274,11 +281,13 @@ class MainActivity : FragmentActivity() { */ private fun removePlaceholderFragmentIfNeeded() { if (actionBar!!.tabCount == 0) { - val placeholder = supportFragmentManager.findFragmentByTag(PlaceholderFragment.TAG) - if (placeholder != null && !placeholder.isDetached) { - val sft = supportFragmentManager.beginTransaction() - sft.detach(placeholder) - sft.commitAllowingStateLoss() + supportFragmentManager.findFragmentByTag(PlaceholderFragment.TAG).let { + if (it != null && !it.isDetached) { + with (supportFragmentManager.beginTransaction()) { + detach(it) + commitAllowingStateLoss() + } + } } } } @@ -293,9 +302,7 @@ class MainActivity : FragmentActivity() { private fun setThemeNow(theme: String, item: MenuItem) { if (!item.isChecked) { item.isChecked = true - val prefEdit = pref.edit() - prefEdit.putString(PREF_THEME, theme) - prefEdit.apply() + Settings.theme = theme recreate() } } @@ -306,8 +313,6 @@ class MainActivity : FragmentActivity() { if (actionBar!!.tabCount > 0) { outState.putInt(BUNDLE_SELECTED_TAB_INDEX, actionBar!!.selectedTab.position) } - - LastFilesMaster.saveSlots(this) } /** @@ -317,20 +322,14 @@ class MainActivity : FragmentActivity() { */ private fun setTextSize(size: Int) { for (i in 0 until actionBar!!.tabCount) { - val tab = actionBar!!.getTabAt(i) - val fragment = tab.tag as EditorFragment - fragment.setEditorTextSize(size.toFloat()) + (actionBar!!.getTabAt(i).tag as EditorFragment).setEditorTextSize(size.toFloat()) } - val prefEditor = pref.edit() - prefEditor.putInt(PREF_TEXT_SIZE, size) - prefEditor.apply() - // TODO: Consider recreating the activity at that point to avoid iterating through tabs + Settings.textSize = size } override fun onRetainCustomNonConfigurationInstance(): Any? { - val tabCount = actionBar!!.tabCount val tabs = LinkedHashMap() - for (i in 0 until tabCount) { + for (i in 0 until actionBar!!.tabCount) { val tab = actionBar!!.getTabAt(i) tabs[(tab.tag as EditorFragment).tag!!] = tab } @@ -339,16 +338,18 @@ class MainActivity : FragmentActivity() { companion object { - /** - * @return An instance of MainActivity - */ - lateinit var instance: MainActivity + // Request codes + const val PICK_TEXT_FILE = 0 + + private const val BUNDLE_SELECTED_TAB_INDEX = "selectedTabIndex" + + lateinit var instance: MainActivity - private fun isText(file: File) : Boolean { + private fun isText(uri: Uri): Boolean { return try { - MimeTypeMap.getSingleton().getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(file).toString()))!!.split("/")[0] == "text"; + MimeTypeMap.getSingleton().getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(uri.toString()))!!.split("/")[0] == "text"; } catch (e: Exception) { - false; + false } } diff --git a/app/src/main/java/ua/leonidius/rtlnotepad/MyApplication.kt b/app/src/main/java/ua/leonidius/rtlnotepad/MyApplication.kt index b66d5b2..827a7ac 100644 --- a/app/src/main/java/ua/leonidius/rtlnotepad/MyApplication.kt +++ b/app/src/main/java/ua/leonidius/rtlnotepad/MyApplication.kt @@ -1,12 +1,14 @@ package ua.leonidius.rtlnotepad import android.app.Application +import com.chibatching.kotpref.Kotpref import ru.elifantiev.android.roboerrorreporter.RoboErrorReporter class MyApplication : Application() { override fun onCreate() { RoboErrorReporter.bindReporter(this) + Kotpref.init(this) super.onCreate() } diff --git a/app/src/main/java/ua/leonidius/rtlnotepad/Settings.kt b/app/src/main/java/ua/leonidius/rtlnotepad/Settings.kt new file mode 100644 index 0000000..6e922a6 --- /dev/null +++ b/app/src/main/java/ua/leonidius/rtlnotepad/Settings.kt @@ -0,0 +1,19 @@ +package ua.leonidius.rtlnotepad + +import com.chibatching.kotpref.KotprefModel +import java.util.* + +object Settings : KotprefModel() { + + const val SIZE_SMALL = 14 + const val SIZE_MEDIUM = 18 + const val SIZE_LARGE = 22 + const val PREF_THEME_LIGHT = "light" + const val PREF_THEME_DARK = "dark" + + var useLegacyDialogs by booleanPref(false) + var theme by stringPref(PREF_THEME_LIGHT) + var textSize by intPref(SIZE_MEDIUM) + val lastFiles by stringSetPref { LinkedHashSet() } + +} \ No newline at end of file diff --git a/app/src/main/java/ua/leonidius/rtlnotepad/dialogs/LastFilesDialog.kt b/app/src/main/java/ua/leonidius/rtlnotepad/dialogs/LastFilesDialog.kt index df41f1e..7294e1f 100644 --- a/app/src/main/java/ua/leonidius/rtlnotepad/dialogs/LastFilesDialog.kt +++ b/app/src/main/java/ua/leonidius/rtlnotepad/dialogs/LastFilesDialog.kt @@ -1,29 +1,26 @@ package ua.leonidius.rtlnotepad.dialogs -import android.app.Activity import android.app.AlertDialog import android.app.Dialog +import android.net.Uri import android.os.Bundle import android.view.View import android.widget.AdapterView import android.widget.ListView -import android.widget.TextView import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import ua.leonidius.rtlnotepad.R -import ua.leonidius.rtlnotepad.utils.LastFilesMaster +import ua.leonidius.rtlnotepad.utils.LastFilesAdapter class LastFilesDialog : BaseDialog(), AdapterView.OnItemClickListener { - private lateinit var viewModel : Model + private lateinit var viewModel: Model companion object { - fun create(callback: (String) -> Unit) : LastFilesDialog { - val dialog = LastFilesDialog() - dialog.initializerFunction = { - dialog.getViewModel().callback = callback + fun create(callback: (Uri) -> Unit): LastFilesDialog { + return LastFilesDialog().apply { + initializerFunction = { getViewModel().callback = callback } } - return dialog } } @@ -32,13 +29,13 @@ class LastFilesDialog : BaseDialog(), AdapterView.OnItemClickListener { adb.setTitle(R.string.last_files) val lastFilesList = ListView(activity) lastFilesList.onItemClickListener = this - lastFilesList.adapter = LastFilesMaster.getAdapter(activity as Activity) + lastFilesList.adapter = LastFilesAdapter(activity!!) adb.setView(lastFilesList) return adb.create() } override fun onItemClick(p1: AdapterView<*>, item: View, p3: Int, p4: Long) { - getViewModel().callback((item.findViewById(R.id.lastFilesItem_path) as TextView).text.toString()) + getViewModel().callback(item.tag as Uri) dialog?.cancel() } @@ -50,7 +47,7 @@ class LastFilesDialog : BaseDialog(), AdapterView.OnItemClickListener { } class Model : ViewModel() { - internal lateinit var callback: ((String) -> Unit) + internal lateinit var callback: ((Uri) -> Unit) } } \ No newline at end of file diff --git a/app/src/main/java/ua/leonidius/rtlnotepad/dialogs/LoadingDialog.kt b/app/src/main/java/ua/leonidius/rtlnotepad/dialogs/LoadingDialog.kt index 2c8c68d..aa04216 100644 --- a/app/src/main/java/ua/leonidius/rtlnotepad/dialogs/LoadingDialog.kt +++ b/app/src/main/java/ua/leonidius/rtlnotepad/dialogs/LoadingDialog.kt @@ -3,14 +3,16 @@ package ua.leonidius.rtlnotepad.dialogs import android.app.AlertDialog import android.app.Dialog import android.os.Bundle +import android.util.Log import androidx.fragment.app.DialogFragment import ua.leonidius.rtlnotepad.R class LoadingDialog : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + Log.d("LoadingDialog", "Inside onCreateDialog") val pdb = AlertDialog.Builder(activity) - // TODO("Show progress bar") + pdb.setTitle(R.string.processing) pdb.setMessage(R.string.processing) pdb.setCancelable(false) return pdb.create() diff --git a/app/src/main/java/ua/leonidius/rtlnotepad/dialogs/PermissionRequestDialog.kt b/app/src/main/java/ua/leonidius/rtlnotepad/dialogs/PermissionRequestDialog.kt deleted file mode 100644 index b6b7bd2..0000000 --- a/app/src/main/java/ua/leonidius/rtlnotepad/dialogs/PermissionRequestDialog.kt +++ /dev/null @@ -1,44 +0,0 @@ -package ua.leonidius.rtlnotepad.dialogs - -import android.app.AlertDialog -import android.app.Dialog -import android.content.DialogInterface -import android.os.Bundle -import androidx.fragment.app.DialogFragment -import ua.leonidius.rtlnotepad.EditorFragment -import ua.leonidius.rtlnotepad.R - -class PermissionRequestDialog : DialogFragment(), DialogInterface.OnClickListener { - - private var type: Int = 0 - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - type = arguments!!.getInt(TYPE) - val adb = AlertDialog.Builder(context) - - if (type == EditorFragment.READ_PERMISSION_CODE) { - adb.setMessage(R.string.grant_read_permissions) - } else { - adb.setMessage(R.string.grant_write_permissions) - } - - adb.setPositiveButton(android.R.string.yes, this) - adb.setNegativeButton(android.R.string.no, this) - - return adb.create() - } - - override fun onClick(dialog: DialogInterface, which: Int) { - if (which == AlertDialog.BUTTON_POSITIVE) { - if (type == EditorFragment.READ_PERMISSION_CODE) { - (parentFragment as EditorFragment).tryReadingFileAgain() - } else - (parentFragment as EditorFragment).tryWritingFileAgain() - } - } - - companion object { - var TYPE = "type" - } - -} \ No newline at end of file diff --git a/app/src/main/java/ua/leonidius/rtlnotepad/utils/EncodingAdapter.kt b/app/src/main/java/ua/leonidius/rtlnotepad/utils/EncodingAdapter.kt index 9034cd9..011e115 100644 --- a/app/src/main/java/ua/leonidius/rtlnotepad/utils/EncodingAdapter.kt +++ b/app/src/main/java/ua/leonidius/rtlnotepad/utils/EncodingAdapter.kt @@ -12,13 +12,10 @@ import ua.leonidius.rtlnotepad.R class EncodingAdapter(internal var context: Context, private var data: Array, var selectedEncoding: String) : ArrayAdapter(context, R.layout.encoding_list_item, R.id.radio_text, data) { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - var view = convertView + val view : View = convertView ?: + LayoutInflater.from(context).inflate(R.layout.encoding_list_item, parent, false) - if (view == null) { - view = LayoutInflater.from(context).inflate(R.layout.encoding_list_item, parent, false) - } - - val tv = view!!.findViewById(R.id.radio_text) + val tv = view.findViewById(R.id.radio_text) tv.text = data[position] val r = view.findViewById(R.id.radio_button) diff --git a/app/src/main/java/ua/leonidius/rtlnotepad/utils/FileHelper.kt b/app/src/main/java/ua/leonidius/rtlnotepad/utils/FileHelper.kt new file mode 100644 index 0000000..0dfe3a9 --- /dev/null +++ b/app/src/main/java/ua/leonidius/rtlnotepad/utils/FileHelper.kt @@ -0,0 +1,80 @@ +package ua.leonidius.rtlnotepad.utils + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.provider.DocumentsContract +import android.provider.OpenableColumns + +fun getFileName(context: Context, uri: Uri) : String? { + val contentResolver = context.contentResolver + var output: String? = null + try { + val cursor: Cursor? = contentResolver.query( + uri, null, null, null, null, null) + cursor?.use { + output = if (!it.moveToFirst()) null + else it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + } + } catch (e: SecurityException) { + e.printStackTrace() + return null + } + if (output == null && uri.scheme == "file") { + output = uri.lastPathSegment + } + return output +} + +fun takePersistablePermissions(context: Context, uri: Uri) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + val contentResolver = context.contentResolver + val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + contentResolver.takePersistableUriPermission(uri, takeFlags) + } +} + +fun canWriteFile(context: Context, uri: Uri): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return true + } + + // If no writing permissions... + if (context.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + != PackageManager.PERMISSION_GRANTED) return false; + + val contentResolver = context.contentResolver + try { + val cursor = contentResolver.query(uri, arrayOf(DocumentsContract.Document.COLUMN_FLAGS), + null, null, null) + cursor?.use { + return if (it.moveToFirst() && !it.isNull(0)) { + val flags = cursor.getInt(0) + (flags and DocumentsContract.Document.FLAG_SUPPORTS_WRITE) != 0 + } else false + } + } catch (e: Exception) { + e.printStackTrace() + return false + } + return false +} + +fun fileExists(context: Context, uri: Uri): Boolean { + val resolver = context.contentResolver + try { + val cursor = resolver.query(uri, arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID), + null, null, null) + cursor?.use { + return cursor.count > 0 + } + return false + } catch (e: Exception) { + e.printStackTrace() + return false + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/leonidius/rtlnotepad/utils/LastFilesAdapter.kt b/app/src/main/java/ua/leonidius/rtlnotepad/utils/LastFilesAdapter.kt new file mode 100644 index 0000000..233493c --- /dev/null +++ b/app/src/main/java/ua/leonidius/rtlnotepad/utils/LastFilesAdapter.kt @@ -0,0 +1,44 @@ +package ua.leonidius.rtlnotepad.utils + +import android.content.Context +import android.net.Uri +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.ImageView +import android.widget.TextView +import ua.leonidius.rtlnotepad.R +import ua.leonidius.rtlnotepad.Settings + +class LastFilesAdapter(private val context: Context): BaseAdapter() { + + private val uriList = Settings.lastFiles.toList() + + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { + val view : View = convertView ?: + LayoutInflater.from(context).inflate(R.layout.last_files_item, parent, false) + + val uri : Uri = Uri.parse(uriList[position]) + + view.findViewById(R.id.lastFilesItem_name).text = getFileName(context, uri) + view.findViewById(R.id.lastFilesItem_path).text = uri.path ?: "" + view.findViewById(R.id.lastFilesItem_image).setImageResource(R.drawable.file) + view.tag = uri + + return view + } + + override fun getItem(position: Int): Any { + return uriList[position] + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getCount(): Int { + return uriList.size + } + +} \ No newline at end of file diff --git a/app/src/main/java/ua/leonidius/rtlnotepad/utils/LastFilesMaster.kt b/app/src/main/java/ua/leonidius/rtlnotepad/utils/LastFilesMaster.kt index 030365a..7d37897 100644 --- a/app/src/main/java/ua/leonidius/rtlnotepad/utils/LastFilesMaster.kt +++ b/app/src/main/java/ua/leonidius/rtlnotepad/utils/LastFilesMaster.kt @@ -1,88 +1,16 @@ package ua.leonidius.rtlnotepad.utils -import android.app.Activity -import android.widget.SimpleAdapter -import ua.leonidius.rtlnotepad.MainActivity -import ua.leonidius.rtlnotepad.R -import java.io.File -import java.io.IOException -import java.util.* +import android.net.Uri +import ua.leonidius.rtlnotepad.Settings /** - * This class manages the list of last opened files. + * This file manages the list of last opened files. */ -object LastFilesMaster { - private lateinit var slots: LinkedList // stores files' paths - private val slot_names = arrayOf("slot1", "slot2", "slot3", "slot4", "slot5") - private const val EMPTY = "empty" - private const val SLOTS_SIZE = 5 +private const val SLOTS_SIZE = 5 - fun add(file: File) { - // Checking if file is already on the list and placing it to the top in this case - for (fileInSlot in slots) { - if (fileInSlot === file) { - slots.remove(fileInSlot) - slots.addFirst(file) - return - } - } - - if (slots.size == SLOTS_SIZE) slots.removeLast() - slots.addFirst(file) - } - - // Retrieves recent files saved in Preferences. Called in main activity's onCreate() - fun initSlots(activity: MainActivity) { - slots = LinkedList() - var path: String - var file: File - for (i in 0 until SLOTS_SIZE) { - path = activity.pref.getString(slot_names[i], EMPTY) as String - if (path != EMPTY) { - file = File(path) - if (file.exists()) - slots.add(file) - else - slots.add(null) - } else - slots.add(null) - } +fun addToLastFiles(uri: Uri) { + with (Settings.lastFiles) { + add(uri.toString()) + if (size == SLOTS_SIZE) remove(first()) } - - // Saves recent files to the preferences. Called in main activity's onSaveInstanceState() - fun saveSlots(activity: MainActivity) { - val edit = activity.pref.edit() - for (i in 0 until SLOTS_SIZE) { - if (slots[i] == null || !slots[i]!!.exists()) { - edit.putString(slot_names[i], EMPTY) - continue - } - try { - edit.putString(slot_names[i], slots[i]!!.canonicalPath) - } catch (e: IOException) { - edit.putString(slot_names[i], EMPTY) - } - - } - edit.apply() - } - - // Returns an adapter for last files list for LastFilesDialog - fun getAdapter(activity: Activity): SimpleAdapter { - val data = ArrayList>() - var m: MutableMap - for (file in slots) { - if (file != null) { - m = HashMap() - m["name"] = file.name - m["path"] = file.path - m["icon"] = R.drawable.file - data.add(m) - } - } - val from = arrayOf("name", "icon", "path") - val to = intArrayOf(R.id.lastFilesItem_name, R.id.lastFilesItem_image, R.id.lastFilesItem_path) - return SimpleAdapter(activity, data, R.layout.last_files_item, from, to) - } - } \ No newline at end of file diff --git a/app/src/main/java/ua/leonidius/rtlnotepad/utils/ReadTask.kt b/app/src/main/java/ua/leonidius/rtlnotepad/utils/ReadTask.kt index 3f8e96c..13ffed0 100644 --- a/app/src/main/java/ua/leonidius/rtlnotepad/utils/ReadTask.kt +++ b/app/src/main/java/ua/leonidius/rtlnotepad/utils/ReadTask.kt @@ -1,38 +1,41 @@ package ua.leonidius.rtlnotepad.utils +import android.content.ContentResolver +import android.net.Uri import android.os.AsyncTask -import android.util.Log - import java.io.BufferedReader -import java.io.File -import java.io.FileInputStream +import java.io.IOException import java.io.InputStreamReader /** * Used for asynchronous reading of a file into a string. The string is returned via * a callback. If failed to read the file, the resulting string would be null. */ -class ReadTask(private val file: File, private val encoding: String, private val callback: (String) -> Unit) : AsyncTask() { +class ReadTask(private val contentResolver : ContentResolver, + private val uri: Uri, private val encoding: String, + private val callback: (String?) -> Unit) : AsyncTask() { - override fun doInBackground(vararg params: Void): String? { + override fun doInBackground(vararg params: Void?): String? { try { - val br = BufferedReader(InputStreamReader(FileInputStream(file), encoding)) - val sb = StringBuilder() - while (!isCancelled) { - val readLine = br.readLine() ?: return sb.toString() - if (sb.isNotEmpty()) sb.append("\n") - sb.append(readLine) + val stringBuilder = StringBuilder() + contentResolver.openInputStream(uri)?.use { inputStream -> + BufferedReader(InputStreamReader(inputStream, encoding)).use { reader -> + var line: String? = reader.readLine() + while (line != null && !isCancelled) { + stringBuilder.append(line) + line = reader.readLine() + } + } } - } catch (e: Exception) { - Log.e("ReadTask RTLnotepad", e.message) + return if (isCancelled) null else stringBuilder.toString() + } catch (e: IOException) { + e.printStackTrace() return null } - - return null } - override fun onPostExecute(result: String) { - callback.invoke(result) + override fun onPostExecute(result: String?) { + callback(result) } } \ No newline at end of file diff --git a/app/src/main/java/ua/leonidius/rtlnotepad/utils/SetTextTask.kt b/app/src/main/java/ua/leonidius/rtlnotepad/utils/SetTextTask.kt new file mode 100644 index 0000000..f673e0c --- /dev/null +++ b/app/src/main/java/ua/leonidius/rtlnotepad/utils/SetTextTask.kt @@ -0,0 +1,49 @@ +package ua.leonidius.rtlnotepad.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.os.AsyncTask +import android.util.Log +import android.view.ViewGroup +import android.widget.EditText +import androidx.fragment.app.FragmentManager +import ua.leonidius.rtlnotepad.dialogs.LoadingDialog + +class SetTextTask(private val context: Context, + private val fragMgr: FragmentManager? = null, + private val oldEditText: EditText? = null, + private val text: CharSequence, + private val callback: (EditText) -> Unit): AsyncTask() { + + private lateinit var dialog: LoadingDialog + + override fun onPreExecute() { + Log.d("SetTextTask", "inside onPreExecute()") + fragMgr?.let { + dialog = LoadingDialog() + dialog.show(it, "loadingDialog") + Log.d("SetTextTask", "dialog should've been shown by now") + } + } + + @SuppressLint("WrongThread") + override fun doInBackground(vararg params: Unit?): EditText { + val editText = EditText(context) + editText.setText(text) + return editText + } + + override fun onPostExecute(result: EditText?) { + Log.d("SetTextTask", "Inside onPostExecute()") + oldEditText?.let { + val group = it.parent as ViewGroup + val index = group.indexOfChild(it) + val layoutParams = it.layoutParams + group.removeView(it) + group.addView(result, index, layoutParams) + } + callback(result!!) + if (::dialog.isInitialized) dialog.dismiss() + } + +} \ No newline at end of file diff --git a/app/src/main/java/ua/leonidius/rtlnotepad/utils/WriteTask.kt b/app/src/main/java/ua/leonidius/rtlnotepad/utils/WriteTask.kt index 0ecbb12..5ef1847 100644 --- a/app/src/main/java/ua/leonidius/rtlnotepad/utils/WriteTask.kt +++ b/app/src/main/java/ua/leonidius/rtlnotepad/utils/WriteTask.kt @@ -1,10 +1,9 @@ package ua.leonidius.rtlnotepad.utils +import android.content.ContentResolver +import android.net.Uri import android.os.AsyncTask -import android.util.Log - import java.io.BufferedWriter -import java.io.File import java.io.FileOutputStream import java.io.OutputStreamWriter @@ -12,23 +11,27 @@ import java.io.OutputStreamWriter * Used for asynchronous writing of a string into a file. The result is returned via a callback * (false if failed to write, true otherwise). */ -class WriteTask(private val file: File, private val text: String, private val encoding: String, private val callback: (Boolean) -> Unit) : AsyncTask() { - - override fun doInBackground(vararg voids: Void): Boolean { - return try { - val bw = BufferedWriter(OutputStreamWriter(FileOutputStream(file), encoding)) - bw.write(text) - bw.close() - true - } catch (e: Exception) { - Log.e("WriteTask RTLnotepad", e.message!!) - false - } +class WriteTask(private val contentResolver : ContentResolver, + private val uri: Uri, private val text : String, + private val encoding: String, + private val callback: (Boolean) -> Unit) : AsyncTask() { + override fun doInBackground(vararg params: Void?): Boolean { + return contentResolver.openFileDescriptor(uri, "w")?.use { + try { + BufferedWriter(OutputStreamWriter(FileOutputStream(it.fileDescriptor), encoding)).use { writer -> + writer.write(text) + } + return@use true + } catch (e: Exception) { + e.printStackTrace() + return@use false + } + }!! } - override fun onPostExecute(success: Boolean) { - callback.invoke(success) + override fun onPostExecute(result: Boolean) { + callback(result) } } \ No newline at end of file diff --git a/app/src/main/res/menu/options_new.xml b/app/src/main/res/menu/options_main.xml similarity index 80% rename from app/src/main/res/menu/options_new.xml rename to app/src/main/res/menu/options_main.xml index 46f72d2..906ba13 100644 --- a/app/src/main/res/menu/options_new.xml +++ b/app/src/main/res/menu/options_main.xml @@ -3,10 +3,6 @@ - - + + + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a598963..9ca6886 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -42,4 +42,5 @@ Файл %1$s успешно сохранён. Чтобы продолжить операцию, необходимо предоставить разрешение на чтение из памяти устройства. Продолжить? Чтобы продолжить операцию, необходимо предоставить разрешение на запись в память устройства. Продолжить? + Этот файл доступен только для чтения. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 05a7574..97a18e2 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -42,4 +42,5 @@ Файл %1$s успішно збережено. Щоб продовжити операцію, необхідно надати дозвіл на читання з пам\'яті пристрою. Продовжити? Щоб продовжити операцію, необхідно надати дозвіл на запис до пам\'яті пристрою. Продовжити? + Цей файл є доступним лише для читання. diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..5d30232 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,9 @@ + + + + text/* + application/javascript + application/ecmascript + + text/*,application/javascript,application/ecmascript,application/json + \ 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 7276786..68d744b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,5 +45,6 @@ Successfully saved %1$s. In order to continue the operation, you need to grant read permissions to the app. Continue? In order to continue the operation, you need to grant write permissions to the app. Continue? + This file is read-only. diff --git a/navdialogs/build.gradle b/navdialogs/build.gradle index a3c36ee..12de999 100644 --- a/navdialogs/build.gradle +++ b/navdialogs/build.gradle @@ -5,7 +5,6 @@ apply plugin: 'kotlin-android' android { compileSdkVersion 29 - defaultConfig { minSdkVersion 14 targetSdkVersion 29 @@ -28,8 +27,7 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - // ViewModel and LiveData - implementation "androidx.lifecycle:lifecycle-extensions:2.2.0-beta01" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } repositories { diff --git a/navdialogs/proguard-rules.pro b/navdialogs/proguard-rules.pro index f1b4245..21c39e6 100644 --- a/navdialogs/proguard-rules.pro +++ b/navdialogs/proguard-rules.pro @@ -19,3 +19,7 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +-assumenosideeffects class kotlin.jvm.internal.Intrinsics { + static void checkParameterIsNotNull(java.lang.Object, java.lang.String); +} \ No newline at end of file diff --git a/navdialogs/src/main/java/ua/leonidius/navdialogs/AdapterCreator.kt b/navdialogs/src/main/java/ua/leonidius/navdialogs/AdapterCreator.kt new file mode 100644 index 0000000..978e396 --- /dev/null +++ b/navdialogs/src/main/java/ua/leonidius/navdialogs/AdapterCreator.kt @@ -0,0 +1,32 @@ +package ua.leonidius.navdialogs + +import android.content.Context +import android.widget.SimpleAdapter + +import java.io.File +import java.util.* +import kotlin.Comparator + +fun getFileAdapter(context: Context, directory: File): SimpleAdapter { + val sortedFiles = sortedSetOf(Comparator { file1, file2 -> + if (file1.isDirectory && !file2.isDirectory) -1 + else if (!file1.isDirectory && file2.isDirectory) 1 + else file1.name.toLowerCase().compareTo(file2.name.toLowerCase()) + }, *directory.listFiles()!!) + + val data = ArrayList>() + var map: MutableMap + for (file in sortedFiles) { + map = HashMap() + map["name"] = file.name + if (file.isDirectory) + map["icon"] = R.drawable.folder + else + map["icon"] = R.drawable.file + data.add(map) + } + val from = arrayOf("name", "icon") + val to = intArrayOf(R.id.listItemText, R.id.listItemIcon) + return SimpleAdapter(context, data, R.layout.navdialogs_files_list_item, from, to) +} + diff --git a/navdialogs/src/main/java/ua/leonidius/navdialogs/AdapterFactory.kt b/navdialogs/src/main/java/ua/leonidius/navdialogs/AdapterFactory.kt deleted file mode 100644 index c420ce1..0000000 --- a/navdialogs/src/main/java/ua/leonidius/navdialogs/AdapterFactory.kt +++ /dev/null @@ -1,35 +0,0 @@ -package ua.leonidius.navdialogs - -import android.content.Context -import android.widget.SimpleAdapter - -import java.io.File -import java.util.* -import kotlin.Comparator - -internal object AdapterFactory { - - fun getFileAdapter(context: Context, directory: File): SimpleAdapter { - val sortedFiles = sortedSetOf(Comparator { file1, file2 -> - if (file1.isDirectory && !file2.isDirectory) -1 - else if (!file1.isDirectory && file2.isDirectory) 1 - else file1.name.toLowerCase().compareTo(file2.name.toLowerCase()) - }, *directory.listFiles()!!) - - val data = ArrayList>() - var map: MutableMap - for (file in sortedFiles) { - map = HashMap() - map["name"] = file.name - if (file.isDirectory) - map["icon"] = R.drawable.folder - else - map["icon"] = R.drawable.file - data.add(map) - } - val from = arrayOf("name", "icon") - val to = intArrayOf(R.id.listItemText, R.id.listItemIcon) - return SimpleAdapter(context, data, R.layout.navdialogs_files_list_item, from, to) - } - -} \ No newline at end of file diff --git a/navdialogs/src/main/java/ua/leonidius/navdialogs/OpenDialog.kt b/navdialogs/src/main/java/ua/leonidius/navdialogs/LegacyOpenDialog.kt similarity index 81% rename from navdialogs/src/main/java/ua/leonidius/navdialogs/OpenDialog.kt rename to navdialogs/src/main/java/ua/leonidius/navdialogs/LegacyOpenDialog.kt index 94ff0c9..ad8f956 100644 --- a/navdialogs/src/main/java/ua/leonidius/navdialogs/OpenDialog.kt +++ b/navdialogs/src/main/java/ua/leonidius/navdialogs/LegacyOpenDialog.kt @@ -1,6 +1,7 @@ package ua.leonidius.navdialogs import android.app.Dialog +import android.net.Uri import android.os.Bundle import android.os.Environment import android.view.LayoutInflater @@ -11,14 +12,15 @@ import androidx.lifecycle.ViewModelProvider import java.io.File -class OpenDialog : NavigationDialog() { +class LegacyOpenDialog : NavigationDialog() { private lateinit var viewModel: Model companion object { - fun create(defaultDir: File = Environment.getExternalStorageDirectory(), callback: (File) -> Unit): OpenDialog { - val dialog = OpenDialog() + fun create(defaultDir: File = Environment.getExternalStorageDirectory(), + callback: (Uri) -> Unit): LegacyOpenDialog { + val dialog = LegacyOpenDialog() dialog.initializerFunction = { with (dialog.getViewModel()) { this.currentDir = defaultDir @@ -38,7 +40,7 @@ class OpenDialog : NavigationDialog() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { // TODO: instead of a header put a horizontal layout with "up" and "create folder" options - return inflater.inflate(R.layout.navdialogs_dialog_open, container) + return inflater.inflate(R.layout.nav_dialogs_open, container) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -48,7 +50,7 @@ class OpenDialog : NavigationDialog() { } override fun onFileClick(file: File) { - getViewModel().callback(file) + getViewModel().callback(Uri.fromFile(file)) dialog!!.dismiss() } @@ -60,7 +62,7 @@ class OpenDialog : NavigationDialog() { } class Model : NavigationDialog.NavDialogViewModel() { - internal lateinit var callback: ((File) -> Unit) + internal lateinit var callback: ((Uri) -> Unit) } } \ No newline at end of file diff --git a/navdialogs/src/main/java/ua/leonidius/navdialogs/SaveDialog.kt b/navdialogs/src/main/java/ua/leonidius/navdialogs/LegacySaveDialog.kt similarity index 89% rename from navdialogs/src/main/java/ua/leonidius/navdialogs/SaveDialog.kt rename to navdialogs/src/main/java/ua/leonidius/navdialogs/LegacySaveDialog.kt index 12e7210..b6194e5 100644 --- a/navdialogs/src/main/java/ua/leonidius/navdialogs/SaveDialog.kt +++ b/navdialogs/src/main/java/ua/leonidius/navdialogs/LegacySaveDialog.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.app.AlertDialog import android.app.Dialog import android.content.DialogInterface +import android.net.Uri import android.os.Bundle import android.os.Environment import android.util.Log @@ -17,7 +18,7 @@ import java.io.File import java.nio.charset.Charset import java.util.* -class SaveDialog : NavigationDialog(), DialogInterface.OnClickListener { +class LegacySaveDialog : NavigationDialog(), DialogInterface.OnClickListener { private lateinit var nameField: EditText private lateinit var encodingSpinner: Spinner @@ -27,8 +28,8 @@ class SaveDialog : NavigationDialog(), DialogInterface.OnClickListener { fun create(defaultName: String = ".txt", defaultEncoding: String = "UTF-8", defaultDirectory: File = Environment.getExternalStorageDirectory(), - callback: (File, String) -> Unit): SaveDialog { - val dialog = SaveDialog() + callback: (Uri, String) -> Unit): LegacySaveDialog { + val dialog = LegacySaveDialog() dialog.initializerFunction = { with(dialog.getViewModel()) { this.fileName = defaultName @@ -48,7 +49,7 @@ class SaveDialog : NavigationDialog(), DialogInterface.OnClickListener { adb.setPositiveButton(android.R.string.ok, this) val inflater = LayoutInflater.from(context) - val dialogView = inflater.inflate(R.layout.navdialogs_dialog_save_as, null, false) + val dialogView = inflater.inflate(R.layout.nav_dialogs_save_as_legacy, null, false) initView(dialogView) adb.setView(dialogView) @@ -93,15 +94,15 @@ class SaveDialog : NavigationDialog(), DialogInterface.OnClickListener { val encoding = getViewModel().getAvailableEncodings()[encodingSpinner.selectedItemPosition] if (!file.exists()) { - getViewModel().callback(file, encoding) + getViewModel().callback(Uri.fromFile(file), encoding) return } lateinit var rewriteDialog : RewriteDialog rewriteDialog = RewriteDialog.create { rewrite -> if (rewrite) - getViewModel().callback(file, encoding) - else + getViewModel().callback(Uri.fromFile(file), encoding) + else { // crash when activity was recreated after orientation change // technically we can use MainActivity.instance here... if (rewriteDialog.activity != null) { @@ -109,6 +110,7 @@ class SaveDialog : NavigationDialog(), DialogInterface.OnClickListener { } else { Log.d("SaveDialog", "There is no reference to the new activity, so the SaveDialog couldn't be shown") } + } rewriteDialog.dialog?.cancel() } rewriteDialog.show(parentFragmentManager, "rewriteDialog") @@ -122,7 +124,7 @@ class SaveDialog : NavigationDialog(), DialogInterface.OnClickListener { } class Model : NavigationDialog.NavDialogViewModel() { - internal lateinit var callback: ((File, String) -> Unit) + internal lateinit var callback: ((Uri, String) -> Unit) internal lateinit var fileName: String internal lateinit var currentEncoding: String diff --git a/navdialogs/src/main/java/ua/leonidius/navdialogs/NavigationDialog.kt b/navdialogs/src/main/java/ua/leonidius/navdialogs/NavigationDialog.kt index 2fd0398..fee195e 100644 --- a/navdialogs/src/main/java/ua/leonidius/navdialogs/NavigationDialog.kt +++ b/navdialogs/src/main/java/ua/leonidius/navdialogs/NavigationDialog.kt @@ -67,7 +67,7 @@ abstract class NavigationDialog : BaseDialog(), OnItemClickListener { fun openDir(directory: File?) { try { - filesList.adapter = AdapterFactory.getFileAdapter(activity as Activity, directory!!) + filesList.adapter = getFileAdapter(activity as Activity, directory!!) getViewModel().currentDir = directory pathView.text = getViewModel().currentDir!!.path } catch (e: Exception) { diff --git a/navdialogs/src/main/res/layout/navdialogs_dialog_open.xml b/navdialogs/src/main/res/layout/nav_dialogs_open.xml similarity index 100% rename from navdialogs/src/main/res/layout/navdialogs_dialog_open.xml rename to navdialogs/src/main/res/layout/nav_dialogs_open.xml diff --git a/navdialogs/src/main/res/layout/nav_dialogs_save_as.xml b/navdialogs/src/main/res/layout/nav_dialogs_save_as.xml new file mode 100644 index 0000000..09bc401 --- /dev/null +++ b/navdialogs/src/main/res/layout/nav_dialogs_save_as.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/navdialogs/src/main/res/layout/nav_dialogs_save_as_base.xml b/navdialogs/src/main/res/layout/nav_dialogs_save_as_base.xml new file mode 100644 index 0000000..98ef832 --- /dev/null +++ b/navdialogs/src/main/res/layout/nav_dialogs_save_as_base.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/navdialogs/src/main/res/layout/nav_dialogs_save_as_legacy.xml b/navdialogs/src/main/res/layout/nav_dialogs_save_as_legacy.xml new file mode 100644 index 0000000..bddf795 --- /dev/null +++ b/navdialogs/src/main/res/layout/nav_dialogs_save_as_legacy.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/navdialogs/src/main/res/layout/navdialogs_dialog_save_as.xml b/navdialogs/src/main/res/layout/navdialogs_dialog_save_as.xml deleted file mode 100644 index 807c8db..0000000 --- a/navdialogs/src/main/res/layout/navdialogs_dialog_save_as.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - \ No newline at end of file