diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 52c299bf..9de38fa0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,7 +4,6 @@ plugins { id ("kotlin-parcelize") id ("kotlin-kapt") id ("com.google.dagger.hilt.android") - id ("com.google.gms.google-services") id ("com.google.devtools.ksp") } @@ -16,8 +15,8 @@ android { applicationId = "com.aritra.notify" minSdk = 24 targetSdk = 34 - versionCode = 4 - versionName = "1.3.0-beta01" + versionCode = 5 + versionName = "1.3.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -57,9 +56,9 @@ android { dependencies { - val lifecycleVersion = "2.6.1" + val lifecycleVersion = "2.6.2" val roomVersion = "2.5.2" - val navVersion = "2.7.0" + val navVersion = "2.7.2" implementation ("androidx.core:core-ktx:1.10.1") @@ -91,7 +90,7 @@ dependencies { // Material 3 implementation ("androidx.compose.material3:material3:1.1.1") implementation ("androidx.compose.material3:material3-window-size-class:1.1.1") - implementation ("androidx.compose.material:material-icons-extended:1.5.0") + implementation ("androidx.compose.material:material-icons-extended:1.5.1") // Room implementation ("androidx.room:room-runtime:$roomVersion") @@ -115,15 +114,9 @@ dependencies { // DataStore implementation ("androidx.datastore:datastore-preferences:1.0.0") - //Swipe - implementation ("me.saket.swipe:swipe:1.2.0") - // Splash API implementation ("androidx.core:core-splashscreen:1.0.1") - // Firebase - implementation("com.google.firebase:firebase-messaging-ktx:23.2.1") - //Coil implementation("io.coil-kt:coil-compose:2.4.0") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eb2da3b8..0aa2c15c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,7 +4,6 @@ - - - - - - diff --git a/app/src/main/java/com/aritra/notify/NotifyMessagingService.kt b/app/src/main/java/com/aritra/notify/NotifyMessagingService.kt deleted file mode 100644 index 34d158e5..00000000 --- a/app/src/main/java/com/aritra/notify/NotifyMessagingService.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.aritra.notify - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.NotificationManager.IMPORTANCE_DEFAULT -import android.app.NotificationManager.IMPORTANCE_HIGH -import android.app.PendingIntent -import android.app.PendingIntent.FLAG_IMMUTABLE -import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP -import android.os.Build -import android.util.Log -import androidx.core.app.NotificationCompat -import com.aritra.notify.ui.screens.MainActivity -import com.google.firebase.messaging.FirebaseMessagingService -import com.google.firebase.messaging.RemoteMessage -import kotlin.random.Random - -class NotifyMessagingService : FirebaseMessagingService() { - - private val random = Random - override fun onMessageReceived(remoteMessage: RemoteMessage) { - remoteMessage.notification?.let { message -> - sendNotification(message) - } - } - - private fun sendNotification(message: RemoteMessage.Notification) { - val intent = Intent(this, MainActivity::class.java).apply { - addFlags(FLAG_ACTIVITY_CLEAR_TOP) - } - - val pendingIntent = PendingIntent.getActivity( - this, 0, intent, FLAG_IMMUTABLE - ) - - val channelId = this.getString(R.string.default_notification_channel_id) - - val notificationBuilder = NotificationCompat.Builder(this, channelId) - .setContentTitle(message.title) - .setContentText(message.body) - .setSmallIcon(R.drawable.notify_logo) - .setAutoCancel(true) - .setContentIntent(pendingIntent) - - val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel(channelId, CHANNEL_NAME, IMPORTANCE_HIGH) - manager.createNotificationChannel(channel) - } - - manager.notify(random.nextInt(), notificationBuilder.build()) - } - - override fun onNewToken(token: String) { - Log.d("FCM","New token: $token") - } - - companion object { - const val CHANNEL_NAME = "FCM notification channel" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/components/actions/SwipeDelete.kt b/app/src/main/java/com/aritra/notify/components/actions/SwipeDelete.kt deleted file mode 100644 index c2d54643..00000000 --- a/app/src/main/java/com/aritra/notify/components/actions/SwipeDelete.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.aritra.notify.components.actions - -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.aritra.notify.R -import com.aritra.notify.components.dialog.TextDialog -import com.aritra.notify.components.note.NotesCard -import com.aritra.notify.data.models.Note -import com.aritra.notify.ui.screens.notes.homeScreen.NoteScreenViewModel -import me.saket.swipe.SwipeAction -import me.saket.swipe.SwipeableActionsBox - -@Composable -fun SwipeDelete( - notesModel: Note, - viewModel: NoteScreenViewModel, - navigateToUpdateNoteScreen: (noteId: Int) -> Unit -) { - - val deleteDialogVisible = remember { mutableStateOf(false) } - - val delete = SwipeAction( - onSwipe = { - deleteDialogVisible.value = true - }, - icon = { - Icon( - modifier = Modifier.padding(12.dp), - painter = painterResource(R.drawable.ic_delete), - contentDescription = null, - tint = Color.White - ) - }, - background = Color.Red, - ) - - SwipeableActionsBox( - modifier = Modifier.padding(10.dp), - swipeThreshold = 100.dp, - endActions = listOf(delete) - ) { - NotesCard(notesModel, navigateToUpdateNoteScreen) - } - if (deleteDialogVisible.value) { - TextDialog( - title = stringResource(R.string.warning), - description = stringResource(R.string.are_you_sure_want_to_delete_these_items_it_cannot_be_recovered), - isOpened = deleteDialogVisible.value, - onDismissCallback = { deleteDialogVisible.value = false }, - onConfirmCallback = { - viewModel.deleteNote(notesModel) - deleteDialogVisible.value = false - } - ) - } -} diff --git a/app/src/main/java/com/aritra/notify/biometric/AppBioMetricManager.kt b/app/src/main/java/com/aritra/notify/components/biometric/AppBioMetricManager.kt similarity index 97% rename from app/src/main/java/com/aritra/notify/biometric/AppBioMetricManager.kt rename to app/src/main/java/com/aritra/notify/components/biometric/AppBioMetricManager.kt index c6c8725e..13be8f1f 100644 --- a/app/src/main/java/com/aritra/notify/biometric/AppBioMetricManager.kt +++ b/app/src/main/java/com/aritra/notify/components/biometric/AppBioMetricManager.kt @@ -1,4 +1,4 @@ -package com.aritra.notify.biometric +package com.aritra.notify.components.biometric import android.content.Context import androidx.biometric.BiometricManager diff --git a/app/src/main/java/com/aritra/notify/biometric/BiometricAuthListener.kt b/app/src/main/java/com/aritra/notify/components/biometric/BiometricAuthListener.kt similarity index 72% rename from app/src/main/java/com/aritra/notify/biometric/BiometricAuthListener.kt rename to app/src/main/java/com/aritra/notify/components/biometric/BiometricAuthListener.kt index 951c2ab2..7d1041b7 100644 --- a/app/src/main/java/com/aritra/notify/biometric/BiometricAuthListener.kt +++ b/app/src/main/java/com/aritra/notify/components/biometric/BiometricAuthListener.kt @@ -1,4 +1,4 @@ -package com.aritra.notify.biometric +package com.aritra.notify.components.biometric interface BiometricAuthListener { fun onBiometricAuthSuccess() diff --git a/app/src/main/java/com/aritra/notify/components/note/GridNoteCard.kt b/app/src/main/java/com/aritra/notify/components/note/GridNoteCard.kt index ca9d4015..ae71cddb 100644 --- a/app/src/main/java/com/aritra/notify/components/note/GridNoteCard.kt +++ b/app/src/main/java/com/aritra/notify/components/note/GridNoteCard.kt @@ -9,29 +9,17 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow @@ -40,24 +28,25 @@ import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import coil.request.ImageRequest import com.aritra.notify.R -import com.aritra.notify.components.dialog.TextDialog -import com.aritra.notify.data.models.Note -import com.aritra.notify.ui.screens.notes.homeScreen.NoteScreenViewModel +import com.aritra.notify.domain.models.Note import com.aritra.notify.utils.Const import java.text.SimpleDateFormat +import java.util.Date import java.util.Locale @Composable fun GridNoteCard( notesModel: Note, - viewModel: NoteScreenViewModel, navigateToUpdateNoteScreen: (noteId: Int) -> Unit, - isGridView: Boolean ) { - var expanded by remember { mutableStateOf(false) } - val deleteDialogVisible = remember { mutableStateOf(false) } - val painter = rememberSaveable { mutableStateOf(notesModel.imagePath) } + val painter = rememberSaveable { mutableStateOf(notesModel.image) } val context = LocalContext.current + val date = remember { + SimpleDateFormat( + Const.DATE_TIME_FORMAT, + Locale.getDefault() + ).format(notesModel.dateTime ?: Date()) + } OutlinedCard( border = CardDefaults.outlinedCardBorder().copy(0.dp), @@ -65,10 +54,12 @@ fun GridNoteCard( .padding(10.dp) .fillMaxWidth() .clickable { navigateToUpdateNoteScreen(notesModel.id) }, - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(15.dp) ) { Column( - modifier = Modifier.padding(12.dp).fillMaxWidth() + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() ) { AsyncImage( model = ImageRequest.Builder(context) @@ -95,45 +86,6 @@ fun GridNoteCard( overflow = TextOverflow.Ellipsis ) } - - if (isGridView) { - IconButton( - onClick = { expanded = true } - ) { - Icon(Icons.Default.MoreVert, "Options") - } - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - ) { - DropdownMenuItem( - text = { Text("Delete") }, - onClick = { - deleteDialogVisible.value = true - expanded = false - }, - leadingIcon = { - Icon( - modifier = Modifier.padding(12.dp), - imageVector = Icons.Outlined.Delete, - contentDescription = null - ) - } - ) - } - if (deleteDialogVisible.value) { - TextDialog( - title = stringResource(R.string.warning), - description = stringResource(R.string.are_you_sure_want_to_delete_these_items_it_cannot_be_recovered), - isOpened = deleteDialogVisible.value, - onDismissCallback = { deleteDialogVisible.value = false }, - onConfirmCallback = { - viewModel.deleteNote(notesModel) - deleteDialogVisible.value = false - } - ) - } - } } Spacer(modifier = Modifier.height(10.dp)) Text( @@ -144,13 +96,8 @@ fun GridNoteCard( overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.height(10.dp)) - val formattedDateTime = - SimpleDateFormat( - Const.DATE_TIME_FORMAT, - Locale.getDefault() - ).format(notesModel.dateTime) Text( - text = formattedDateTime, + text = date, fontSize = 14.sp, fontFamily = FontFamily(Font(R.font.poppins_medium)), color = Color.Gray diff --git a/app/src/main/java/com/aritra/notify/components/note/NoteCard.kt b/app/src/main/java/com/aritra/notify/components/note/NoteCard.kt index ef885a78..b7c6cf17 100644 --- a/app/src/main/java/com/aritra/notify/components/note/NoteCard.kt +++ b/app/src/main/java/com/aritra/notify/components/note/NoteCard.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text @@ -18,7 +17,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily @@ -28,7 +26,7 @@ import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import coil.request.ImageRequest import com.aritra.notify.R -import com.aritra.notify.data.models.Note +import com.aritra.notify.domain.models.Note import com.aritra.notify.utils.Const import java.text.SimpleDateFormat import java.util.Locale @@ -38,21 +36,21 @@ fun NotesCard( noteModel: Note, navigateToUpdateNoteScreen: (noteId: Int) -> Unit ) { - val painter = rememberSaveable { mutableStateOf(noteModel.imagePath) } + val painter = rememberSaveable { mutableStateOf(noteModel.image) } val context = LocalContext.current OutlinedCard( border = CardDefaults.outlinedCardBorder().copy(0.dp), modifier = Modifier - .padding(2.dp) + .padding(10.dp) .fillMaxHeight() .clickable { navigateToUpdateNoteScreen(noteModel.id) }, - shape = RoundedCornerShape(8.dp), + shape = RoundedCornerShape(15.dp), ) { Column( modifier = Modifier .fillMaxWidth() - .padding(12.dp) + .padding(16.dp) ) { AsyncImage( model = ImageRequest.Builder(context) diff --git a/app/src/main/java/com/aritra/notify/components/topbar/AddNoteTopBar.kt b/app/src/main/java/com/aritra/notify/components/topbar/AddNoteTopBar.kt index 2485f6c5..d18bdad5 100644 --- a/app/src/main/java/com/aritra/notify/components/topbar/AddNoteTopBar.kt +++ b/app/src/main/java/com/aritra/notify/components/topbar/AddNoteTopBar.kt @@ -1,8 +1,7 @@ package com.aritra.notify.components.topbar -import android.graphics.Bitmap -import android.widget.Toast +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -34,24 +33,19 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import com.aritra.notify.R import com.aritra.notify.components.actions.ShareOption -import com.aritra.notify.data.models.Note -import com.aritra.notify.ui.screens.notes.addNoteScreen.AddNoteViewModel import com.aritra.notify.utils.shareAsImage import com.aritra.notify.utils.shareAsPdf import com.aritra.notify.utils.shareNoteAsText -import java.util.Date @OptIn(ExperimentalMaterial3Api::class) @Composable fun AddNoteTopBar( - viewModel: AddNoteViewModel, - onBackPress: () -> Unit, - onSave: () -> Unit, title: String, description: String, - dateTime: Date, - imagePath: Bitmap?, + modifier: Modifier = Modifier, + onBackPress: () -> Unit, + saveNote: () -> Unit, ) { var showSheet by remember { mutableStateOf(false) } val context = LocalContext.current @@ -62,9 +56,14 @@ fun AddNoteTopBar( val view = LocalView.current val bitmapSize = view.width to view.height - + BackHandler { + if (title.isNotEmpty() && description.isNotEmpty()) { + saveNote() + } else onBackPress() + } CenterAlignedTopAppBar( + modifier = modifier, colors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = MaterialTheme.colorScheme.surface ), @@ -130,14 +129,7 @@ fun AddNoteTopBar( } } - IconButton(onClick = { - val noteDB = - Note(id = 0, title = title, note = description, dateTime = dateTime, imagePath = imagePath) - viewModel.insertNote(noteDB) - onSave() - Toast.makeText(context, "Successfully Saved!", Toast.LENGTH_SHORT).show() - - }) { + IconButton(onClick = saveNote) { Icon( painterResource(R.drawable.save), contentDescription = stringResource(R.string.save) diff --git a/app/src/main/java/com/aritra/notify/components/topbar/EditNoteTopBar.kt b/app/src/main/java/com/aritra/notify/components/topbar/EditNoteTopBar.kt index e1124d52..c381facd 100644 --- a/app/src/main/java/com/aritra/notify/components/topbar/EditNoteTopBar.kt +++ b/app/src/main/java/com/aritra/notify/components/topbar/EditNoteTopBar.kt @@ -1,7 +1,6 @@ package com.aritra.notify.components.topbar -import android.graphics.Bitmap -import android.widget.Toast +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -34,24 +33,20 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.aritra.notify.R import com.aritra.notify.components.actions.ShareOption import com.aritra.notify.components.dialog.TextDialog -import com.aritra.notify.data.models.Note -import com.aritra.notify.ui.screens.notes.editNoteScreen.EditScreenViewModel +import com.aritra.notify.domain.models.Note import com.aritra.notify.ui.screens.notes.homeScreen.NoteScreenViewModel import com.aritra.notify.utils.shareAsImage import com.aritra.notify.utils.shareAsPdf import com.aritra.notify.utils.shareNoteAsText -import java.util.Date @OptIn(ExperimentalMaterial3Api::class) @Composable fun EditNoteTopBar( note: Note, - viewModel: EditScreenViewModel, - noteId: Int, navigateBack: () -> Unit, title: String, description: String, - imagePath: Bitmap? + updateNote: () -> Unit, ) { val noteScreenViewModel = hiltViewModel() var showSheet by remember { mutableStateOf(false) } @@ -62,9 +57,10 @@ fun EditNoteTopBar( ) val view = LocalView.current val bitmapSize = view.width to view.height - val currentDateTime = Date() val deleteDialogVisible = remember { mutableStateOf(false) } + BackHandler(onBack = updateNote) + CenterAlignedTopAppBar( colors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = MaterialTheme.colorScheme.surface @@ -76,7 +72,7 @@ fun EditNoteTopBar( ) }, navigationIcon = { - IconButton(onClick = { navigateBack() }) { + IconButton(onClick = updateNote) { Icon( painterResource(R.drawable.back), contentDescription = stringResource(R.string.back) @@ -149,18 +145,12 @@ fun EditNoteTopBar( } } } - IconButton(onClick = { - val updateNote = Note(noteId, title, description, currentDateTime, imagePath) - viewModel.updateNotes(updateNote) - navigateBack() - Toast.makeText(context, "Successfully Updated!", Toast.LENGTH_SHORT).show() - }) { + IconButton(onClick = updateNote) { Icon( painterResource(R.drawable.save), contentDescription = stringResource(R.string.save) ) } - } ) } \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/data/converters/DateTypeConverter.kt b/app/src/main/java/com/aritra/notify/data/converters/DateTypeConverter.kt new file mode 100644 index 00000000..bb4b5cb1 --- /dev/null +++ b/app/src/main/java/com/aritra/notify/data/converters/DateTypeConverter.kt @@ -0,0 +1,29 @@ +package com.aritra.notify.data.converters + +import android.annotation.SuppressLint +import androidx.room.TypeConverter +import com.aritra.notify.utils.Const +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +object DateTypeConverter { + @SuppressLint("ConstantLocale") + private val displayDateFormat = SimpleDateFormat(Const.DATE_TIME_FORMAT, Locale.getDefault()) + + @TypeConverter + @JvmStatic + fun toDate(value: String?): Date? { + return if (value.isNullOrEmpty()) { + null + } else { + displayDateFormat.parse(value) + } + } + + @TypeConverter + @JvmStatic + fun toString(date: Date?): String? { + return date?.let { displayDateFormat.format(date) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/data/converters/UriConverter.kt b/app/src/main/java/com/aritra/notify/data/converters/UriConverter.kt new file mode 100644 index 00000000..76d10ad7 --- /dev/null +++ b/app/src/main/java/com/aritra/notify/data/converters/UriConverter.kt @@ -0,0 +1,20 @@ +package com.aritra.notify.data.converters + +import android.net.Uri +import androidx.room.TypeConverter + +object UriConverter { + @TypeConverter + fun fromUri(uri: Uri?): String? { + return uri?.toString() + } + + @TypeConverter + fun toUri(string: String?): Uri? { + if (string == null) { + return null + } + + return Uri.parse(string) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/data/dao/NoteDao.kt b/app/src/main/java/com/aritra/notify/data/dao/NoteDao.kt index 31b46b8e..41970e4c 100644 --- a/app/src/main/java/com/aritra/notify/data/dao/NoteDao.kt +++ b/app/src/main/java/com/aritra/notify/data/dao/NoteDao.kt @@ -6,7 +6,7 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update -import com.aritra.notify.data.models.Note +import com.aritra.notify.domain.models.Note import kotlinx.coroutines.flow.Flow @Dao @@ -20,11 +20,14 @@ interface NoteDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertNote(noteModel: Note) + suspend fun insertNote(noteModel: Note): Long @Update suspend fun updateNote(noteModel: Note) @Delete suspend fun deleteNote(noteModel: Note) + + @Query("DELETE FROM note") + suspend fun clear() } \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/data/db/NoteDatabase.kt b/app/src/main/java/com/aritra/notify/data/db/NoteDatabase.kt index 17d8fc01..b67930e4 100644 --- a/app/src/main/java/com/aritra/notify/data/db/NoteDatabase.kt +++ b/app/src/main/java/com/aritra/notify/data/db/NoteDatabase.kt @@ -4,14 +4,10 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase -import androidx.room.TypeConverter -import androidx.room.TypeConverters import com.aritra.notify.data.dao.NoteDao -import com.aritra.notify.data.models.BitmapConverters -import com.aritra.notify.data.models.Note +import com.aritra.notify.domain.models.Note @Database(entities = [Note::class], version = 2) -@TypeConverters(BitmapConverters::class) abstract class NoteDatabase : RoomDatabase() { abstract fun noteDao(): NoteDao @@ -28,7 +24,6 @@ abstract class NoteDatabase : RoomDatabase() { "Note_database" ) .fallbackToDestructiveMigration() - .allowMainThreadQueries() .build() INSTANCE = instance } diff --git a/app/src/main/java/com/aritra/notify/data/models/Note.kt b/app/src/main/java/com/aritra/notify/data/models/Note.kt deleted file mode 100644 index 2328f8a8..00000000 --- a/app/src/main/java/com/aritra/notify/data/models/Note.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.aritra.notify.data.models - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.os.Parcelable -import androidx.room.Entity -import androidx.room.PrimaryKey -import androidx.room.TypeConverter -import androidx.room.TypeConverters -import com.aritra.notify.utils.DateTypeConverter -import kotlinx.parcelize.Parcelize -import java.io.ByteArrayOutputStream -import java.util.Date - -@Parcelize -@Entity(tableName = "note") -@TypeConverters(DateTypeConverter::class) -data class Note( - @PrimaryKey(autoGenerate = true) - var id: Int = 0, - var title: String, - var note: String, - var dateTime: Date?, - @TypeConverters(BitmapConverters::class) - var imagePath: Bitmap? -) : Parcelable -class BitmapConverters { - @TypeConverter - fun fromBitmap(bitmap: Bitmap?): ByteArray? { - val outputStream = ByteArrayOutputStream() - if (bitmap != null) { - bitmap.compress(Bitmap.CompressFormat.PNG, 10, outputStream) - return outputStream.toByteArray() - } - return null - } - - @TypeConverter - fun toBitmap(byteArray: ByteArray?): Bitmap? { - return if (byteArray != null) - BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) - else null - } -} \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/data/repository/BackupRepository.kt b/app/src/main/java/com/aritra/notify/data/repository/BackupRepository.kt deleted file mode 100644 index 55b8a39e..00000000 --- a/app/src/main/java/com/aritra/notify/data/repository/BackupRepository.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.aritra.notify.data.repository - -import android.content.Context -import android.net.Uri -import com.aritra.notify.data.db.NoteDatabase -import com.aritra.notify.utils.Const -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext - -class BackupRepository( - private val provider: NoteDatabase, - private val context: Context, - private val mutex: Mutex, - private val scope: CoroutineScope, - private val dispatcher: CoroutineDispatcher, -) { - suspend fun export(uri: Uri) { - withContext(dispatcher + scope.coroutineContext) { - mutex.withLock { - provider.close() - - context.contentResolver.openOutputStream(uri)?.use { stream -> - context.getDatabasePath(Const.DB_NAME).inputStream().copyTo(stream) - } - } - } - } - - suspend fun import(uri: Uri) { - withContext(dispatcher + scope.coroutineContext) { - mutex.withLock { - provider.close() - - context.contentResolver.openInputStream(uri)?.use { stream -> - val dbFile = context.getDatabasePath(Const.DB_NAME) - dbFile?.delete() - stream.copyTo(dbFile.outputStream()) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/di/AppModule.kt b/app/src/main/java/com/aritra/notify/di/AppModule.kt index a5a02930..179ebf2c 100644 --- a/app/src/main/java/com/aritra/notify/di/AppModule.kt +++ b/app/src/main/java/com/aritra/notify/di/AppModule.kt @@ -2,7 +2,7 @@ package com.aritra.notify.di import android.app.Application import android.content.Context -import com.aritra.notify.data.repository.NoteRepository +import com.aritra.notify.domain.repository.NoteRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/aritra/notify/di/BioMetricUtil.kt b/app/src/main/java/com/aritra/notify/di/BioMetricUtil.kt index a89629b7..601aa9f4 100644 --- a/app/src/main/java/com/aritra/notify/di/BioMetricUtil.kt +++ b/app/src/main/java/com/aritra/notify/di/BioMetricUtil.kt @@ -1,7 +1,7 @@ package com.aritra.notify.di import android.content.Context -import com.aritra.notify.biometric.AppBioMetricManager +import com.aritra.notify.components.biometric.AppBioMetricManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/aritra/notify/di/ViewModelModule.kt b/app/src/main/java/com/aritra/notify/di/ViewModelModule.kt index 1995d01c..2611e3db 100644 --- a/app/src/main/java/com/aritra/notify/di/ViewModelModule.kt +++ b/app/src/main/java/com/aritra/notify/di/ViewModelModule.kt @@ -1,6 +1,6 @@ package com.aritra.notify.di -import com.aritra.notify.biometric.AppBioMetricManager +import com.aritra.notify.components.biometric.AppBioMetricManager import com.aritra.notify.viewmodel.MainViewModel import dagger.Module import dagger.Provides diff --git a/app/src/main/java/com/aritra/notify/domain/models/Note.kt b/app/src/main/java/com/aritra/notify/domain/models/Note.kt new file mode 100644 index 00000000..1ca0cc72 --- /dev/null +++ b/app/src/main/java/com/aritra/notify/domain/models/Note.kt @@ -0,0 +1,23 @@ +package com.aritra.notify.domain.models + +import android.net.Uri +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.aritra.notify.data.converters.DateTypeConverter +import com.aritra.notify.data.converters.UriConverter +import kotlinx.parcelize.Parcelize +import java.util.Date + +@Parcelize +@Entity(tableName = "note") +@TypeConverters(DateTypeConverter::class, UriConverter::class) +data class Note( + @PrimaryKey(autoGenerate = true) + var id: Int = 0, + var title: String, + var note: String, + var dateTime: Date?, + var image: Uri? +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/domain/repository/BackupRepository.kt b/app/src/main/java/com/aritra/notify/domain/repository/BackupRepository.kt new file mode 100644 index 00000000..56dea6fc --- /dev/null +++ b/app/src/main/java/com/aritra/notify/domain/repository/BackupRepository.kt @@ -0,0 +1,177 @@ +package com.aritra.notify.domain.repository + +import android.content.Context +import android.net.Uri +import android.util.Log +import androidx.core.content.FileProvider +import com.aritra.notify.data.converters.DateTypeConverter +import com.aritra.notify.data.db.NoteDatabase +import com.aritra.notify.domain.models.Note +import com.aritra.notify.domain.usecase.SaveSelectedImageUseCase +import com.aritra.notify.utils.CsvIo +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileReader +import java.io.FileWriter +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +class BackupRepository( + private val provider: NoteDatabase, + private val context: Context, + private val mutex: Mutex, + private val scope: CoroutineScope, + private val dispatcher: CoroutineDispatcher, +) { + suspend fun export(uri: Uri) { + withContext(dispatcher + scope.coroutineContext) { + mutex.withLock { + try { + // create a backup folder in the cache dir + val backupDir = File(context.externalCacheDir, "backup") + // if the backup directory already exists, delete it + if (backupDir.exists()) backupDir.deleteRecursively() + // create a new backup directory + backupDir.mkdir() + // creates a csv writer for writing the notes to a csv file in the backup directory + val csvWriter = CsvIo.Writer(FileWriter(File(backupDir, "notes.csv"))) + // write the headers to the csv file + csvWriter.writeNext(arrayOf("Id", "Title", "Content", "Date", "Image")) + // write the notes to the csv file + provider.noteDao().getAllNotes().first().forEach { note -> + // write the image to the backup directory if it exists + val image = note.image?.let { image -> + val imageName = "image_${note.id}.webp" + val imageFile = File(backupDir, imageName) + context.contentResolver.openInputStream(image)?.use { inputStream -> + imageFile.outputStream().use { outputStream -> + inputStream.copyTo(outputStream) + } + } + imageName + } + csvWriter.writeNext( + arrayOf( + note.id.toString(), + note.title, + note.note, + DateTypeConverter.toString(note.dateTime).orEmpty(), + image.orEmpty() + ) + ) + } + // close the csv writer + csvWriter.close() + // create a zip file containing the csv and the images + ZipOutputStream( + BufferedOutputStream( + context.contentResolver.openOutputStream( + uri + ) + ) + ).use { zip -> + backupDir.listFiles()?.forEach { file -> + zip.putNextEntry(ZipEntry(file.name)) + file.inputStream().copyTo(zip) + zip.closeEntry() + } + } + // delete the backup directory + backupDir.deleteRecursively() + } catch (e: Exception) { + Log.e(BackupRepository::class.simpleName, "Export", e) + } + } + } + } + + suspend fun import(uri: Uri) { + withContext(dispatcher + scope.coroutineContext) { + mutex.withLock { + try { + val restoreDir = File(context.externalCacheDir, "restore") + // delete the restore directory if it already exists + if (restoreDir.exists()) restoreDir.deleteRecursively() + // create a new restore directory + restoreDir.mkdir() + // extract the zip file to the restore directory + context.contentResolver.openInputStream(uri)?.use { stream -> + ZipInputStream(BufferedInputStream(stream)).use { zip -> + var entry = zip.nextEntry + while (entry != null) { + val file = File(restoreDir, entry.name) + file.outputStream().use { output -> + zip.copyTo(output) + } + entry = zip.nextEntry + } + } + } + // open the notes csv file + val csvReader = CsvIo.Reader(FileReader(File(restoreDir, "notes.csv"))) + // read all the lines and discard the headers + val rows = csvReader.rows().drop(1) + Log.d( + BackupRepository::class.simpleName!!, + "import: ${rows.map { it.toList() }}}" + ) + // close the csv reader + csvReader.close() + // clear the database to remove all the existing notes + provider.noteDao().clear() + // delete all images from the cache directory + File(context.externalCacheDir, SaveSelectedImageUseCase.DIRECTORY) + .deleteRecursively() + // import the notes from the csv file into the database + rows.forEach { columns -> + val id = columns[0].toInt() + val title = columns[1] + val text = columns[2] + val dateTime = DateTypeConverter.toDate(columns[3]) + val imageName = columns[4] + val image = if (imageName.isNotEmpty()) { + val imageStore = SaveSelectedImageUseCase.image(context, id) + // copy the image from the restore directory to the cache directory + context.contentResolver.openInputStream( + Uri.fromFile(File(restoreDir, imageName)) + )?.use { inputStream -> + imageStore.outputStream().use { outputStream -> + inputStream.copyTo(outputStream) + } + } + // get the uri for the image in the cache directory + FileProvider.getUriForFile( + context, + "${context.packageName}.provider", + imageStore + ) + } else null + val note = Note( + id = id, + title = title, + note = text, + dateTime = dateTime, + image = image + ) + Log.d(BackupRepository::class.simpleName, "import: $note") + provider.noteDao().insertNote( + note + ) + } + // delete the restore directory + restoreDir.deleteRecursively() + } catch (e: Exception) { + Log.e(BackupRepository::class.simpleName, "Import", e) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/data/repository/NoteRepository.kt b/app/src/main/java/com/aritra/notify/domain/repository/NoteRepository.kt similarity index 81% rename from app/src/main/java/com/aritra/notify/data/repository/NoteRepository.kt rename to app/src/main/java/com/aritra/notify/domain/repository/NoteRepository.kt index b886318e..5c35c152 100644 --- a/app/src/main/java/com/aritra/notify/data/repository/NoteRepository.kt +++ b/app/src/main/java/com/aritra/notify/domain/repository/NoteRepository.kt @@ -1,9 +1,9 @@ -package com.aritra.notify.data.repository +package com.aritra.notify.domain.repository import android.app.Application import com.aritra.notify.data.dao.NoteDao import com.aritra.notify.data.db.NoteDatabase -import com.aritra.notify.data.models.Note +import com.aritra.notify.domain.models.Note import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -21,7 +21,7 @@ class NoteRepository @Inject constructor(application: Application) { fun getNoteByIdFromRoom(noteId: Int): Flow = noteDao.getNoteById(noteId) - suspend fun insertNoteToRoom(note: Note) = noteDao.insertNote(note) + suspend fun insertNoteToRoom(note: Note): Long = noteDao.insertNote(note) suspend fun updateNoteInRoom(note: Note) = noteDao.updateNote(note) diff --git a/app/src/main/java/com/aritra/notify/domain/usecase/SaveSelectedImageUseCase.kt b/app/src/main/java/com/aritra/notify/domain/usecase/SaveSelectedImageUseCase.kt new file mode 100644 index 00000000..0df2c4e2 --- /dev/null +++ b/app/src/main/java/com/aritra/notify/domain/usecase/SaveSelectedImageUseCase.kt @@ -0,0 +1,62 @@ +package com.aritra.notify.domain.usecase + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import androidx.core.content.FileProvider +import java.io.File + +object SaveSelectedImageUseCase { + const val DIRECTORY = "image" + + /** + * Returns the image file in the cache directory + */ + fun image(context: Context, id: Int) = File( + File(context.externalCacheDir, DIRECTORY).apply { + if (!exists()) { + mkdirs() + } + }, + "image_${id}.webp" + ) + + /** + * Saves the selected image to the cache directory and returns the uri of the saved image + */ + operator fun invoke(context: Context, uri: Uri, noteId: Int): Uri? = try { + // copy the image to cache directory because opening the + // image uri after app restart doesn't work for external storage uri on android 11 and above + val image = image(context, noteId) + val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ImageDecoder.decodeBitmap( + ImageDecoder.createSource( + context.contentResolver, + uri + ) + ) + } else { + MediaStore.Images.Media.getBitmap(context.contentResolver, uri) + } + // compress the image to 80% webp quality before saving + bitmap.compress( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Bitmap.CompressFormat.WEBP_LOSSY + } else { + Bitmap.CompressFormat.WEBP + }, + 80, + image.outputStream() + ) + FileProvider.getUriForFile( + context, + "${context.packageName}.provider", + image + ) + } catch (_: Exception) { + null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/navigation/NotifyApp.kt b/app/src/main/java/com/aritra/notify/navigation/NotifyApp.kt index 383ff0d6..ae26c595 100644 --- a/app/src/main/java/com/aritra/notify/navigation/NotifyApp.kt +++ b/app/src/main/java/com/aritra/notify/navigation/NotifyApp.kt @@ -38,10 +38,6 @@ import com.google.accompanist.permissions.shouldShowRationale @Composable fun NotifyApp(navController: NavHostController = rememberNavController()) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - RequestNotificationPermissionDialog() - } - val bottomNavItem = listOf( BottomNavItem( name = "Notes", @@ -136,15 +132,3 @@ fun NotifyApp(navController: NavHostController = rememberNavController()) { } } } - -@OptIn(ExperimentalPermissionsApi::class) -@RequiresApi(Build.VERSION_CODES.TIRAMISU) -@Composable -fun RequestNotificationPermissionDialog() { - val permissionState = rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS) - - if (!permissionState.status.isGranted) { - if (permissionState.status.shouldShowRationale) RationaleDialog() - else PermissionDialog { permissionState.launchPermissionRequest() } - } -} diff --git a/app/src/main/java/com/aritra/notify/ui/screens/MainActivity.kt b/app/src/main/java/com/aritra/notify/ui/screens/MainActivity.kt index 73b0f1bb..89d3e068 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/MainActivity.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/MainActivity.kt @@ -10,7 +10,7 @@ import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import com.aritra.notify.biometric.AppBioMetricManager +import com.aritra.notify.components.biometric.AppBioMetricManager import com.aritra.notify.navigation.NotifyApp import com.aritra.notify.ui.theme.NotifyTheme import com.aritra.notify.viewmodel.MainViewModel diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/addNoteScreen/AddNoteViewModel.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/addNoteScreen/AddNoteViewModel.kt index 2df39bf9..3859dc80 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/addNoteScreen/AddNoteViewModel.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/addNoteScreen/AddNoteViewModel.kt @@ -3,11 +3,13 @@ package com.aritra.notify.ui.screens.notes.addNoteScreen import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope -import com.aritra.notify.data.models.Note -import com.aritra.notify.data.repository.NoteRepository +import com.aritra.notify.domain.models.Note +import com.aritra.notify.domain.repository.NoteRepository +import com.aritra.notify.domain.usecase.SaveSelectedImageUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel @@ -16,7 +18,27 @@ class AddNoteViewModel @Inject constructor( private val addRepository: NoteRepository ) : AndroidViewModel(application) { - fun insertNote(note: Note) = viewModelScope.launch(Dispatchers.IO) { - addRepository.insertNoteToRoom(note) + fun insertNote(note: Note, onSuccess: () -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + val id: Int = addRepository.insertNoteToRoom(note).toInt() + + if (note.image != null) { + // update the note with the new image uri + addRepository.updateNoteInRoom( + note.copy( + id = id, + image = SaveSelectedImageUseCase( + context = getApplication(), + uri = note.image!!, + noteId = id + ) + ) + ) + } + + withContext(Dispatchers.Main) { + onSuccess() + } + } } } diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/addNoteScreen/AddNotesScreen.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/addNoteScreen/AddNotesScreen.kt index f548de03..741dec8c 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/addNoteScreen/AddNotesScreen.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/addNoteScreen/AddNotesScreen.kt @@ -2,24 +2,18 @@ package com.aritra.notify.ui.screens.notes.addNoteScreen import android.Manifest -import android.graphics.Bitmap -import android.graphics.ImageDecoder import android.net.Uri -import android.os.Build -import android.provider.MediaStore import android.util.Log +import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding @@ -51,7 +45,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -67,11 +60,13 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import coil.compose.AsyncImage import com.aritra.notify.R import com.aritra.notify.components.actions.BottomSheetOptions import com.aritra.notify.components.actions.SpeechRecognizerContract -import com.aritra.notify.components.topbar.AddNoteTopBar import com.aritra.notify.components.dialog.TextDialog +import com.aritra.notify.components.topbar.AddNoteTopBar +import com.aritra.notify.domain.models.Note import com.aritra.notify.utils.Const import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted @@ -86,10 +81,10 @@ fun AddNotesScreen( navigateBack: () -> Unit ) { val addViewModel = hiltViewModel() + val context = LocalContext.current var title by remember { mutableStateOf("") } var description by remember { mutableStateOf("") } val dateTime by remember { mutableStateOf(Calendar.getInstance().time) } - var imagePath by remember { mutableStateOf(null) } var characterCount by remember { mutableIntStateOf(title.length + description.length) } val cancelDialogState = remember { mutableStateOf(false) } var showSheet by remember { mutableStateOf(false) } @@ -103,7 +98,6 @@ fun AddNotesScreen( val bottomSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = skipPartiallyExpanded ) - val context = LocalContext.current var photoUri: Uri? by remember { mutableStateOf(null) } val launcher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> @@ -128,16 +122,31 @@ fun AddNotesScreen( } ) + val saveNote = remember { + { + addViewModel.insertNote( + note = Note( + id = 0, + title = title, + note = description, + dateTime = dateTime, + image = photoUri + ), + onSuccess = { + navigateBack() + Toast.makeText(context, "Successfully Saved!", Toast.LENGTH_SHORT).show() + } + ) + } + } + Scaffold( topBar = { AddNoteTopBar( - addViewModel, + title = title, + description = description, onBackPress = { cancelDialogState.value = true }, - onSave = { navigateBack() }, - title, - description, - dateTime, - imagePath + saveNote = saveNote, ) }, bottomBar = { @@ -215,7 +224,6 @@ fun AddNotesScreen( IconButton( onClick = { photoUri = null - imagePath = null }, ) { Icon( @@ -225,24 +233,12 @@ fun AddNotesScreen( } } - val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - ImageDecoder.decodeBitmap( - ImageDecoder.createSource( - context.contentResolver, - photoUri!! - ) - ) - } else { - MediaStore.Images.Media.getBitmap(context.contentResolver, photoUri!!) - } - Image( - bitmap = bitmap.asImageBitmap(), + AsyncImage( + model = photoUri, contentDescription = stringResource(R.string.image), modifier = Modifier.fillMaxWidth(), contentScale = ContentScale.Crop ) - - imagePath = bitmap } TextField( diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/editNoteScreen/EditNotesScreen.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/editNoteScreen/EditNotesScreen.kt index 46b97942..5b4ab5b3 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/editNoteScreen/EditNotesScreen.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/editNoteScreen/EditNotesScreen.kt @@ -1,10 +1,9 @@ package com.aritra.notify.ui.screens.notes.editNoteScreen +import android.widget.Toast import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -13,23 +12,20 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Close -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -45,7 +41,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.rememberAsyncImagePainter import com.aritra.notify.R import com.aritra.notify.components.topbar.EditNoteTopBar -import com.aritra.notify.data.models.Note +import com.aritra.notify.domain.models.Note import com.aritra.notify.utils.Const import java.text.SimpleDateFormat import java.util.Date @@ -56,23 +52,35 @@ fun EditNotesScreen( noteId: Int, navigateBack: () -> Unit ) { - val note = Note(noteId, "","", Date(),null) + val note = Note(noteId, "", "", Date(), null) + val context = LocalContext.current val editViewModel = hiltViewModel() val title = editViewModel.noteModel.observeAsState().value?.title ?: "" val description = editViewModel.noteModel.observeAsState().value?.note ?: "" - val imagePath = editViewModel.noteModel.observeAsState().value?.imagePath + val imagePath = editViewModel.noteModel.observeAsState().value?.image val dateTime = editViewModel.noteModel.observeAsState().value?.dateTime val focus = LocalFocusManager.current - val formattedDateTime = SimpleDateFormat(Const.DATE_TIME_FORMAT, Locale.getDefault()).format(dateTime ?: 0) + val formattedDateTime = + SimpleDateFormat(Const.DATE_TIME_FORMAT, Locale.getDefault()).format(dateTime ?: 0) val formattedCharacterCount = "${(title.length) + (description.length)} characters" + val saveNote: () -> Unit = remember { + { + editViewModel.updateNotes { + navigateBack() + Toast.makeText(context, "Successfully Updated!", Toast.LENGTH_SHORT).show() + } + navigateBack() + } + } + LaunchedEffect(Unit) { editViewModel.getNoteById(noteId) } Scaffold( topBar = { - dateTime?.let { - EditNoteTopBar(note,editViewModel, noteId, navigateBack, title, description, imagePath) + if (dateTime != null) { + EditNoteTopBar(note, navigateBack, title, description, saveNote) } } ) { diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/editNoteScreen/EditScreenViewModel.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/editNoteScreen/EditScreenViewModel.kt index 23ede807..a11cf620 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/editNoteScreen/EditScreenViewModel.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/editNoteScreen/EditScreenViewModel.kt @@ -1,18 +1,19 @@ package com.aritra.notify.ui.screens.notes.editNoteScreen import android.app.Application -import android.graphics.Bitmap -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue +import android.net.Uri import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import com.aritra.notify.data.models.Note -import com.aritra.notify.data.repository.NoteRepository +import com.aritra.notify.domain.models.Note +import com.aritra.notify.domain.repository.NoteRepository +import com.aritra.notify.domain.usecase.SaveSelectedImageUseCase +import com.aritra.notify.utils.toFile import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.Date import javax.inject.Inject @@ -22,16 +23,42 @@ class EditScreenViewModel @Inject constructor( private val editScreenRepository: NoteRepository ) : AndroidViewModel(application) { - var noteModel = MutableLiveData(Note(0,"","",Date(),null)) - + var noteModel = MutableLiveData(Note(0, "", "", Date(), null)) fun getNoteById(noteId: Int) = viewModelScope.launch(Dispatchers.IO) { editScreenRepository.getNoteByIdFromRoom(noteId).collect { response -> noteModel.postValue(response) } } - fun updateNotes(noteModel: Note) = viewModelScope.launch(Dispatchers.IO) { - editScreenRepository.updateNoteInRoom(noteModel) + fun updateNotes(onSuccess: () -> Unit) = viewModelScope.launch(Dispatchers.IO) { + val newNote = noteModel.value ?: return@launch + // retrieve the note from the database to check if the image has been modified + val oldNote = editScreenRepository.getNoteByIdFromRoom(newNote.id).first() + // exit the method if the note has not been modified + if (oldNote.title == newNote.title && oldNote.note == newNote.note && oldNote.image == newNote.image) return@launch + // if the image has been modified, delete the old image + if (oldNote.image != newNote.image) { + oldNote.image?.toFile(getApplication())?.delete() + } + editScreenRepository.updateNoteInRoom( + newNote.copy( + // if the image has not been modified, use the old image uri + image = if (oldNote.image == newNote.image) { + oldNote.image + } else if (newNote.image != null) { + // if the image has been modified, save the new image uri + SaveSelectedImageUseCase( + getApplication(), + newNote.image!!, + newNote.id + ) + } else null + ) + ) + + withContext(Dispatchers.Main) { + onSuccess() + } } fun updateTitle(title: String) { @@ -42,7 +69,7 @@ class EditScreenViewModel @Inject constructor( noteModel.postValue(noteModel.value?.copy(note = description)) } - fun updateImage(image: Bitmap?) { - noteModel.postValue(noteModel.value?.copy(imagePath = image)) + fun updateImage(image: Uri?) { + noteModel.postValue(noteModel.value?.copy(image = image)) } } \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreen.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreen.kt index 37b3de95..ecf842c5 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreen.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreen.kt @@ -40,8 +40,8 @@ import com.aritra.notify.R import com.aritra.notify.components.actions.BackPressHandler import com.aritra.notify.components.actions.LayoutToggleButton import com.aritra.notify.components.actions.NoList -import com.aritra.notify.components.actions.SwipeDelete import com.aritra.notify.components.note.GridNoteCard +import com.aritra.notify.components.note.NotesCard @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -119,9 +119,7 @@ fun NoteScreen( }) { _, notesModel -> GridNoteCard( notesModel, - viewModel, navigateToUpdateNoteScreen, - isGridView ) } } @@ -136,7 +134,7 @@ fun NoteScreen( items(listOfAllNotes.filter { note -> note.title.contains(searchQuery, true) }) { notesModel -> - SwipeDelete(notesModel, viewModel, navigateToUpdateNoteScreen) + NotesCard(noteModel = notesModel, navigateToUpdateNoteScreen = navigateToUpdateNoteScreen) } } } diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreenViewModel.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreenViewModel.kt index 35f6613a..2383081c 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreenViewModel.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreenViewModel.kt @@ -4,8 +4,8 @@ import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope -import com.aritra.notify.data.models.Note -import com.aritra.notify.data.repository.NoteRepository +import com.aritra.notify.domain.models.Note +import com.aritra.notify.domain.repository.NoteRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject diff --git a/app/src/main/java/com/aritra/notify/ui/screens/settingsScreen/SettingsViewModel.kt b/app/src/main/java/com/aritra/notify/ui/screens/settingsScreen/SettingsViewModel.kt index 762fb1b6..7762b2d2 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/settingsScreen/SettingsViewModel.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/settingsScreen/SettingsViewModel.kt @@ -8,12 +8,12 @@ import androidx.compose.runtime.setValue import androidx.datastore.preferences.core.edit import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope -import com.aritra.notify.biometric.AppBioMetricManager -import com.aritra.notify.biometric.BiometricAuthListener +import com.aritra.notify.components.biometric.AppBioMetricManager +import com.aritra.notify.components.biometric.BiometricAuthListener import com.aritra.notify.data.db.NoteDatabase -import com.aritra.notify.data.models.Note -import com.aritra.notify.data.repository.BackupRepository -import com.aritra.notify.data.repository.NoteRepository +import com.aritra.notify.domain.models.Note +import com.aritra.notify.domain.repository.BackupRepository +import com.aritra.notify.domain.repository.NoteRepository import com.aritra.notify.di.DataStoreUtil import com.aritra.notify.ui.screens.MainActivity import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/app/src/main/java/com/aritra/notify/utils/Const.kt b/app/src/main/java/com/aritra/notify/utils/Const.kt index 7960f635..fcce4269 100644 --- a/app/src/main/java/com/aritra/notify/utils/Const.kt +++ b/app/src/main/java/com/aritra/notify/utils/Const.kt @@ -2,7 +2,7 @@ package com.aritra.notify.utils object Const { - const val DATABASE_FILE_NAME = "Notify.db" + const val DATABASE_FILE_NAME = "Notify.zip" const val DB_NAME = "Note_database" const val DATE_TIME_FORMAT = "dd MMM, hh:mm a" const val DATE_FORMAT = "dd MMM" diff --git a/app/src/main/java/com/aritra/notify/utils/Context.kt b/app/src/main/java/com/aritra/notify/utils/Context.kt new file mode 100644 index 00000000..ddc53ec3 --- /dev/null +++ b/app/src/main/java/com/aritra/notify/utils/Context.kt @@ -0,0 +1,20 @@ +package com.aritra.notify.utils + +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.provider.OpenableColumns + +fun Context.getFileInfo( + file: Uri, + columnName: String = OpenableColumns.DISPLAY_NAME, + data: Cursor.(Int) -> T, +) = contentResolver?.query( + file, null, null, null, null +)?.run { + val index = getColumnIndex(columnName) + moveToFirst() + val result = data(index) + close() + result +} \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/utils/CsvIo.kt b/app/src/main/java/com/aritra/notify/utils/CsvIo.kt new file mode 100644 index 00000000..11261eff --- /dev/null +++ b/app/src/main/java/com/aritra/notify/utils/CsvIo.kt @@ -0,0 +1,140 @@ +package com.aritra.notify.utils + +internal object CsvIo { + + /** The character used for escaping quotes. */ + const val DEFAULT_ESCAPE_CHARACTER = '"' + + /** The default separator to use if none is supplied to the constructor. */ + const val DEFAULT_SEPARATOR = ';' + + /** + * The default quote character to use if none is supplied to the + * constructor. + */ + const val DEFAULT_QUOTE_CHARACTER = '"' + + /** The quote constant to use when you wish to suppress all quoting. */ + const val NO_QUOTE_CHARACTER = '\u0000' + + /** The escape constant to use when you wish to suppress all escaping. */ + const val NO_ESCAPE_CHARACTER = '\u0000' + + /** Default line terminator uses platform encoding. */ + const val DEFAULT_LINE_END = "\n" + + /** + * Constructs a Csv Writer with supplied separator, quote char, escape char and line ending. + * + * @param writer + * the writer to an underlying CSV source. + * @param separator + * the delimiter to use for separating entries + * @param quoteChar + * the character to use for quoted elements + * @param escapeChar + * the character to use for escaping quoteChars or escapeChars + * @param lineEnd + * the line feed terminator to use + */ + class Writer( + private val writer: java.io.Writer?, + private val separator: Char = DEFAULT_SEPARATOR, + private val quoteChar: Char = DEFAULT_QUOTE_CHARACTER, + private val escapeChar: Char = DEFAULT_ESCAPE_CHARACTER, + private val lineEnd: String = DEFAULT_LINE_END + ) { + init { + // write the separator to the top of the file + writer?.write("sep=$separator\n") + } + + /** + * Writes the next line to the file. + * + * @param nextLine + * a string array with each comma-separated element as a separate + * entry. + */ + fun writeNext(nextLine: Array) { + val builder = StringBuilder() + for (i in nextLine.indices) { + if (i != 0) { + builder.append(separator) + } + val nextElement = nextLine[i] + + if (quoteChar != NO_QUOTE_CHARACTER) + builder.append(quoteChar) + + for (element in nextElement) { + if (escapeChar == NO_ESCAPE_CHARACTER && (element == quoteChar || element == escapeChar)) { + builder.append(escapeChar) + } + builder.append(element) + } + + if (quoteChar != NO_QUOTE_CHARACTER) + builder.append(quoteChar) + } + builder.append(lineEnd) + writer?.write(builder.toString()) + } + + /** + * Close the underlying stream writer flushing any buffered content. + */ + fun close() = writer?.runCatching { + flush() + close() + } + } + + /** + * Constructs a Csv Reader with supplied separator, quote char, escape char and line ending. + * + * @param reader + * the writer to an underlying CSV source. + * @param separator + * the delimiter to use for separating entries + * @param quoteChar + * the character to use for quoted elements + * @param escapeChar + * the character to use for escaping quoteChars or escapeChars + * @param lineEnd + * the line feed terminator to use + */ + class Reader( + private val reader: java.io.Reader?, + private val separator: Char = DEFAULT_SEPARATOR, + private val quoteChar: Char = DEFAULT_QUOTE_CHARACTER, + private val escapeChar: Char = DEFAULT_ESCAPE_CHARACTER, + private val lineEnd: String = DEFAULT_LINE_END + ) { + + /** + * Reads the entire file into a List with each element being a String[] of tokens. + */ + fun rows(): List> { + var lines = reader?.readLines() + // remove the first line if it starts with "sep=" + lines = if (lines?.firstOrNull()?.startsWith("sep=") == true) { + lines.drop(1) + } else lines + // split the lines into chunks and trim the chunks + return lines?.map { line -> + val tokens = line.split(separator) + tokens.map { token -> + token.replace(quoteChar, ' ').replace(escapeChar, ' ').trim() + }.toTypedArray() + } ?: emptyList() + } + + /** + * Close the underlying stream reader. + */ + fun close() = reader?.runCatching { + close() + } + } +} diff --git a/app/src/main/java/com/aritra/notify/utils/DateTypeConverter.kt b/app/src/main/java/com/aritra/notify/utils/DateTypeConverter.kt deleted file mode 100644 index 0b9c1687..00000000 --- a/app/src/main/java/com/aritra/notify/utils/DateTypeConverter.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.aritra.notify.utils - -import androidx.room.TypeConverter -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -class DateTypeConverter { - - companion object { - private val displayDateFormat = - SimpleDateFormat(Const.DATE_TIME_FORMAT, Locale.getDefault()) - - @TypeConverter - @JvmStatic - fun toDate(value: String?): Date? { - return if (value.isNullOrEmpty()) { - null - } else { - displayDateFormat.parse(value) - } - } - - @TypeConverter - @JvmStatic - fun toString(date: Date?): String? { - return date?.let { displayDateFormat.format(date) } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/utils/Uri.kt b/app/src/main/java/com/aritra/notify/utils/Uri.kt new file mode 100644 index 00000000..9784165e --- /dev/null +++ b/app/src/main/java/com/aritra/notify/utils/Uri.kt @@ -0,0 +1,37 @@ +package com.aritra.notify.utils + +import android.content.Context +import android.net.Uri +import android.os.Environment +import androidx.core.net.toFile +import java.io.File + +/** + * Converts an android content uri to a file. + */ +fun Uri.toFile(context: Context): File? { + if (!exists()) return null + + return try { + toFile() + } catch (e: IllegalArgumentException) { + context.getFileInfo(this) { + File(getString(it)) + } + } +} + +/** + * Checks if an android content uri exists. + */ +fun Uri.exists(): Boolean { + return try { + toFile().exists() + } catch (e: IllegalArgumentException) { + val path = path?.replace( + "external_files", + Environment.getExternalStorageDirectory().toString() + ).toString() + File(path).exists() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/viewmodel/MainViewModel.kt b/app/src/main/java/com/aritra/notify/viewmodel/MainViewModel.kt index 85db1f46..e832447a 100644 --- a/app/src/main/java/com/aritra/notify/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/aritra/notify/viewmodel/MainViewModel.kt @@ -2,8 +2,8 @@ package com.aritra.notify.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.aritra.notify.biometric.AppBioMetricManager -import com.aritra.notify.biometric.BiometricAuthListener +import com.aritra.notify.components.biometric.AppBioMetricManager +import com.aritra.notify.components.biometric.BiometricAuthListener import com.aritra.notify.di.DataStoreUtil import com.aritra.notify.ui.screens.MainActivity import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/app/src/main/res/xml/file_provider_paths.xml b/app/src/main/res/xml/file_provider_paths.xml index 389acff7..ea608578 100644 --- a/app/src/main/res/xml/file_provider_paths.xml +++ b/app/src/main/res/xml/file_provider_paths.xml @@ -1,6 +1,14 @@ - + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index be0c6404..6373fc0d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,6 @@ buildscript { dependencies { classpath ("com.android.tools.build:gradle:3.4.0") - classpath ("com.google.gms:google-services:4.3.15") } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins {