diff --git a/app/build.gradle b/app/build.gradle index 50dbbf7..cf5e4ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,13 +41,16 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'androidx.activity:activity-ktx:1.2.0' - implementation 'androidx.fragment:fragment-ktx:1.3.0' + implementation 'androidx.activity:activity-ktx:1.2.1' + implementation 'androidx.fragment:fragment-ktx:1.3.1' implementation 'com.google.android.material:material:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0' implementation 'androidx.preference:preference-ktx:1.1.1' implementation "com.google.dagger:hilt-android:$hilt_version" + + implementation project(":data-lib") + kapt "com.google.dagger:hilt-android-compiler:$hilt_version" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 40f95bc..fd5d354 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + android:theme="@style/Theme.Notian" + android:requestLegacyExternalStorage="true"> diff --git a/app/src/main/java/me/profiluefter/profinote/HiltModule.kt b/app/src/main/java/me/profiluefter/profinote/HiltModule.kt index a4d257a..54aace1 100644 --- a/app/src/main/java/me/profiluefter/profinote/HiltModule.kt +++ b/app/src/main/java/me/profiluefter/profinote/HiltModule.kt @@ -34,10 +34,12 @@ object PreferenceBasedModule { @Provides fun storageBinding( @ApplicationContext context: Context, - private: Provider + private: Provider, + external: Provider ): Storage = when (PreferenceManager.getDefaultSharedPreferences(context) .getString("storageLocation", "privateFile")) { "privateFile" -> private.get() + "externalStorage" -> external.get() else -> null!! } } \ No newline at end of file diff --git a/app/src/main/java/me/profiluefter/profinote/activities/MainActivity.kt b/app/src/main/java/me/profiluefter/profinote/activities/MainActivity.kt index e7ebe77..54d3c26 100644 --- a/app/src/main/java/me/profiluefter/profinote/activities/MainActivity.kt +++ b/app/src/main/java/me/profiluefter/profinote/activities/MainActivity.kt @@ -1,8 +1,11 @@ package me.profiluefter.profinote.activities +import android.Manifest import android.annotation.SuppressLint import android.content.Intent +import android.content.pm.PackageManager import android.os.Bundle +import android.os.Environment import android.view.Menu import android.view.MenuItem import android.view.View @@ -11,27 +14,30 @@ import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.menu.MenuBuilder import androidx.databinding.DataBindingUtil +import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import me.profiluefter.profinote.R +import me.profiluefter.profinote.data.Note import me.profiluefter.profinote.databinding.ActivityMainBinding import me.profiluefter.profinote.models.MainActivityViewModel -import me.profiluefter.profinote.models.Note @AndroidEntryPoint class MainActivity : AppCompatActivity() { private val viewModel: MainActivityViewModel by viewModels() private val editorActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if(it.resultCode != RESULT_OK || it.data == null) return@registerForActivityResult + if (it.resultCode != RESULT_OK || it.data == null) return@registerForActivityResult val note = it.data!!.getSerializableExtra("note") as Note val position = it.data!!.getIntExtra("position", -1) viewModel.setNote(position, note) } + private val permissionRequestCode = 187 + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding: ActivityMainBinding = @@ -59,7 +65,7 @@ class MainActivity : AppCompatActivity() { override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.main_menu, menu) - if(menu is MenuBuilder) { + if (menu is MenuBuilder) { menu.setOptionalIconsVisible(true) } @@ -69,10 +75,30 @@ class MainActivity : AppCompatActivity() { fun onNewNote(item: MenuItem) = onNewNote() fun onSaveNotes(item: MenuItem) { + val usesExternalStorage = PreferenceManager.getDefaultSharedPreferences(this).getString("storageLocation", "") + .equals("externalStorage") + if (usesExternalStorage && checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), permissionRequestCode) + return + } + if (!checkExternalStorage()) return viewModel.saveNotes() Snackbar.make(findViewById(R.id.notes), R.string.saved_notes, Snackbar.LENGTH_SHORT).show() } + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode != permissionRequestCode) return + if (grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if(!checkExternalStorage()) return + viewModel.saveNotes() + } else Snackbar.make(findViewById(R.id.notes), R.string.permission_denied, Snackbar.LENGTH_SHORT) + .setAction(R.string.grant_permission) { + requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), permissionRequestCode) + } + .show() + } + fun onEditNote(index: Int) { val intent = Intent(this, NoteEditorActivity::class.java) intent.putExtra("position", index) @@ -107,4 +133,11 @@ class MainActivity : AppCompatActivity() { intent.putExtra("position", -1) editorActivity.launch(intent) } + + private fun checkExternalStorage(): Boolean = + (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED).also { + if (!it) { + Snackbar.make(findViewById(R.id.notes), R.string.no_sd_card, Snackbar.LENGTH_SHORT).show() + } + } } \ No newline at end of file diff --git a/app/src/main/java/me/profiluefter/profinote/activities/NoteDetailsActivity.kt b/app/src/main/java/me/profiluefter/profinote/activities/NoteDetailsActivity.kt index e14974e..85a6aaa 100644 --- a/app/src/main/java/me/profiluefter/profinote/activities/NoteDetailsActivity.kt +++ b/app/src/main/java/me/profiluefter/profinote/activities/NoteDetailsActivity.kt @@ -6,7 +6,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import me.profiluefter.profinote.R import me.profiluefter.profinote.databinding.ActivityNoteDetailsBinding -import me.profiluefter.profinote.models.Note +import me.profiluefter.profinote.data.Note class NoteDetailsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/me/profiluefter/profinote/activities/NotesAdapter.kt b/app/src/main/java/me/profiluefter/profinote/activities/NotesAdapter.kt index 8bd548f..c90b780 100644 --- a/app/src/main/java/me/profiluefter/profinote/activities/NotesAdapter.kt +++ b/app/src/main/java/me/profiluefter/profinote/activities/NotesAdapter.kt @@ -10,7 +10,7 @@ import androidx.databinding.BindingAdapter import androidx.recyclerview.widget.RecyclerView import me.profiluefter.profinote.R import me.profiluefter.profinote.databinding.RecyclerViewItemBinding -import me.profiluefter.profinote.models.Note +import me.profiluefter.profinote.data.Note class NotesAdapter(notes: List, private val context: MainActivity) : RecyclerView.Adapter() { diff --git a/app/src/main/java/me/profiluefter/profinote/data/CSVSerializer.kt b/app/src/main/java/me/profiluefter/profinote/data/CSVSerializer.kt index eb26aec..598dde6 100644 --- a/app/src/main/java/me/profiluefter/profinote/data/CSVSerializer.kt +++ b/app/src/main/java/me/profiluefter/profinote/data/CSVSerializer.kt @@ -1,7 +1,6 @@ package me.profiluefter.profinote.data import android.util.Log -import me.profiluefter.profinote.models.Note import java.net.URLDecoder import java.net.URLEncoder import javax.inject.Inject diff --git a/app/src/main/java/me/profiluefter/profinote/data/ExternalStorage.kt b/app/src/main/java/me/profiluefter/profinote/data/ExternalStorage.kt new file mode 100644 index 0000000..332b623 --- /dev/null +++ b/app/src/main/java/me/profiluefter/profinote/data/ExternalStorage.kt @@ -0,0 +1,37 @@ +package me.profiluefter.profinote.data + +import android.content.Context +import android.util.Log +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileNotFoundException +import javax.inject.Inject +import javax.inject.Named + +private const val TAG = "ExternalStorage" + +class ExternalStorage @Inject constructor( + @ApplicationContext private val context: Context, + @Named("fileName") private val fileName: String +) : Storage { + override suspend fun store(data: ByteArray) = withContext(Dispatchers.IO) { + val externalStorage = context.getExternalFilesDir(null) + val file = File(externalStorage!!.absolutePath + File.separator + fileName) + Log.i(TAG, "Saving data to ${file.absolutePath}!") + file.writeBytes(data) + } + + override suspend fun get(): ByteArray? = withContext(Dispatchers.IO) { + val externalStorage = context.getExternalFilesDir(null) + try { + val file = File(externalStorage!!.absolutePath + File.separator + fileName) + Log.i(TAG, "Trying to read data from ${file.absolutePath}!") + file.readBytes() + } catch (e: FileNotFoundException) { + Log.w(TAG, "File not found.") + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/profiluefter/profinote/data/SampleSerializer.kt b/app/src/main/java/me/profiluefter/profinote/data/SampleSerializer.kt index 0f727f6..f4038af 100644 --- a/app/src/main/java/me/profiluefter/profinote/data/SampleSerializer.kt +++ b/app/src/main/java/me/profiluefter/profinote/data/SampleSerializer.kt @@ -1,7 +1,6 @@ package me.profiluefter.profinote.data import android.util.Log -import me.profiluefter.profinote.models.Note import javax.inject.Inject import kotlin.math.min import kotlin.random.Random diff --git a/app/src/main/java/me/profiluefter/profinote/models/MainActivityViewModel.kt b/app/src/main/java/me/profiluefter/profinote/models/MainActivityViewModel.kt index 5f14e6f..9451761 100644 --- a/app/src/main/java/me/profiluefter/profinote/models/MainActivityViewModel.kt +++ b/app/src/main/java/me/profiluefter/profinote/models/MainActivityViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import me.profiluefter.profinote.data.Note import me.profiluefter.profinote.data.Serializer import javax.inject.Inject import javax.inject.Provider diff --git a/app/src/main/java/me/profiluefter/profinote/models/NoteEditorActivityViewModel.kt b/app/src/main/java/me/profiluefter/profinote/models/NoteEditorActivityViewModel.kt index e856662..e479616 100644 --- a/app/src/main/java/me/profiluefter/profinote/models/NoteEditorActivityViewModel.kt +++ b/app/src/main/java/me/profiluefter/profinote/models/NoteEditorActivityViewModel.kt @@ -4,6 +4,7 @@ import android.content.Intent import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import me.profiluefter.profinote.data.* import java.util.* class NoteEditorActivityViewModelFactory(private val intent: Intent) : ViewModelProvider.Factory { diff --git a/app/src/main/res/layout/activity_note_details.xml b/app/src/main/res/layout/activity_note_details.xml index f0a300c..935d054 100644 --- a/app/src/main/res/layout/activity_note_details.xml +++ b/app/src/main/res/layout/activity_note_details.xml @@ -5,11 +5,11 @@ - + + type="me.profiluefter.profinote.data.Note" /> - + + type="me.profiluefter.profinote.data.Note" /> @string/storage_location_private_file + @string/storage_location_external_storage privateFile + externalStorage \ 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 b0238ce..995ae0e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -26,4 +26,8 @@ Preferences File Name + Permission denied + Grant permission + No SD-Card inserted + External Storage \ No newline at end of file diff --git a/data-lib/.gitignore b/data-lib/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/data-lib/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data-lib/build.gradle b/data-lib/build.gradle new file mode 100644 index 0000000..87f70a3 --- /dev/null +++ b/data-lib/build.gradle @@ -0,0 +1,31 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' + id 'java-library' + id 'org.jetbrains.dokka' version '1.4.20' +} + +dependencies { + implementation 'javax.inject:javax.inject:1' +} + +task srcZip(type: Zip) { + archiveAppendix = "src" + destinationDirectory = file("$buildDir/output") + from sourceSets*.allSource +} + +jar { + destinationDirectory = file("$buildDir/output") +} + +tasks.named("dokkaHtml") { + outputDirectory = file("$buildDir/dokka") +} + +task docZip(type: Zip, dependsOn: [dokkaHtml]) { + archiveAppendix = "doc" + destinationDirectory = file("$buildDir/output") + from file("$buildDir/dokka") +} + +task generateZIPs(dependsOn: [jar, srcZip, docZip]) \ No newline at end of file diff --git a/app/src/main/java/me/profiluefter/profinote/data/BinarySerializer.kt b/data-lib/src/main/java/me/profiluefter/profinote/data/BinarySerializer.kt similarity index 81% rename from app/src/main/java/me/profiluefter/profinote/data/BinarySerializer.kt rename to data-lib/src/main/java/me/profiluefter/profinote/data/BinarySerializer.kt index 0fb96a1..8dd1633 100644 --- a/app/src/main/java/me/profiluefter/profinote/data/BinarySerializer.kt +++ b/data-lib/src/main/java/me/profiluefter/profinote/data/BinarySerializer.kt @@ -1,6 +1,5 @@ package me.profiluefter.profinote.data -import me.profiluefter.profinote.models.Note import java.nio.BufferUnderflowException import java.nio.ByteBuffer import javax.inject.Inject @@ -22,15 +21,17 @@ class BinarySerializer @Inject constructor(private val storage: Storage) : Seria } repeat(size) { - notes.add(Note( - readString(), - buffer.get().toInt(), - buffer.get().toInt(), - buffer.get().toInt(), - buffer.get().toInt(), - buffer.short.toInt(), - readString() - )) + notes.add( + Note( + readString(), + buffer.get().toInt(), + buffer.get().toInt(), + buffer.get().toInt(), + buffer.get().toInt(), + buffer.short.toInt(), + readString() + ) + ) } return notes diff --git a/app/src/main/java/me/profiluefter/profinote/models/Note.kt b/data-lib/src/main/java/me/profiluefter/profinote/data/Note.kt similarity index 96% rename from app/src/main/java/me/profiluefter/profinote/models/Note.kt rename to data-lib/src/main/java/me/profiluefter/profinote/data/Note.kt index fa59dc5..9fb6263 100644 --- a/app/src/main/java/me/profiluefter/profinote/models/Note.kt +++ b/data-lib/src/main/java/me/profiluefter/profinote/data/Note.kt @@ -1,4 +1,4 @@ -package me.profiluefter.profinote.models +package me.profiluefter.profinote.data import java.io.Serializable import java.util.* diff --git a/app/src/main/java/me/profiluefter/profinote/data/Serializer.kt b/data-lib/src/main/java/me/profiluefter/profinote/data/Serializer.kt similarity index 75% rename from app/src/main/java/me/profiluefter/profinote/data/Serializer.kt rename to data-lib/src/main/java/me/profiluefter/profinote/data/Serializer.kt index 3253a63..d312e1a 100644 --- a/app/src/main/java/me/profiluefter/profinote/data/Serializer.kt +++ b/data-lib/src/main/java/me/profiluefter/profinote/data/Serializer.kt @@ -1,7 +1,5 @@ package me.profiluefter.profinote.data -import me.profiluefter.profinote.models.Note - interface Serializer { suspend fun load(): List suspend fun save(notes: List) diff --git a/app/src/main/java/me/profiluefter/profinote/data/Storage.kt b/data-lib/src/main/java/me/profiluefter/profinote/data/Storage.kt similarity index 78% rename from app/src/main/java/me/profiluefter/profinote/data/Storage.kt rename to data-lib/src/main/java/me/profiluefter/profinote/data/Storage.kt index 99b966e..4d50cf7 100644 --- a/app/src/main/java/me/profiluefter/profinote/data/Storage.kt +++ b/data-lib/src/main/java/me/profiluefter/profinote/data/Storage.kt @@ -1,5 +1,8 @@ package me.profiluefter.profinote.data +/** + * A binary storage backend + */ interface Storage { suspend fun store(data: ByteArray) suspend fun get(): ByteArray? diff --git a/settings.gradle b/settings.gradle index c81b9b3..5e7e859 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,3 @@ include ':app' +include ':data-lib' rootProject.name = "Notian" \ No newline at end of file