From d9afb67d4444cded493f39ed863a2124d225bc4a Mon Sep 17 00:00:00 2001 From: William Brawner Date: Fri, 12 Jun 2020 22:54:32 -0700 Subject: [PATCH] Convert (most) Activities to Fragments and use AndroidX navigation component Signed-off-by: William Brawner --- app/build.gradle | 7 +- app/src/main/AndroidManifest.xml | 12 - .../view/activity/MainActivity.kt | 256 +-------------- .../view/activity/SettingsActivity.kt | 29 -- .../view/activity/SupportActivity.kt | 58 ---- .../view/fragment/MainFragment.kt | 301 ++++++++++++++++++ .../view/fragment/MainMenuFragment.kt | 81 ----- .../MarkdownInfoFragment.kt} | 42 ++- .../view/fragment/PreviewFragment.kt | 2 +- .../fragment/SettingsContainerFragment.kt | 28 ++ .../view/fragment/SupportFragment.kt | 61 ++++ .../main/res/layout-land/activity_main.xml | 47 --- .../main/res/layout-land/fragment_main.xml | 61 ++++ app/src/main/res/layout/activity_main.xml | 51 +-- app/src/main/res/layout/fragment_main.xml | 63 ++++ ...wn_info.xml => fragment_markdown_info.xml} | 2 +- .../main/res/layout/fragment_menu_main.xml | 7 - ...ity_settings.xml => fragment_settings.xml} | 16 +- ...ivity_support.xml => fragment_support.xml} | 0 app/src/main/res/menu/menu_main.xml | 10 +- app/src/main/res/navigation/nav_graph.xml | 87 +++++ 21 files changed, 651 insertions(+), 570 deletions(-) delete mode 100644 app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SettingsActivity.kt delete mode 100644 app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SupportActivity.kt create mode 100644 app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainFragment.kt delete mode 100644 app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainMenuFragment.kt rename app/src/main/java/com/wbrawner/simplemarkdown/view/{activity/MarkdownInfoActivity.kt => fragment/MarkdownInfoFragment.kt} (62%) create mode 100644 app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SettingsContainerFragment.kt create mode 100644 app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SupportFragment.kt delete mode 100644 app/src/main/res/layout-land/activity_main.xml create mode 100644 app/src/main/res/layout-land/fragment_main.xml create mode 100644 app/src/main/res/layout/fragment_main.xml rename app/src/main/res/layout/{activity_markdown_info.xml => fragment_markdown_info.xml} (95%) delete mode 100644 app/src/main/res/layout/fragment_menu_main.xml rename app/src/main/res/layout/{activity_settings.xml => fragment_settings.xml} (56%) rename app/src/main/res/layout/{activity_support.xml => fragment_support.xml} (100%) create mode 100644 app/src/main/res/navigation/nav_graph.xml diff --git a/app/build.gradle b/app/build.gradle index bcc6be2..8e6459a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,8 +41,8 @@ android { applicationId "com.wbrawner.simplemarkdown" minSdkVersion 21 targetSdkVersion 29 - versionCode 26 - versionName "0.8.4" + versionCode 27 + versionName "0.8.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders = [ sentryDsn: "https://399270639b2e4b10a028a2be9192d1d3@sentry.brawner.dev/2" @@ -89,6 +89,8 @@ android { } dependencies { + implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2' + implementation 'androidx.navigation:navigation-ui-ktx:2.2.2' testImplementation 'junit:junit:4.12' testImplementation 'org.robolectric:robolectric:4.2.1' implementation fileTree(include: ['*.jar'], dir: 'libs') @@ -118,7 +120,6 @@ dependencies { def coroutines_version = "1.3.4" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" - def lifecycle_version = "2.2.0" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" kapt "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2f9652c..fd885fb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,10 +43,6 @@ - - - - - - - - diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/MainActivity.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/MainActivity.kt index 14d8e62..196ce48 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/MainActivity.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/MainActivity.kt @@ -1,61 +1,23 @@ package com.wbrawner.simplemarkdown.view.activity -import android.Manifest -import android.app.Activity -import android.content.Intent -import android.content.pm.PackageManager -import android.content.res.Configuration -import android.net.Uri -import android.os.Build import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.webkit.MimeTypeMap -import android.widget.Toast import androidx.activity.viewModels -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import androidx.lifecycle.Observer -import androidx.preference.PreferenceManager -import com.wbrawner.simplemarkdown.MarkdownApplication +import androidx.navigation.findNavController import com.wbrawner.simplemarkdown.R -import com.wbrawner.simplemarkdown.utility.hideKeyboard -import com.wbrawner.simplemarkdown.view.adapter.EditPagerAdapter -import com.wbrawner.simplemarkdown.view.fragment.MainMenuFragment import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel -import kotlinx.android.synthetic.main.activity_main.* import kotlinx.coroutines.* -import java.io.File import kotlin.coroutines.CoroutineContext class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback, CoroutineScope { - private var shouldAutoSave = true override val coroutineContext: CoroutineContext = Dispatchers.Main private val viewModel: MarkdownViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - setSupportActionBar(toolbar) - supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu_black_24dp) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - val adapter = EditPagerAdapter(supportFragmentManager, this@MainActivity) - pager.adapter = adapter - pager.addOnPageChangeListener(adapter) - pager.pageMargin = 1 - pager.setPageMarginDrawable(R.color.colorAccent) - tabLayout.setupWithViewPager(pager) - if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { - tabLayout!!.visibility = View.GONE - } - @Suppress("CAST_NEVER_SUCCEEDS") - viewModel.fileName.observe(this, Observer { - toolbar?.title = it - }) intent?.data?.let { launch { viewModel.load(this@MainActivity, it) @@ -63,214 +25,8 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes } } - override fun onStart() { - super.onStart() - launch { - withContext(Dispatchers.IO) { - val enableErrorReports = PreferenceManager.getDefaultSharedPreferences(this@MainActivity) - .getBoolean(getString(R.string.error_reports_enabled), true) - (application as MarkdownApplication).errorHandler.enable(enableErrorReports) - } - } - } - - override fun onUserLeaveHint() { - super.onUserLeaveHint() - launch { - withContext(Dispatchers.IO) { - val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this@MainActivity) - val isAutoSaveEnabled = sharedPrefs.getBoolean(KEY_AUTOSAVE, true) - if (!shouldAutoSave || !isAutoSaveEnabled) { - return@withContext - } - - val uri = if (viewModel.save(this@MainActivity)) { - viewModel.uri.value - } else { - // The user has left the app, with autosave enabled, and we don't already have a - // Uri for them or for some reason we were unable to save to the original Uri. In - // this case, we need to just save to internal file storage so that we can recover - val fileUri = Uri.fromFile(File(filesDir, viewModel.fileName.value!!)) - if (viewModel.save(this@MainActivity, fileUri)) { - fileUri - } else { - null - } - } ?: return@withContext - sharedPrefs.edit() - .putString(getString(R.string.pref_key_autosave_uri), uri.toString()) - .apply() - } - } - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) - tabLayout!!.visibility = View.GONE - else - tabLayout!!.visibility = View.VISIBLE - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_edit, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - MainMenuFragment().show(supportFragmentManager, null) - window.decorView.hideKeyboard() - } - R.id.action_save -> { - launch { - if (!viewModel.save(this@MainActivity)) { - requestFileOp(REQUEST_SAVE_FILE) - } else { - Toast.makeText( - this@MainActivity, - getString(R.string.file_saved, viewModel.fileName.value), - Toast.LENGTH_SHORT - ).show() - } - } - } - R.id.action_save_as -> { - requestFileOp(REQUEST_SAVE_FILE) - } - R.id.action_share -> { - val shareIntent = Intent(Intent.ACTION_SEND) - shareIntent.putExtra(Intent.EXTRA_TEXT, viewModel.markdownUpdates.value) - shareIntent.type = "text/plain" - startActivity(Intent.createChooser( - shareIntent, - getString(R.string.share_file) - )) - } - R.id.action_load -> requestFileOp(REQUEST_OPEN_FILE) - R.id.action_new -> promptSaveOrDiscardChanges() - R.id.action_lock_swipe -> { - item.isChecked = !item.isChecked - pager!!.setSwipeLocked(item.isChecked) - } - } - return super.onOptionsItemSelected(item) - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - when (requestCode) { - REQUEST_SAVE_FILE, REQUEST_OPEN_FILE -> { - // If request is cancelled, the result arrays are empty. - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // Permission granted, open file save dialog - requestFileOp(requestCode) - } else { - // Permission denied, do nothing - Toast.makeText(this@MainActivity, R.string.no_permissions, Toast.LENGTH_SHORT) - .show() - } - } - } - } - override fun onBackPressed() { - if (pager!!.currentItem == EditPagerAdapter.FRAGMENT_EDIT) - super.onBackPressed() - else - pager!!.currentItem = EditPagerAdapter.FRAGMENT_EDIT - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - when (requestCode) { - REQUEST_OPEN_FILE -> { - if (resultCode != Activity.RESULT_OK || data?.data == null) { - return - } - - launch { - val fileLoaded = viewModel.load(this@MainActivity, data.data) - if (!fileLoaded) { - Toast.makeText(this@MainActivity, R.string.file_load_error, Toast.LENGTH_SHORT) - .show() - } - } - } - REQUEST_SAVE_FILE -> { - if (resultCode != Activity.RESULT_OK || data?.data == null) { - return - } - - launch { - viewModel.save(this@MainActivity, data.data) - } - } - } - super.onActivityResult(requestCode, resultCode, data) - } - - private fun promptSaveOrDiscardChanges() { - if (viewModel.originalMarkdown.value == viewModel.markdownUpdates.value) { - viewModel.reset("Untitled.md") - return - } - AlertDialog.Builder(this) - .setTitle(R.string.save_changes) - .setMessage(R.string.prompt_save_changes) - .setNegativeButton(R.string.action_discard) { _, _ -> - viewModel.reset("Untitled.md") - } - .setPositiveButton(R.string.action_save) { _, _ -> - requestFileOp(REQUEST_SAVE_FILE) - } - .create() - .show() - } - - private fun requestFileOp(requestType: Int) { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED && Build.VERSION.SDK_INT > 22) { - requestPermissions( - arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), - requestType - ) - return - } - // If the user is going to save the file, we don't want to auto-save it for them - shouldAutoSave = false - val intent = when (requestType) { - REQUEST_SAVE_FILE -> { - Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - type = "text/markdown" - putExtra(Intent.EXTRA_TITLE, viewModel.fileName.value) - } - } - REQUEST_OPEN_FILE -> { - Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - type = "*/*" - if (MimeTypeMap.getSingleton().hasMimeType("md")) { - // If the device doesn't recognize markdown files then we're not going to be - // able to open them at all, so there's no sense in filtering them out. - putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("text/plain", "text/markdown")) - } - } - } - else -> null - } ?: return - intent.addCategory(Intent.CATEGORY_OPENABLE) - startActivityForResult( - intent, - requestType - ) - } - - override fun onResume() { - super.onResume() - shouldAutoSave = true + findNavController(R.id.content).navigateUp() } override fun onDestroy() { @@ -279,12 +35,4 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes cancel() } } - - companion object { - // Request codes - const val REQUEST_OPEN_FILE = 1 - const val REQUEST_SAVE_FILE = 2 - const val REQUEST_DARK_MODE = 4 - const val KEY_AUTOSAVE = "autosave" - } } diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SettingsActivity.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SettingsActivity.kt deleted file mode 100644 index cb053fe..0000000 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SettingsActivity.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.wbrawner.simplemarkdown.view.activity - -import android.os.Bundle -import android.view.MenuItem -import androidx.appcompat.app.AppCompatActivity -import com.wbrawner.simplemarkdown.R -import com.wbrawner.simplemarkdown.view.fragment.SettingsFragment - -class SettingsActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_settings) - setSupportActionBar(findViewById(R.id.toolbar)) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - if (supportFragmentManager.fragments.isEmpty()) { - supportFragmentManager.beginTransaction() - .add(R.id.fragment_settings, SettingsFragment()) - .commit() - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - // The only menu item is the back button - setResult(RESULT_OK) - finish() - return true - } -} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SupportActivity.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SupportActivity.kt deleted file mode 100644 index 91ef1f0..0000000 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SupportActivity.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.wbrawner.simplemarkdown.view.activity - -import android.content.ActivityNotFoundException -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.view.MenuItem -import androidx.appcompat.app.AppCompatActivity -import androidx.browser.customtabs.CustomTabsIntent -import androidx.lifecycle.Observer -import com.wbrawner.simplemarkdown.R -import com.wbrawner.simplemarkdown.utility.SupportLinkProvider -import kotlinx.android.synthetic.main.activity_support.* - - -class SupportActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_support) - setSupportActionBar(toolbar) - setTitle(R.string.support_title) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - githubButton.setOnClickListener { - CustomTabsIntent.Builder() - .addDefaultShareMenuItem() - .build() - .launchUrl(this@SupportActivity, Uri.parse("https://github.com/wbrawner/SimpleMarkdown")) - } - rateButton.setOnClickListener { - val playStoreIntent = Intent(Intent.ACTION_VIEW) - .apply { - data = Uri.parse("market://details?id=${packageName}") - addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or - Intent.FLAG_ACTIVITY_NEW_DOCUMENT or - Intent.FLAG_ACTIVITY_MULTIPLE_TASK) - } - try { - startActivity(playStoreIntent) - } catch (ignored: ActivityNotFoundException) { - playStoreIntent.data = Uri.parse("https://play.google.com/store/apps/details?id=${packageName}") - startActivity(playStoreIntent) - } - } - SupportLinkProvider(this).supportLinks.observe(this, Observer { links -> - links.forEach { - supportButtons.addView(it) - } - }) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - finish() - return true - } - return super.onOptionsItemSelected(item) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainFragment.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainFragment.kt new file mode 100644 index 0000000..d06ac70 --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainFragment.kt @@ -0,0 +1,301 @@ +package com.wbrawner.simplemarkdown.view.fragment + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.MimeTypeMap +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.onNavDestinationSelected +import androidx.navigation.ui.setupWithNavController +import androidx.preference.PreferenceManager +import com.wbrawner.simplemarkdown.MarkdownApplication +import com.wbrawner.simplemarkdown.R +import com.wbrawner.simplemarkdown.view.adapter.EditPagerAdapter +import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel +import kotlinx.android.synthetic.main.fragment_main.* +import kotlinx.coroutines.* +import java.io.File +import kotlin.coroutines.CoroutineContext + +class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallback, CoroutineScope { + + private var shouldAutoSave = true + override val coroutineContext: CoroutineContext = Dispatchers.Main + private val viewModel: MarkdownViewModel by activityViewModels() + private var appBarConfiguration: AppBarConfiguration? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = + inflater.inflate(R.layout.fragment_main, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + with(findNavController()) { + appBarConfiguration = AppBarConfiguration(graph, drawerLayout) + toolbar.setupWithNavController(this, appBarConfiguration!!) + toolbar.inflateMenu(R.menu.menu_edit) + toolbar.setOnMenuItemClickListener { item -> + return@setOnMenuItemClickListener when (item.itemId) { + R.id.action_save -> { + launch { + if (!viewModel.save(requireContext())) { + requestFileOp(REQUEST_SAVE_FILE) + } else { + Toast.makeText( + requireContext(), + getString(R.string.file_saved, viewModel.fileName.value), + Toast.LENGTH_SHORT + ).show() + } + } + true + } + R.id.action_save_as -> { + requestFileOp(REQUEST_SAVE_FILE) + true + } + R.id.action_share -> { + val shareIntent = Intent(Intent.ACTION_SEND) + shareIntent.putExtra(Intent.EXTRA_TEXT, viewModel.markdownUpdates.value) + shareIntent.type = "text/plain" + startActivity(Intent.createChooser( + shareIntent, + getString(R.string.share_file) + )) + true + } + R.id.action_load -> { + requestFileOp(REQUEST_OPEN_FILE) + true + } + R.id.action_new -> { + promptSaveOrDiscardChanges() + true + } + R.id.action_lock_swipe -> { + item.isChecked = !item.isChecked + pager!!.setSwipeLocked(item.isChecked) + true + } + else -> item.onNavDestinationSelected(findNavController()) + } + } + navigationView.setupWithNavController(this) + } + val adapter = EditPagerAdapter(childFragmentManager, view.context) + pager.adapter = adapter + pager.addOnPageChangeListener(adapter) + pager.pageMargin = 1 + pager.setPageMarginDrawable(R.color.colorAccent) + tabLayout.setupWithViewPager(pager) + if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + tabLayout!!.visibility = View.GONE + } + @Suppress("CAST_NEVER_SUCCEEDS") + viewModel.fileName.observe(viewLifecycleOwner, Observer { + toolbar?.title = it + }) + } + + override fun onStart() { + super.onStart() + launch { + withContext(Dispatchers.IO) { + val enableErrorReports = PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getBoolean(getString(R.string.error_reports_enabled), true) + (requireActivity().application as MarkdownApplication).errorHandler.enable(enableErrorReports) + } + } + } + + override fun onPause() { + super.onPause() + launch { + val context = context?.applicationContext ?: return@launch + withContext(Dispatchers.IO) { + val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context) + val isAutoSaveEnabled = sharedPrefs.getBoolean(KEY_AUTOSAVE, true) + if (!shouldAutoSave || !isAutoSaveEnabled) { + return@withContext + } + + val uri = if (viewModel.save(context)) { + viewModel.uri.value + } else { + // The user has left the app, with autosave enabled, and we don't already have a + // Uri for them or for some reason we were unable to save to the original Uri. In + // this case, we need to just save to internal file storage so that we can recover + val fileUri = Uri.fromFile(File(context.filesDir, viewModel.fileName.value!!)) + if (viewModel.save(context, fileUri)) { + fileUri + } else { + null + } + } ?: return@withContext + sharedPrefs.edit() + .putString(getString(R.string.pref_key_autosave_uri), uri.toString()) + .apply() + } + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) + tabLayout!!.visibility = View.GONE + else + tabLayout!!.visibility = View.VISIBLE + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + when (requestCode) { + REQUEST_SAVE_FILE, REQUEST_OPEN_FILE -> { + // If request is cancelled, the result arrays are empty. + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Permission granted, open file save dialog + requestFileOp(requestCode) + } else { + // Permission denied, do nothing + context?.let { + Toast.makeText(it, R.string.no_permissions, Toast.LENGTH_SHORT) + .show() + } + } + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + REQUEST_OPEN_FILE -> { + if (resultCode != Activity.RESULT_OK || data?.data == null) { + return + } + + launch { + val fileLoaded = context?.let { + viewModel.load(it, data.data) + } + if (fileLoaded == false) { + context?.let { + Toast.makeText(it, R.string.file_load_error, Toast.LENGTH_SHORT) + .show() + } + } + } + } + REQUEST_SAVE_FILE -> { + if (resultCode != Activity.RESULT_OK || data?.data == null) { + return + } + + launch { + context?.let { + viewModel.save(it, data.data) + } + } + } + } + super.onActivityResult(requestCode, resultCode, data) + } + + private fun promptSaveOrDiscardChanges() { + if (viewModel.originalMarkdown.value == viewModel.markdownUpdates.value) { + viewModel.reset("Untitled.md") + return + } + val context = context ?: return + AlertDialog.Builder(context) + .setTitle(R.string.save_changes) + .setMessage(R.string.prompt_save_changes) + .setNegativeButton(R.string.action_discard) { _, _ -> + viewModel.reset("Untitled.md") + } + .setPositiveButton(R.string.action_save) { _, _ -> + requestFileOp(REQUEST_SAVE_FILE) + } + .create() + .show() + } + + private fun requestFileOp(requestType: Int) { + val context = context ?: return + if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED && Build.VERSION.SDK_INT > 22) { + requestPermissions( + arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), + requestType + ) + return + } + // If the user is going to save the file, we don't want to auto-save it for them + shouldAutoSave = false + val intent = when (requestType) { + REQUEST_SAVE_FILE -> { + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + type = "text/markdown" + putExtra(Intent.EXTRA_TITLE, viewModel.fileName.value) + } + } + REQUEST_OPEN_FILE -> { + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + type = "*/*" + if (MimeTypeMap.getSingleton().hasMimeType("md")) { + // If the device doesn't recognize markdown files then we're not going to be + // able to open them at all, so there's no sense in filtering them out. + putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("text/plain", "text/markdown")) + } + } + } + else -> null + } ?: return + intent.addCategory(Intent.CATEGORY_OPENABLE) + startActivityForResult( + intent, + requestType + ) + } + + override fun onResume() { + super.onResume() + shouldAutoSave = true + } + + override fun onDestroy() { + super.onDestroy() + coroutineContext[Job]?.let { + cancel() + } + } + + companion object { + // Request codes + const val REQUEST_OPEN_FILE = 1 + const val REQUEST_SAVE_FILE = 2 + const val REQUEST_DARK_MODE = 4 + const val KEY_AUTOSAVE = "autosave" + } +} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainMenuFragment.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainMenuFragment.kt deleted file mode 100644 index 5063908..0000000 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainMenuFragment.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.wbrawner.simplemarkdown.view.fragment - -import android.content.Intent -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.wbrawner.simplemarkdown.R -import com.wbrawner.simplemarkdown.view.activity.MainActivity -import com.wbrawner.simplemarkdown.view.activity.MarkdownInfoActivity -import com.wbrawner.simplemarkdown.view.activity.MarkdownInfoActivity.Companion.EXTRA_FILE -import com.wbrawner.simplemarkdown.view.activity.MarkdownInfoActivity.Companion.EXTRA_TITLE -import com.wbrawner.simplemarkdown.view.activity.SettingsActivity -import com.wbrawner.simplemarkdown.view.activity.SupportActivity -import kotlinx.android.synthetic.main.fragment_menu_main.* - -class MainMenuFragment : BottomSheetDialogFragment() { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? = inflater.inflate(R.layout.fragment_menu_main, container, false) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - mainMenuNavigationView.setNavigationItemSelectedListener { menuItem -> - val (intentClass, fileName, title) = when (menuItem.itemId) { - R.id.action_help -> Triple( - MarkdownInfoActivity::class.java, - "Cheatsheet.md", - R.string.action_help - ) - R.id.action_settings -> Triple( - SettingsActivity::class.java, - null, - null - ) - R.id.action_libraries -> Triple( - MarkdownInfoActivity::class.java, - "Libraries.md", - R.string.action_libraries - ) - R.id.action_privacy -> Triple( - MarkdownInfoActivity::class.java, - "Privacy Policy.md", - R.string.action_privacy - ) - R.id.action_support -> Triple( - SupportActivity::class.java, - null, - null - ) - else -> throw IllegalStateException("This shouldn't happen") - } - val intent = Intent(context, intentClass) - fileName?.let { - intent.putExtra(EXTRA_FILE, it) - } - title?.let { - intent.putExtra(EXTRA_TITLE, getString(it)) - } - if (intentClass == SettingsActivity::class.java) { - startActivityForResult(intent, MainActivity.REQUEST_DARK_MODE) - } else { - startActivity(intent) - dialog?.dismiss() - } - true - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == MainActivity.REQUEST_DARK_MODE) { - activity?.recreate() - } else { - super.onActivityResult(requestCode, resultCode, data) - } - dialog?.dismiss() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/MarkdownInfoActivity.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MarkdownInfoFragment.kt similarity index 62% rename from app/src/main/java/com/wbrawner/simplemarkdown/view/activity/MarkdownInfoActivity.kt rename to app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MarkdownInfoFragment.kt index 1e1c2ee..25c888a 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/MarkdownInfoActivity.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MarkdownInfoFragment.kt @@ -1,38 +1,46 @@ -package com.wbrawner.simplemarkdown.view.activity +package com.wbrawner.simplemarkdown.view.fragment import android.content.res.Configuration import android.os.Bundle +import android.view.LayoutInflater import android.view.MenuItem +import android.view.View +import android.view.ViewGroup import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.navigation.ui.setupWithNavController import com.wbrawner.simplemarkdown.MarkdownApplication import com.wbrawner.simplemarkdown.R import com.wbrawner.simplemarkdown.utility.readAssetToString import com.wbrawner.simplemarkdown.utility.toHtml -import kotlinx.android.synthetic.main.activity_markdown_info.* +import kotlinx.android.synthetic.main.fragment_markdown_info.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext -class MarkdownInfoActivity : AppCompatActivity(), CoroutineScope { +class MarkdownInfoFragment : Fragment(), CoroutineScope { override val coroutineContext: CoroutineContext = Dispatchers.Main override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_markdown_info) - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - val title = intent?.getStringExtra(EXTRA_TITLE) - val fileName = intent?.getStringExtra(EXTRA_FILE) - if (title.isNullOrBlank() || fileName.isNullOrBlank()) { - finish() + setHasOptionsMenu(true) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = + inflater.inflate(R.layout.fragment_markdown_info, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val fileName = arguments?.getString(EXTRA_FILE) + if (fileName.isNullOrBlank()) { + findNavController().navigateUp() return } + toolbar.setupWithNavController(findNavController()) - setTitle(title) val isNightMode = AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES || resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES @@ -44,7 +52,7 @@ class MarkdownInfoActivity : AppCompatActivity(), CoroutineScope { val css: String? = getString(defaultCssId) launch { try { - val html = assets?.readAssetToString(fileName) + val html = view.context.assets?.readAssetToString(fileName) ?.toHtml() ?: throw RuntimeException("Unable to open stream to $fileName") infoWebview.loadDataWithBaseURL(null, @@ -53,9 +61,9 @@ class MarkdownInfoActivity : AppCompatActivity(), CoroutineScope { "UTF-8", null ) } catch (e: Exception) { - (application as MarkdownApplication).errorHandler.reportException(e) - Toast.makeText(this@MarkdownInfoActivity, R.string.file_load_error, Toast.LENGTH_SHORT).show() - finish() + (requireActivity().application as MarkdownApplication).errorHandler.reportException(e) + Toast.makeText(view.context, R.string.file_load_error, Toast.LENGTH_SHORT).show() + findNavController().navigateUp() } } } @@ -68,7 +76,7 @@ class MarkdownInfoActivity : AppCompatActivity(), CoroutineScope { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + findNavController().navigateUp() return true } return super.onOptionsItemSelected(item) diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/PreviewFragment.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/PreviewFragment.kt index 4faeaf3..8931524 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/PreviewFragment.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/PreviewFragment.kt @@ -64,7 +64,7 @@ class PreviewFragment : Fragment(), CoroutineScope { override fun onAttach(context: Context) { super.onAttach(context) updateWebContent(viewModel.markdownUpdates.value ?: "") - viewModel.markdownUpdates.observe(viewLifecycleOwner, Observer { + viewModel.markdownUpdates.observe(this, Observer { updateWebContent(it) }) } diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SettingsContainerFragment.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SettingsContainerFragment.kt new file mode 100644 index 0000000..e9d80e4 --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SettingsContainerFragment.kt @@ -0,0 +1,28 @@ +package com.wbrawner.simplemarkdown.view.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.navigation.ui.setupWithNavController +import com.wbrawner.simplemarkdown.R +import kotlinx.android.synthetic.main.fragment_settings.* + +class SettingsContainerFragment : Fragment() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = + inflater.inflate(R.layout.fragment_settings, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + toolbar.setupWithNavController(findNavController()) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = findNavController().navigateUp() +} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SupportFragment.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SupportFragment.kt new file mode 100644 index 0000000..8a5eff8 --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SupportFragment.kt @@ -0,0 +1,61 @@ +package com.wbrawner.simplemarkdown.view.fragment + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.browser.customtabs.CustomTabsIntent +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import androidx.navigation.ui.setupWithNavController +import com.wbrawner.simplemarkdown.R +import com.wbrawner.simplemarkdown.utility.SupportLinkProvider +import kotlinx.android.synthetic.main.fragment_support.* + +class SupportFragment : Fragment() { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = + inflater.inflate(R.layout.fragment_support, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + toolbar.setupWithNavController(findNavController()) + githubButton.setOnClickListener { + CustomTabsIntent.Builder() + .addDefaultShareMenuItem() + .build() + .launchUrl(view.context, Uri.parse("https://github" + + ".com/wbrawner/SimpleMarkdown")) + } + rateButton.setOnClickListener { + val playStoreIntent = Intent(Intent.ACTION_VIEW) + .apply { + data = Uri.parse("market://details?id=${view.context.packageName}") + addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or + Intent.FLAG_ACTIVITY_NEW_DOCUMENT or + Intent.FLAG_ACTIVITY_MULTIPLE_TASK) + } + try { + startActivity(playStoreIntent) + } catch (ignored: ActivityNotFoundException) { + playStoreIntent.data = Uri.parse("https://play.google.com/store/apps/details?id=${view.context.packageName}") + startActivity(playStoreIntent) + } + } + SupportLinkProvider(requireActivity()).supportLinks.observe(viewLifecycleOwner, Observer { links -> + links.forEach { + supportButtons.addView(it) + } + }) + } + +// override fun onOptionsItemSelected(item: MenuItem): Boolean { +// if (item.itemId == android.R.id.home) { +// findNavController().navigateUp() +// return true +// } +// return super.onOptionsItemSelected(item) +// } +} \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml deleted file mode 100644 index 33207f6..0000000 --- a/app/src/main/res/layout-land/activity_main.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_main.xml b/app/src/main/res/layout-land/fragment_main.xml new file mode 100644 index 0000000..fe45a2b --- /dev/null +++ b/app/src/main/res/layout-land/fragment_main.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index fa01013..1a47a1a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,48 +1,9 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file + android:layout_height="match_parent" + app:defaultNavHost="true" + app:navGraph="@navigation/nav_graph" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml new file mode 100644 index 0000000..6c77982 --- /dev/null +++ b/app/src/main/res/layout/fragment_main.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_markdown_info.xml b/app/src/main/res/layout/fragment_markdown_info.xml similarity index 95% rename from app/src/main/res/layout/activity_markdown_info.xml rename to app/src/main/res/layout/fragment_markdown_info.xml index 8446872..987b398 100644 --- a/app/src/main/res/layout/activity_markdown_info.xml +++ b/app/src/main/res/layout/fragment_markdown_info.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="com.wbrawner.simplemarkdown.view.activity.MarkdownInfoActivity"> + tools:context="com.wbrawner.simplemarkdown.view.fragment.MarkdownInfoFragment"> - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/fragment_settings.xml similarity index 56% rename from app/src/main/res/layout/activity_settings.xml rename to app/src/main/res/layout/fragment_settings.xml index 005fbd3..6dc19de 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -1,15 +1,13 @@ - + android:orientation="vertical"> + android:layout_height="wrap_content"> + android:layout_weight="1" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_support.xml b/app/src/main/res/layout/fragment_support.xml similarity index 100% rename from app/src/main/res/layout/activity_support.xml rename to app/src/main/res/layout/fragment_support.xml diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index dfe9495..30f818c 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -3,29 +3,29 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 0000000..522612a --- /dev/null +++ b/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file