diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/contracts/PermissionContract.kt b/app/src/main/java/dev/arkbuilders/arkmemo/contracts/PermissionContract.kt index f7e98852..1dda411c 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/contracts/PermissionContract.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/contracts/PermissionContract.kt @@ -14,7 +14,9 @@ class PermissionContract : ActivityResultContract() { override fun createIntent( context: Context, input: String, - ) = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse(input)) + ) = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse(input)).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } @RequiresApi(Build.VERSION_CODES.R) override fun parseResult( diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/di/RepositoryModule.kt b/app/src/main/java/dev/arkbuilders/arkmemo/di/RepositoryModule.kt index cdffa14c..b2adff2b 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/di/RepositoryModule.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/di/RepositoryModule.kt @@ -9,12 +9,14 @@ import dev.arkbuilders.arklib.user.properties.PropertiesStorageRepo import dev.arkbuilders.arkmemo.models.GraphicNote import dev.arkbuilders.arkmemo.models.TextNote import dev.arkbuilders.arkmemo.models.VoiceNote -import dev.arkbuilders.arkmemo.preferences.MemoPreferences import dev.arkbuilders.arkmemo.repo.NotesRepo import dev.arkbuilders.arkmemo.repo.NotesRepoHelper import dev.arkbuilders.arkmemo.repo.graphics.GraphicNotesRepo import dev.arkbuilders.arkmemo.repo.text.TextNotesRepo import dev.arkbuilders.arkmemo.repo.voices.VoiceNotesRepo +import kotlinx.coroutines.CoroutineDispatcher +import javax.inject.Named +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module @@ -27,12 +29,15 @@ abstract class RepositoryModule { @Binds abstract fun bindVoiceNotesRepo(impl: VoiceNotesRepo): NotesRepo +} - companion object { - @Provides - fun provideNotesRepoHelper( - memoPreferences: MemoPreferences, - propertiesStorageRepo: PropertiesStorageRepo, - ) = NotesRepoHelper(memoPreferences, propertiesStorageRepo) - } +@InstallIn(SingletonComponent::class) +@Module +object RepoHelperModule { + @Singleton + @Provides + fun provideNotesRepoHelper( + propertiesStorageRepo: PropertiesStorageRepo, + @Named(IO_DISPATCHER) coroutineDispatcher: CoroutineDispatcher, + ) = NotesRepoHelper(propertiesStorageRepo, coroutineDispatcher) } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/models/NoteType.kt b/app/src/main/java/dev/arkbuilders/arkmemo/models/NoteType.kt new file mode 100644 index 00000000..5b57d02f --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/arkmemo/models/NoteType.kt @@ -0,0 +1,7 @@ +package dev.arkbuilders.arkmemo.models + +enum class NoteType { + TEXT, + VOICE, + GRAPHIC, +} diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/preferences/MemoPreferences.kt b/app/src/main/java/dev/arkbuilders/arkmemo/preferences/MemoPreferences.kt index da9238e9..404807b7 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/preferences/MemoPreferences.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/preferences/MemoPreferences.kt @@ -14,4 +14,8 @@ interface MemoPreferences { fun getCrashReportEnabled(): Boolean fun storageNotAvailable(): Boolean + + fun isLastLaunchSuccess(): Boolean + + fun setLastLaunchSuccess(success: Boolean) } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/preferences/MemoPreferencesImpl.kt b/app/src/main/java/dev/arkbuilders/arkmemo/preferences/MemoPreferencesImpl.kt index 4472a7fe..8fd12f31 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/preferences/MemoPreferencesImpl.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/preferences/MemoPreferencesImpl.kt @@ -11,6 +11,7 @@ import kotlin.io.path.exists private const val NAME = "memo_prefs" private const val CURRENT_NOTES_PATH = "current_notes_path" +private const val PREF_LAST_LAUNCH_SUCCESS = "pref_last_launch_success" class MemoPreferencesImpl @Inject @@ -38,4 +39,12 @@ class MemoPreferencesImpl override fun storageNotAvailable(): Boolean { return getPath().isEmpty() || !getNotesStorage().exists() } + + override fun isLastLaunchSuccess(): Boolean { + return sharedPreferences.getBoolean(PREF_LAST_LAUNCH_SUCCESS, true) + } + + override fun setLastLaunchSuccess(success: Boolean) { + prefEditor.putBoolean(PREF_LAST_LAUNCH_SUCCESS, success).apply() + } } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepo.kt b/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepo.kt index ab92ae34..3b10f9d8 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepo.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepo.kt @@ -1,9 +1,10 @@ package dev.arkbuilders.arkmemo.repo +import dev.arkbuilders.arklib.ResourceId import dev.arkbuilders.arkmemo.models.SaveNoteResult interface NotesRepo { - suspend fun init() + suspend fun init(root: String) suspend fun save( note: Note, @@ -15,4 +16,6 @@ interface NotesRepo { suspend fun delete(note: Note) suspend fun delete(notes: List) + + suspend fun findNote(id: ResourceId): Note? } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepoHelper.kt b/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepoHelper.kt index 63d4c426..46acac4b 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepoHelper.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepoHelper.kt @@ -7,14 +7,19 @@ import dev.arkbuilders.arklib.data.index.RootIndex import dev.arkbuilders.arklib.user.properties.Properties import dev.arkbuilders.arklib.user.properties.PropertiesStorage import dev.arkbuilders.arklib.user.properties.PropertiesStorageRepo +import dev.arkbuilders.arkmemo.di.IO_DISPATCHER import dev.arkbuilders.arkmemo.models.Note -import dev.arkbuilders.arkmemo.preferences.MemoPreferences import dev.arkbuilders.arkmemo.utils.isEqual +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.withContext import java.nio.file.Path import javax.inject.Inject +import javax.inject.Named import kotlin.NullPointerException +import kotlin.io.path.Path import kotlin.io.path.deleteIfExists import kotlin.io.path.extension import kotlin.io.path.getLastModifiedTime @@ -24,15 +29,22 @@ import kotlin.io.path.name class NotesRepoHelper @Inject constructor( - private val memoPreferences: MemoPreferences, private val propertiesStorageRepo: PropertiesStorageRepo, + @Named(IO_DISPATCHER) private val iODispatcher: CoroutineDispatcher, ) { - private lateinit var root: Path + lateinit var root: Path + private lateinit var propertiesStorage: PropertiesStorage + private val lazyPropertiesStorage by lazy { + CoroutineScope(iODispatcher).async { + val propertyStorage = propertiesStorageRepo.provide(RootIndex.provide(root)) + propertyStorage + } + } - suspend fun init() { - root = memoPreferences.getNotesStorage() - propertiesStorage = propertiesStorageRepo.provide(RootIndex.provide(root)) + suspend fun init(root: String) { + this.root = Path(root) + propertiesStorage = lazyPropertiesStorage.await() } suspend fun persistNoteProperties( diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/repo/graphics/GraphicNotesRepo.kt b/app/src/main/java/dev/arkbuilders/arkmemo/repo/graphics/GraphicNotesRepo.kt index 055773da..08627a40 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/repo/graphics/GraphicNotesRepo.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/repo/graphics/GraphicNotesRepo.kt @@ -8,6 +8,7 @@ import android.graphics.Canvas import android.os.Environment import android.util.Log import dagger.hilt.android.qualifiers.ApplicationContext +import dev.arkbuilders.arklib.ResourceId import dev.arkbuilders.arklib.computeId import dev.arkbuilders.arklib.data.index.Resource import dev.arkbuilders.arkmemo.R @@ -16,7 +17,6 @@ import dev.arkbuilders.arkmemo.graphics.ColorCode import dev.arkbuilders.arkmemo.graphics.SVG import dev.arkbuilders.arkmemo.models.GraphicNote import dev.arkbuilders.arkmemo.models.SaveNoteResult -import dev.arkbuilders.arkmemo.preferences.MemoPreferences import dev.arkbuilders.arkmemo.repo.NotesRepo import dev.arkbuilders.arkmemo.repo.NotesRepoHelper import dev.arkbuilders.arkmemo.utils.dpToPx @@ -39,12 +39,11 @@ import kotlin.io.path.name class GraphicNotesRepo @Inject constructor( - private val memoPreferences: MemoPreferences, @Named(IO_DISPATCHER) private val iODispatcher: CoroutineDispatcher, private val helper: NotesRepoHelper, @ApplicationContext private val context: Context, ) : NotesRepo { - private lateinit var root: Path + private val root: Path by lazy { helper.root } private val displayMetrics by lazy { Resources.getSystem().displayMetrics } private val screenWidth by lazy { displayMetrics.widthPixels } @@ -53,9 +52,8 @@ class GraphicNotesRepo private val thumbDirectory by lazy { context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) } - override suspend fun init() { - helper.init() - root = memoPreferences.getNotesStorage() + override suspend fun init(root: String) { + helper.init(root) } override suspend fun save( @@ -119,33 +117,20 @@ class GraphicNotesRepo private suspend fun readStorage() = withContext(iODispatcher) { root.listFiles(SVG_EXT) { path -> - val svg = SVG.parse(path) - if (svg == null) { - Log.w(GRAPHICS_REPO, "Skipping invalid SVG: " + path) - } - val size = path.fileSize() - val id = computeId(size, path) - val resource = - Resource( - id = id, - name = path.fileName.name, - extension = path.extension, - modified = path.getLastModifiedTime(), - ) - - val userNoteProperties = helper.readProperties(id, "") - val bitmap = exportBitmapFromSvg(fileName = id.toString(), svg = svg) - - GraphicNote( - title = userNoteProperties.title, - description = userNoteProperties.description, - svg = svg, - resource = resource, - thumb = bitmap, - ) + path.toGraphicNote(helper = helper) }.filter { graphicNote -> graphicNote.svg != null } } + override suspend fun findNote(id: ResourceId): GraphicNote? { + return withContext(iODispatcher) { + root.listFiles(SVG_EXT) { path -> + path.toGraphicNote(helper = helper) + }.firstOrNull { graphicNote -> + graphicNote.svg != null && id == graphicNote.resource?.id + } + } + } + private fun exportBitmapFromSvg( fileName: String, svg: SVG?, @@ -207,6 +192,33 @@ class GraphicNotesRepo return null } } + + private fun Path.toGraphicNote(helper: NotesRepoHelper): GraphicNote { + val svg = SVG.parse(this) + if (svg == null) { + Log.w(GRAPHICS_REPO, "Skipping invalid SVG: " + this) + } + val size = this.fileSize() + val id = computeId(size, this) + val resource = + Resource( + id = id, + name = this.fileName.name, + extension = this.extension, + modified = this.getLastModifiedTime(), + ) + + val userNoteProperties = helper.readProperties(id, "") + val bitmap = exportBitmapFromSvg(fileName = id.toString(), svg = svg) + + return GraphicNote( + title = userNoteProperties.title, + description = userNoteProperties.description, + svg = svg, + resource = resource, + thumb = bitmap, + ) + } } private const val GRAPHICS_REPO = "GraphicNotesRepo" diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/repo/text/TextNotesRepo.kt b/app/src/main/java/dev/arkbuilders/arkmemo/repo/text/TextNotesRepo.kt index 93521be0..f52e25fe 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/repo/text/TextNotesRepo.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/repo/text/TextNotesRepo.kt @@ -1,12 +1,12 @@ package dev.arkbuilders.arkmemo.repo.text import android.util.Log +import dev.arkbuilders.arklib.ResourceId import dev.arkbuilders.arklib.computeId import dev.arkbuilders.arklib.data.index.Resource import dev.arkbuilders.arkmemo.di.IO_DISPATCHER import dev.arkbuilders.arkmemo.models.SaveNoteResult import dev.arkbuilders.arkmemo.models.TextNote -import dev.arkbuilders.arkmemo.preferences.MemoPreferences import dev.arkbuilders.arkmemo.repo.NotesRepo import dev.arkbuilders.arkmemo.repo.NotesRepoHelper import dev.arkbuilders.arkmemo.utils.listFiles @@ -27,16 +27,14 @@ import kotlin.io.path.writeLines class TextNotesRepo @Inject constructor( - private val memoPreferences: MemoPreferences, @Named(IO_DISPATCHER) private val iODispatcher: CoroutineDispatcher, private val helper: NotesRepoHelper, ) : NotesRepo { - private lateinit var root: Path + private val root: Path by lazy { helper.root } - override suspend fun init() { - root = memoPreferences.getNotesStorage() - helper.init() + override suspend fun init(root: String) { + helper.init(root) } override suspend fun save( @@ -59,6 +57,10 @@ class TextNotesRepo readStorage() } + override suspend fun findNote(id: ResourceId): TextNote? { + return null + } + private suspend fun write( note: TextNote, callback: (SaveNoteResult) -> Unit, diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/repo/voices/VoiceNotesRepo.kt b/app/src/main/java/dev/arkbuilders/arkmemo/repo/voices/VoiceNotesRepo.kt index e83a4e30..305e3eaa 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/repo/voices/VoiceNotesRepo.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/repo/voices/VoiceNotesRepo.kt @@ -1,12 +1,12 @@ package dev.arkbuilders.arkmemo.repo.voices import android.util.Log +import dev.arkbuilders.arklib.ResourceId import dev.arkbuilders.arklib.computeId import dev.arkbuilders.arklib.data.index.Resource import dev.arkbuilders.arkmemo.di.IO_DISPATCHER import dev.arkbuilders.arkmemo.models.SaveNoteResult import dev.arkbuilders.arkmemo.models.VoiceNote -import dev.arkbuilders.arkmemo.preferences.MemoPreferences import dev.arkbuilders.arkmemo.repo.NotesRepo import dev.arkbuilders.arkmemo.repo.NotesRepoHelper import dev.arkbuilders.arkmemo.utils.extractDuration @@ -26,15 +26,13 @@ import kotlin.io.path.pathString class VoiceNotesRepo @Inject constructor( - private val memoPreferences: MemoPreferences, @Named(IO_DISPATCHER) private val iODispatcher: CoroutineDispatcher, private val helper: NotesRepoHelper, ) : NotesRepo { - private lateinit var root: Path + private val root: Path by lazy { helper.root } - override suspend fun init() { - root = memoPreferences.getNotesStorage() - helper.init() + override suspend fun init(root: String) { + helper.init(root) } override suspend fun read(): List = @@ -42,6 +40,16 @@ class VoiceNotesRepo readStorage() } + override suspend fun findNote(id: ResourceId): VoiceNote? { + return withContext(iODispatcher) { + root.listFiles(VOICE_EXT) { path -> + path.toVoiceNote(helper = helper) + }.firstOrNull { voiceNote -> + voiceNote.duration.isNotEmpty() && id == voiceNote.resource?.id + } + } + } + override suspend fun delete(notes: List) { helper.deleteNotes(notes) } @@ -104,25 +112,29 @@ class VoiceNotesRepo private suspend fun readStorage(): List = withContext(iODispatcher) { root.listFiles(VOICE_EXT) { path -> - val id = computeId(path.fileSize(), path) - val resource = - Resource( - id = id, - name = path.name, - extension = path.extension, - modified = path.getLastModifiedTime(), - ) - - val userNoteProperties = helper.readProperties(id, "") - VoiceNote( - title = userNoteProperties.title, - description = userNoteProperties.description, - path = path, - duration = extractDuration(path.pathString), - resource = resource, - ) + path.toVoiceNote(helper) }.filter { voiceNote -> voiceNote.duration.isNotEmpty() } } + + private fun Path.toVoiceNote(helper: NotesRepoHelper): VoiceNote { + val id = computeId(this.fileSize(), this) + val resource = + Resource( + id = id, + name = this.name, + extension = this.extension, + modified = this.getLastModifiedTime(), + ) + + val userNoteProperties = helper.readProperties(id, "") + return VoiceNote( + title = userNoteProperties.title, + description = userNoteProperties.description, + path = this, + duration = extractDuration(this.pathString), + resource = resource, + ) + } } private const val VOICES_REPO = "VoiceNotesRepo" diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/activities/MainActivity.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/activities/MainActivity.kt index 1491bd0b..052d2971 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/activities/MainActivity.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/activities/MainActivity.kt @@ -3,7 +3,6 @@ package dev.arkbuilders.arkmemo.ui.activities import android.content.Intent import android.os.Bundle import android.view.WindowManager -import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.IdRes import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat @@ -13,7 +12,6 @@ import androidx.fragment.app.Fragment import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint import dev.arkbuilders.arkmemo.R -import dev.arkbuilders.arkmemo.contracts.PermissionContract import dev.arkbuilders.arkmemo.databinding.ActivityMainBinding import dev.arkbuilders.arkmemo.models.RootNotFound import dev.arkbuilders.arkmemo.preferences.MemoPreferences @@ -22,6 +20,7 @@ import dev.arkbuilders.arkmemo.ui.dialogs.FilePickerDialog import dev.arkbuilders.arkmemo.ui.fragments.BaseFragment import dev.arkbuilders.arkmemo.ui.fragments.EditTextNotesFragment import dev.arkbuilders.arkmemo.ui.fragments.NotesFragment +import dev.arkbuilders.arkmemo.utils.PermissionManager import dev.arkbuilders.components.filepicker.onArkPathPicked import javax.inject.Inject import kotlin.io.path.exists @@ -37,25 +36,10 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { private val fragContainer = R.id.container var fragment: Fragment = NotesFragment() + val permissionManager = PermissionManager(activity = this) init { - FilePickerDialog.readPermLauncher = - registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> - if (isGranted) { - FilePickerDialog.show() - } else { - finish() - } - } - - FilePickerDialog.readPermLauncherSdkR = - registerForActivityResult(PermissionContract()) { isGranted -> - if (isGranted) { - FilePickerDialog.show() - } else { - finish() - } - } + FilePickerDialog.permissionManager = permissionManager } override fun onCreate(savedInstanceState: Bundle?) { @@ -67,48 +51,61 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { onBackPressedDispatcher.onBackPressed() } - fun showFragment() { - val textDataFromIntent = intent?.getStringExtra(Intent.EXTRA_TEXT) - if (textDataFromIntent != null) { - fragment = EditTextNotesFragment.newInstance(textDataFromIntent) - supportFragmentManager.beginTransaction().apply { - replace(fragContainer, fragment, EditTextNotesFragment.TAG) - commit() - } + supportFragmentManager.onArkPathPicked(this) { + memoPreferences.storePath(it.toString()) + showFragment(savedInstanceState) + } + + permissionManager.askForWriteStorage { granted -> + if (!granted) { + finish() } else { - if (savedInstanceState == null) { - supportFragmentManager.beginTransaction().apply { - add(fragContainer, fragment, NotesFragment.TAG) - commit() + val storageFolderExisting = memoPreferences.getNotesStorage().exists() + if (memoPreferences.storageNotAvailable()) { + if (!storageFolderExisting) { + showNoNoteStorageDialog(RootNotFound(rootPath = memoPreferences.getPath())) + } else { + FilePickerDialog.show(this, supportFragmentManager) } } else { - supportFragmentManager.apply { - val tag = savedInstanceState.getString(CURRENT_FRAGMENT_TAG) - findFragmentByTag(tag)?.let { - fragment = it - if (!fragment.isInLayout) { - resumeFragment(fragment) - } - } + if (memoPreferences.isLastLaunchSuccess()) { + showFragment(savedInstanceState) + } else { + showRetrySelectRootDialog( + rootPath = memoPreferences.getPath(), + savedInstanceState = savedInstanceState, + ) } } } } + } - val storageFolderExisting = memoPreferences.getNotesStorage().exists() - if (memoPreferences.storageNotAvailable()) { - if (!storageFolderExisting) { - showNoNoteStorageDialog(RootNotFound(rootPath = memoPreferences.getPath())) - } else { - FilePickerDialog.show(this, supportFragmentManager) - } - - supportFragmentManager.onArkPathPicked(this) { - memoPreferences.storePath(it.toString()) - showFragment() + private fun showFragment(savedInstanceState: Bundle?) { + val textDataFromIntent = intent?.getStringExtra(Intent.EXTRA_TEXT) + if (textDataFromIntent != null) { + fragment = EditTextNotesFragment.newInstance(textDataFromIntent) + supportFragmentManager.beginTransaction().apply { + replace(fragContainer, fragment, EditTextNotesFragment.TAG) + commit() } } else { - showFragment() + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction().apply { + add(fragContainer, fragment, NotesFragment.TAG) + commit() + } + } else { + supportFragmentManager.apply { + val tag = savedInstanceState.getString(CURRENT_FRAGMENT_TAG) + findFragmentByTag(tag)?.let { + fragment = it + if (!fragment.isInLayout) { + resumeFragment(fragment) + } + } + } + } } } @@ -133,6 +130,35 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { loadFailDialog.show(supportFragmentManager, CommonActionDialog.TAG) } + private fun showRetrySelectRootDialog( + rootPath: String, + savedInstanceState: Bundle?, + ) { + val loadFailDialog = + CommonActionDialog( + title = getString(R.string.error_load_notes_crash_title), + message = getString(R.string.error_load_notes_crash_description, rootPath), + positiveText = R.string.error_load_notes_failed_retry_action, + negativeText = R.string.error_load_notes_failed_negative_action, + neutralText = R.string.error_load_notes_failed_positive_action, + isAlert = false, + enableNeutralOption = true, + onPositiveClick = { + showFragment(savedInstanceState) + }, + onNegativeClicked = { + finish() + }, + onNeutralClicked = { + FilePickerDialog.show(this, supportFragmentManager) + }, + onCloseClicked = { + finish() + }, + ) + loadFailDialog.show(supportFragmentManager, CommonActionDialog.TAG) + } + override fun onSaveInstanceState(outState: Bundle) { outState.putString(CURRENT_FRAGMENT_TAG, fragment.tag) super.onSaveInstanceState(outState) diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/CommonActionDialog.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/CommonActionDialog.kt index 06813c9c..b5c46574 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/CommonActionDialog.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/CommonActionDialog.kt @@ -8,6 +8,7 @@ import androidx.annotation.StringRes import androidx.fragment.app.DialogFragment import dev.arkbuilders.arkmemo.R import dev.arkbuilders.arkmemo.databinding.DialogCommonActionBinding +import dev.arkbuilders.arkmemo.utils.visible /** * This is a common action dialog that can be used inside app. @@ -18,9 +19,12 @@ class CommonActionDialog( private val message: String, @StringRes private val positiveText: Int, @StringRes private val negativeText: Int, + @StringRes private val neutralText: Int? = null, private val isAlert: Boolean = false, + private val enableNeutralOption: Boolean = false, private val onPositiveClick: (() -> Unit)? = null, private val onNegativeClicked: (() -> Unit)? = null, + private val onNeutralClicked: (() -> Unit)? = null, private val onCloseClicked: (() -> Unit)? = null, ) : DialogFragment() { companion object { @@ -48,10 +52,15 @@ class CommonActionDialog( mBinding.tvPositive.setBackgroundResource(R.drawable.bg_red_button) } + if (enableNeutralOption) { + mBinding.tvNeutral.visible() + } + mBinding.tvTitle.text = title mBinding.tvMessage.text = message mBinding.tvPositive.setText(positiveText) mBinding.tvNegative.setText(negativeText) + neutralText?.let { mBinding.tvNeutral.setText(neutralText) } mBinding.ivClose.setOnClickListener { onCloseClicked?.invoke() dismiss() @@ -66,6 +75,11 @@ class CommonActionDialog( onNegativeClicked?.invoke() dismiss() } + + mBinding.tvNeutral.setOnClickListener { + onNeutralClicked?.invoke() + dismiss() + } } override fun onResume() { diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/FilePickerDialog.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/FilePickerDialog.kt index eb65bc29..ff168ce8 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/FilePickerDialog.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/FilePickerDialog.kt @@ -1,18 +1,13 @@ package dev.arkbuilders.arkmemo.ui.dialogs -import android.Manifest -import android.content.pm.PackageManager -import android.os.Build import android.os.Bundle -import android.os.Environment -import androidx.activity.result.ActivityResultLauncher import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentManager import dagger.hilt.android.AndroidEntryPoint -import dev.arkbuilders.arkmemo.BuildConfig import dev.arkbuilders.arkmemo.R import dev.arkbuilders.arkmemo.preferences.MemoPreferences +import dev.arkbuilders.arkmemo.utils.Permission +import dev.arkbuilders.arkmemo.utils.PermissionManager import dev.arkbuilders.components.filepicker.ArkFilePickerConfig import dev.arkbuilders.components.filepicker.ArkFilePickerFragment import dev.arkbuilders.components.filepicker.ArkFilePickerMode @@ -39,8 +34,7 @@ class FilePickerDialog : ArkFilePickerFragment() { companion object { private const val TAG = "file_picker" private lateinit var fragmentManager: FragmentManager - var readPermLauncher: ActivityResultLauncher? = null - var readPermLauncherSdkR: ActivityResultLauncher? = null + var permissionManager: PermissionManager? = null fun show() { newInstance(getFilePickerConfig()).show(fragmentManager, TAG) @@ -51,10 +45,16 @@ class FilePickerDialog : ArkFilePickerFragment() { fragmentManager: FragmentManager, ) { Companion.fragmentManager = fragmentManager - if (isReadPermissionGranted(activity)) { + if (Permission.hasStoragePermission(activity)) { show() } else { - askForReadPermissions() + permissionManager?.askForWriteStorage { granted -> + if (granted) { + show() + } else { + activity.finish() + } + } } } @@ -63,24 +63,6 @@ class FilePickerDialog : ArkFilePickerFragment() { setConfig(config) } - private fun isReadPermissionGranted(activity: AppCompatActivity): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Environment.isExternalStorageManager() - } else { - ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) == - PackageManager.PERMISSION_GRANTED - } - } - - private fun askForReadPermissions() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val packageUri = "package:" + BuildConfig.APPLICATION_ID - readPermLauncherSdkR?.launch(packageUri) - } else { - readPermLauncher?.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) - } - } - private fun getFilePickerConfig() = ArkFilePickerConfig( mode = ArkFilePickerMode.FOLDER, diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkMediaPlayerFragment.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkMediaPlayerFragment.kt index 432d03a1..becfb7c0 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkMediaPlayerFragment.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkMediaPlayerFragment.kt @@ -59,6 +59,9 @@ class ArkMediaPlayerFragment : BaseEditNoteFragment() { return false } + override fun onViewRestoredWithNote(note: Note) { + } + private fun initUI() { binding.toolbar.ivRightActionIcon.setOnClickListener { showDeleteNoteDialog(note) diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkRecorderFragment.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkRecorderFragment.kt index 641d4874..04690915 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkRecorderFragment.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkRecorderFragment.kt @@ -117,7 +117,6 @@ class ArkRecorderFragment : BaseEditNoteFragment() { } private fun observeDataStates() { - notesViewModel.init {} observeRecordingState() observeSaveResult(notesViewModel.getSaveNoteResultLiveData()) } @@ -400,6 +399,11 @@ class ArkRecorderFragment : BaseEditNoteFragment() { ) } + override fun onViewRestoredWithNote(note: Note) { + setNote(note as VoiceNote) + initExistingNoteUI() + } + private fun saveNote() { notesViewModel.onSaveClick(createNewNote(), parentNote = note) { show -> activity.showProgressBar(show) diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/BaseEditNoteFragment.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/BaseEditNoteFragment.kt index f5842a0c..e185d0de 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/BaseEditNoteFragment.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/BaseEditNoteFragment.kt @@ -6,13 +6,16 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels +import dev.arkbuilders.arklib.ResourceId import dev.arkbuilders.arkmemo.R import dev.arkbuilders.arkmemo.databinding.FragmentEditNotesBinding import dev.arkbuilders.arkmemo.models.Note +import dev.arkbuilders.arkmemo.models.NoteType import dev.arkbuilders.arkmemo.ui.activities.MainActivity import dev.arkbuilders.arkmemo.ui.dialogs.CommonActionDialog import dev.arkbuilders.arkmemo.ui.viewmodels.NotesViewModel import dev.arkbuilders.arkmemo.ui.views.toast +import dev.arkbuilders.arkmemo.utils.getParcelableCompat import dev.arkbuilders.arkmemo.utils.gone import dev.arkbuilders.arkmemo.utils.visible import java.util.Calendar @@ -23,6 +26,10 @@ abstract class BaseEditNoteFragment : BaseFragment() { val notesViewModel: NotesViewModel by activityViewModels() val hostActivity by lazy { activity as MainActivity } + companion object { + const val BUNDLE_KEY_NOTE_ID = "bundle_key_note_id" + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -174,6 +181,36 @@ abstract class BaseEditNoteFragment : BaseFragment() { handleBackPressed() } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putParcelable(BUNDLE_KEY_NOTE_ID, getCurrentNote().resource?.id) + } + + override fun onViewStateRestored(savedInstanceState: Bundle?) { + super.onViewStateRestored(savedInstanceState) + val noteId = savedInstanceState?.getParcelableCompat(BUNDLE_KEY_NOTE_ID, ResourceId::class.java) + noteId?.let { + hostActivity.permissionManager.askForWriteStorage { granted -> + if (granted) { + notesViewModel.init { + val noteType = + when (this@BaseEditNoteFragment) { + is EditGraphicNotesFragment -> NoteType.GRAPHIC + is ArkRecorderFragment -> NoteType.VOICE + is EditTextNotesFragment -> NoteType.TEXT + else -> NoteType.TEXT + } + + notesViewModel.findNote(id = noteId, type = noteType) { foundNote -> + foundNote ?: return@findNote + onViewRestoredWithNote(foundNote) + } + } + } + } + } + } + abstract fun createNewNote(): Note abstract fun getCurrentNote(): Note @@ -181,4 +218,6 @@ abstract class BaseEditNoteFragment : BaseFragment() { abstract fun isContentChanged(): Boolean abstract fun isContentEmpty(): Boolean + + abstract fun onViewRestoredWithNote(note: Note) } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt index 45d22798..15db7b2e 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt @@ -66,7 +66,6 @@ class EditGraphicNotesFragment : BaseEditNoteFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - notesViewModel.init {} arguments?.getParcelableCompat(GRAPHICAL_NOTE_KEY, GraphicNote::class.java)?.let { note = it graphicNotesViewModel.onNoteOpened(note) @@ -173,6 +172,12 @@ class EditGraphicNotesFragment : BaseEditNoteFragment() { return graphicNotesViewModel.svg().getPaths().isEmpty() } + override fun onViewRestoredWithNote(note: Note) { + this.note = note as GraphicNote + graphicNotesViewModel.onNoteOpened(note) + binding.notesCanvas.invalidate() + } + private fun initBottomControls() { val tvBrushSize = binding.layoutGraphicsControl.tvBrushSize tvBrushSize.setOnClickListener { diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt index fba1b95f..2ec0fd8d 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt @@ -69,7 +69,6 @@ class EditTextNotesFragment : BaseEditNoteFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - notesViewModel.init {} if (arguments != null) { requireArguments().getParcelableCompat(NOTE_KEY, TextNote::class.java)?.let { @@ -167,6 +166,9 @@ class EditTextNotesFragment : BaseEditNoteFragment() { return binding.editNote.text.toString().trim().isEmpty() } + override fun onViewRestoredWithNote(note: Note) { + } + override fun onDestroyView() { super.onDestroyView() view?.viewTreeObserver?.removeOnWindowFocusChangeListener(windowFocusedListener) diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt index 3b98ab16..afa1b43e 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt @@ -168,10 +168,21 @@ class NotesFragment : BaseFragment() { initEmptyStateViews() binding.pbLoading.visible() - notesViewModel.apply { - init { - readAllNotes { - onNotesLoaded(it) + activity.permissionManager.askForWriteStorage { granted -> + if (!granted) { + if (isVisible) { + activity.finish() + } + return@askForWriteStorage + } else { + notesViewModel.apply { + setLastLaunchSuccess(false) + init { + readAllNotes { + onNotesLoaded(it) + setLastLaunchSuccess(true) + } + } } } } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/GraphicNotesViewModel.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/GraphicNotesViewModel.kt index ca05b1b2..269d7aa4 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/GraphicNotesViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/GraphicNotesViewModel.kt @@ -41,8 +41,10 @@ class GraphicNotesViewModel fun onNoteOpened(note: GraphicNote) { viewModelScope.launch { if (editPaths.isNotEmpty()) editPaths.clear() - editPaths.addAll(note.svg?.getPaths()!!) - svg = note.svg.copy() + note.svg?.getPaths()?.let { paths -> + editPaths.addAll(paths) + svg = note.svg.copy() + } } } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt index e3cf5436..9b186849 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt @@ -9,6 +9,7 @@ import dev.arkbuilders.arklib.ResourceId import dev.arkbuilders.arkmemo.di.IO_DISPATCHER import dev.arkbuilders.arkmemo.models.GraphicNote import dev.arkbuilders.arkmemo.models.Note +import dev.arkbuilders.arkmemo.models.NoteType import dev.arkbuilders.arkmemo.models.SaveNoteResult import dev.arkbuilders.arkmemo.models.TextNote import dev.arkbuilders.arkmemo.models.VoiceNote @@ -40,15 +41,16 @@ class NotesViewModel private val mSaveNoteResultLiveData = MutableLiveData() private var searchJob: Job? = null - @Inject - lateinit var memoPreferences: MemoPreferences + @set:Inject + internal lateinit var memoPreferences: MemoPreferences fun init(extraBlock: () -> Unit) { + val root = memoPreferences.getPath() val initJob = viewModelScope.launch(iODispatcher) { - textNotesRepo.init() - graphicNotesRepo.init() - voiceNotesRepo.init() + textNotesRepo.init(root) + graphicNotesRepo.init(root) + voiceNotesRepo.init(root) } viewModelScope.launch { initJob.join() @@ -68,6 +70,25 @@ class NotesViewModel } } + fun findNote( + id: ResourceId, + type: NoteType, + onResult: (note: Note?) -> Unit, + ) { + viewModelScope.launch(iODispatcher) { + val noteRepo = + when (type) { + NoteType.TEXT -> textNotesRepo + NoteType.VOICE -> voiceNotesRepo + NoteType.GRAPHIC -> graphicNotesRepo + } + val note = noteRepo.findNote(id) + withContext(Dispatchers.Main) { + onResult.invoke(note) + } + } + } + fun searchNote( keyword: String, onSuccess: (notes: List) -> Unit, @@ -191,4 +212,8 @@ class NotesViewModel fun getStorageFolderPath(): String { return memoPreferences.getPath() } + + fun setLastLaunchSuccess(success: Boolean) { + memoPreferences.setLastLaunchSuccess(success) + } } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/utils/Permission.kt b/app/src/main/java/dev/arkbuilders/arkmemo/utils/Permission.kt index fa06017c..b07e9eef 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/utils/Permission.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/utils/Permission.kt @@ -1,7 +1,11 @@ package dev.arkbuilders.arkmemo.utils +import android.Manifest import android.content.Context import android.content.pm.PackageManager +import android.os.Build +import android.os.Environment +import androidx.core.content.ContextCompat object Permission { fun hasPermission( @@ -10,4 +14,15 @@ object Permission { ): Boolean { return context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED } + + fun hasStoragePermission(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Environment.isExternalStorageManager() + } else { + ContextCompat.checkSelfPermission( + context, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + ) == PackageManager.PERMISSION_GRANTED + } + } } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/utils/PermissionManager.kt b/app/src/main/java/dev/arkbuilders/arkmemo/utils/PermissionManager.kt new file mode 100644 index 00000000..17c0fa6d --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/arkmemo/utils/PermissionManager.kt @@ -0,0 +1,38 @@ +package dev.arkbuilders.arkmemo.utils + +import android.Manifest +import android.os.Build +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import dev.arkbuilders.arkmemo.BuildConfig +import dev.arkbuilders.arkmemo.contracts.PermissionContract + +class PermissionManager(val activity: ComponentActivity) { + private var permissionResultCallback: ((granted: Boolean) -> Unit)? = null + private val permissionLauncher: ActivityResultLauncher = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + activity.registerForActivityResult(PermissionContract()) { isGranted -> + permissionResultCallback?.invoke(isGranted) + } + } else { + activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + permissionResultCallback?.invoke(isGranted) + } + } + + fun askForWriteStorage(onResult: ((granted: Boolean) -> Unit)? = null) { + if (Permission.hasStoragePermission(activity)) { + onResult?.invoke(true) + return + } + + permissionResultCallback = onResult + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val packageUri = "package:" + BuildConfig.APPLICATION_ID + permissionLauncher.launch(packageUri) + } else { + permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } +} diff --git a/app/src/main/res/layout/dialog_common_action.xml b/app/src/main/res/layout/dialog_common_action.xml index 045ec13e..d027b60a 100644 --- a/app/src/main/res/layout/dialog_common_action.xml +++ b/app/src/main/res/layout/dialog_common_action.xml @@ -55,6 +55,21 @@ android:layout_marginTop="24dp" android:id="@+id/tv_positive"/> + + Select All Deselect All Cannot find notes + Could not load notes The folder %s with notes data cannot be located.\nPlease select a new folder. Select Leave + Retry + There\'s a crash while loading the folder %s.\nPlease retry or select a new folder. \ No newline at end of file