diff --git a/app/.gitignore b/app/.gitignore index 956c004..42afabf 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,2 +1 @@ -/build -/release \ No newline at end of file +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index decde6c..1a78f45 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,51 +1,28 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) alias(libs.plugins.ksp) + alias(libs.plugins.aboutlibraries) } -ksp { - arg("room.schemaLocation", "$projectDir/schemas") -} - -val baseVersionName = "1.0.6" -val commitHash by lazy { "git rev-parse --short HEAD".exec() } -val verCode by lazy { "git rev-list --count HEAD".exec().toInt() } android { namespace = "cn.super12138.todo" compileSdk = 35 - // 获取 Release 签名 - val releaseSigning = if (project.hasProperty("releaseStoreFile")) { - signingConfigs.create("release") { - storeFile = File(project.properties["releaseStoreFile"] as String) - storePassword = project.properties["releaseStorePassword"] as String - keyAlias = project.properties["releaseKeyAlias"] as String - keyPassword = project.properties["releaseKeyPassword"] as String - } - } else { - signingConfigs.getByName("debug") - } - defaultConfig { applicationId = "cn.super12138.todo" minSdk = 24 targetSdk = 35 - versionCode = verCode - versionName = "${baseVersionName}-${commitHash}" + versionCode = 1 + versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - - base.archivesName.set("todo-${baseVersionName}") } buildTypes { - all { - signingConfig = releaseSigning - } release { isMinifyEnabled = true - isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" @@ -53,52 +30,54 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_18 - targetCompatibility = JavaVersion.VERSION_18 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } kotlinOptions { - jvmTarget = "18" + jvmTarget = "21" } buildFeatures { - viewBinding = true + compose = true } } dependencies { // Android X - implementation(libs.androidx.core) implementation(libs.androidx.core.ktx) - implementation(libs.androidx.core.core.splashscreen) - implementation(libs.androidx.appcompat) - implementation(libs.androidx.activity) - implementation(libs.androidx.activity.ktx) - implementation(libs.androidx.constraintlayout) - implementation(libs.androidx.fragment) - implementation(libs.androidx.fragment.ktx) - implementation(libs.androidx.recyclerview) - implementation(libs.androidx.lifecycle.viewmodel) - implementation(libs.androidx.lifecycle.viewmodel.ktx) - implementation(libs.androidx.preference) - implementation(libs.androidx.preference.ktx) - implementation(libs.androidx.security) - // Material Design - implementation(libs.material) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.compose.runtime.livedata) + // implementation(libs.androidx.security) + // Compose + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.animation) + implementation(libs.androidx.navigation) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.android) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.material.icon.core) + implementation(libs.androidx.material.icon.extended) + // About Libraries + implementation(libs.aboutlibraries.core) + implementation(libs.aboutlibraries.compose) + // Kotlin Coroutines + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) // Room implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) annotationProcessor(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler) - // FastScroll - implementation(project(":fastscroll")) // Test testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) -} - -fun String.exec(): String = exec(this) - -fun Project.exec(command: String): String = providers.exec { - commandLine(command.split(" ")) -}.standardOutput.asText.get().trim() \ No newline at end of file + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f4ec64c..481bb43 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,6 +18,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile --dontwarn javax.annotation.Nullable --dontwarn javax.annotation.concurrent.GuardedBy \ No newline at end of file +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/schemas/cn.super12138.todo.logic.dao.ToDoRoomDB/1.json b/app/schemas/cn.super12138.todo.logic.dao.ToDoRoomDB/1.json deleted file mode 100644 index 0bd7a91..0000000 --- a/app/schemas/cn.super12138.todo.logic.dao.ToDoRoomDB/1.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "bf23f9a3f857e7cfe28dce9eb1657534", - "entities": [ - { - "tableName": "todo", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `state` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`uuid`))", - "fields": [ - { - "fieldPath": "uuid", - "columnName": "uuid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "state", - "columnName": "state", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subject", - "columnName": "subject", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "uuid" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bf23f9a3f857e7cfe28dce9eb1657534')" - ] - } -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/cn/super12138/todo/ExampleInstrumentedTest.kt b/app/src/androidTest/kotlin/cn/super12138/todo/ExampleInstrumentedTest.kt index 6f68032..491bd80 100644 --- a/app/src/androidTest/kotlin/cn/super12138/todo/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/kotlin/cn/super12138/todo/ExampleInstrumentedTest.kt @@ -1,11 +1,13 @@ package cn.super12138.todo -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.assertEquals +import androidx.test.ext.junit.runners.AndroidJUnit4 + import org.junit.Test import org.junit.runner.RunWith +import org.junit.Assert.* + /** * Instrumented test, which will execute on an Android device. * diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0e3e6b4..9aa3ce1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,34 +3,26 @@ xmlns:tools="http://schemas.android.com/tools"> - + android:theme="@style/Theme.ToDo" + tools:targetApi="31"> + android:label="@string/app_name" + android:theme="@style/Theme.ToDo"> - - - + \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/MainActivity.kt b/app/src/main/kotlin/cn/super12138/todo/MainActivity.kt new file mode 100644 index 0000000..0589e32 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/MainActivity.kt @@ -0,0 +1,47 @@ +package cn.super12138.todo + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import cn.super12138.todo.ui.theme.ToDoTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + ToDoTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Greeting( + name = "Android", + modifier = Modifier.padding(innerPadding) + ) + } + } + } + } +} + +@Composable +fun Greeting(name: String, modifier: Modifier = Modifier) { + Text( + text = "Hello $name!", + modifier = modifier + ) +} + +@Preview(showBackground = true) +@Composable +fun GreetingPreview() { + ToDoTheme { + Greeting("Android") + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ToDoApp.kt b/app/src/main/kotlin/cn/super12138/todo/ToDoApp.kt deleted file mode 100644 index 8308a25..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ToDoApp.kt +++ /dev/null @@ -1,29 +0,0 @@ -package cn.super12138.todo - -import android.annotation.SuppressLint -import android.app.Application -import android.content.Context -import cn.super12138.todo.logic.dao.ToDoRoomDB -import cn.super12138.todo.views.activities.CrashHandler -import com.google.android.material.color.DynamicColors - -class ToDoApp : Application() { - private val database by lazy { ToDoRoomDB.getDatabase(this) } - - companion object { - @SuppressLint("StaticFieldLeak") - lateinit var context: Context - lateinit var db: ToDoRoomDB - } - - override fun onCreate() { - super.onCreate() - DynamicColors.applyToActivitiesIfAvailable(this) - context = applicationContext - - val crashHandler = CrashHandler(this) - Thread.setDefaultUncaughtExceptionHandler(crashHandler) - - db = database - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/constant/Constants.kt b/app/src/main/kotlin/cn/super12138/todo/constant/Constants.kt deleted file mode 100644 index 8a54be8..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/constant/Constants.kt +++ /dev/null @@ -1,38 +0,0 @@ -package cn.super12138.todo.constant - -object Constants { - const val WELCOME_PAGE= "welcome_page" - - const val AUTHOR_GITHUB_URL = "https://github.com/Super12138/" - const val REPO_GITHUB_URL = "https://github.com/Super12138/ToDo" - const val UPDATE_URL = "https://github.com/Super12138/ToDo/releases" - - const val SP_NAME = "cn.super12138.todo_preferences" - - const val PREF_DARK_MODE = "dark_mode" - const val PREF_SECURE_MODE = "secure_mode" - const val PREF_HAPTIC_FEEDBACK = "haptic_feedback" - const val PREF_ALL_TASKS = "all_tasks" - const val PREF_REENTER_WELCOME_ACTIVITY = "reenter_welcome_activity" - const val PREF_ABOUT = "about" - const val PREF_DEV_MODE = "dev_mode" - // const val PREF_SPRING_FESTIVAL_THEME = "spring_festival_theme" - const val PREF_BACKUP_DB = "backup_db" - const val PREF_RESTORE_DB = "restore_db" - - - const val STRING_DEV_MODE = "/DEV_MODE" - - const val TAG_INFO_BOTTOM_SHEET = "InfoBottomSheet" - const val TAG_TODO_BOTTOM_SHEET = "ToDoBottomSheet" - - const val BUNDLE_EDIT_MODE = "editMode" - const val BUNDLE_POSITION = "todoPosition" - const val BUNDLE_TODO_CONTENT = "todoContent" - const val BUNDLE_TODO_SUBJECT = "todoSubject" - const val BUNDLE_TODO_STATE = "todoState" - const val BUNDLE_TODO_UUID = "todoUUID" - - const val EMPTY_VIEW_TYPE = 0 - const val DEFAULT_VIEW_TYPE = 1 -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/constant/GlobalValues.kt b/app/src/main/kotlin/cn/super12138/todo/constant/GlobalValues.kt deleted file mode 100644 index 430ed93..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/constant/GlobalValues.kt +++ /dev/null @@ -1,12 +0,0 @@ -package cn.super12138.todo.constant - -import cn.super12138.todo.utils.sp.SPDelegates - -object GlobalValues { - var welcomePage: Boolean by SPDelegates(Constants.WELCOME_PAGE, false) - var darkMode: String by SPDelegates(Constants.PREF_DARK_MODE, "0") - var devMode: Boolean by SPDelegates(Constants.PREF_DEV_MODE, false) - // var springFestivalTheme: Boolean by SPDelegates(Constants.PREF_SPRING_FESTIVAL_THEME, false) - var secureMode: Boolean by SPDelegates(Constants.PREF_SECURE_MODE, false) - var hapticFeedback: Boolean by SPDelegates(Constants.PREF_HAPTIC_FEEDBACK, true) -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt b/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt deleted file mode 100644 index 077682f..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt +++ /dev/null @@ -1,87 +0,0 @@ -package cn.super12138.todo.logic - -import cn.super12138.todo.ToDoApp -import cn.super12138.todo.logic.dao.ToDoRoom -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -object Repository { - - // Room - private val db get() = ToDoApp.db - private val todoDao = db.toDoRoomDao() - - /** - * @param toDoRoom 要插入的数据 - */ - suspend fun insert(toDoRoom: ToDoRoom) { - withContext(Dispatchers.IO) { - todoDao.insert(toDoRoom) - } - } - - /** - * 获取全部未完成的待办 - * @return List - */ - suspend fun getAllIncomplete(): List { - return withContext(Dispatchers.IO) { - todoDao.getAllUnfinished() - } - } - - /** - * 获取全部已完成的待办 - * @return List - */ - suspend fun getAllComplete(): List { - return withContext(Dispatchers.IO) { - todoDao.getAllComplete() - } - } - - /** - * 获取全部待办 - * @return List - */ - suspend fun getAll(): List { - return withContext(Dispatchers.IO) { - todoDao.getAll() - } - } - - /** - * 根据待办的UUID删除指定待办 - * @param uuid 待办的UUID - */ - suspend fun deleteByUUID(uuid: String) { - withContext(Dispatchers.IO) { - todoDao.deleteByUUID(uuid) - } - } - - /** - * 删除全部代办 - */ - suspend fun deleteAll() { - withContext(Dispatchers.IO) { - todoDao.deleteAll() - } - } - - /** - * 根据代办的UUID来把待办状态更新为“已完成” - * @param uuid 待办的UUID - */ - suspend fun updateStateByUUID(uuid: String) { - withContext(Dispatchers.IO) { - todoDao.updateStateByUUID(uuid) - } - } - - suspend fun update(toDoRoom: ToDoRoom) { - withContext(Dispatchers.IO) { - todoDao.update(toDoRoom) - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoom.kt b/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoom.kt deleted file mode 100644 index 69cf07c..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoom.kt +++ /dev/null @@ -1,19 +0,0 @@ -package cn.super12138.todo.logic.dao - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey - -/** - * @param uuid String 待办的uuid - * @param state Int 待办的完成状态,0表示未完成,1表示完成 - * @param subject String 待办的学科 - * @param content String 待办的内容 - */ -@Entity(tableName = "todo") -data class ToDoRoom( - @PrimaryKey @ColumnInfo(name = "uuid") val uuid: String, - @ColumnInfo(name = "state") val state: Int, - @ColumnInfo(name = "subject") val subject: String, - @ColumnInfo(name = "content") val content: String -) \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDB.kt b/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDB.kt deleted file mode 100644 index bcc1f2f..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDB.kt +++ /dev/null @@ -1,28 +0,0 @@ -package cn.super12138.todo.logic.dao - -import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase - -@Database(entities = [ToDoRoom::class], version = 1) -abstract class ToDoRoomDB : RoomDatabase() { - abstract fun toDoRoomDao(): ToDoRoomDao - - companion object { - @Volatile - private var INSTANCE: ToDoRoomDB? = null - fun getDatabase(context: Context): ToDoRoomDB { - return INSTANCE ?: synchronized(this) { - val instance = Room.databaseBuilder( - context.applicationContext, - ToDoRoomDB::class.java, - "todo" - ).build() - - INSTANCE = instance - return instance - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDao.kt b/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDao.kt deleted file mode 100644 index c05fbd3..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDao.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cn.super12138.todo.logic.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.Query -import androidx.room.Update - -@Dao -interface ToDoRoomDao { - @Insert - suspend fun insert(toDoRoom: ToDoRoom) - - @Query("SELECT * FROM todo") - suspend fun getAll(): List - - @Query("SELECT * FROM todo WHERE state = 0") - suspend fun getAllUnfinished(): List - - @Query("SELECT * FROM todo WHERE state = 1") - suspend fun getAllComplete(): List - - @Query("DELETE FROM todo") - suspend fun deleteAll() - - @Query("DELETE FROM todo WHERE uuid = :uuid") - suspend fun deleteByUUID(uuid: String) - - @Query("UPDATE todo SET state = 1 WHERE uuid = :uuid") - suspend fun updateStateByUUID(uuid: String) - - @Update - suspend fun update(toDoRoom: ToDoRoom) -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/ToDo.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/ToDo.kt deleted file mode 100644 index 86a7caa..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/logic/model/ToDo.kt +++ /dev/null @@ -1,5 +0,0 @@ -package cn.super12138.todo.logic.model - -data class ToDo(val uuid: String, val state: Int, val content: String, val subject: String) { - // var isAnimated = false -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Color.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Color.kt new file mode 100644 index 0000000..9233036 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package cn.super12138.todo.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt new file mode 100644 index 0000000..0ff3880 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package cn.super12138.todo.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun ToDoTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Type.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Type.kt new file mode 100644 index 0000000..c25d260 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package cn.super12138.todo.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/TextUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/TextUtils.kt deleted file mode 100644 index 93adf32..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/utils/TextUtils.kt +++ /dev/null @@ -1,49 +0,0 @@ -package cn.super12138.todo.utils - -import android.text.Editable -import cn.super12138.todo.R -import cn.super12138.todo.ToDoApp - -object TextUtils { - // 更换为id匹配(数据库) - private val subjectMap = mapOf( - globalGetString(R.string.subject_chinese) to R.id.subject_chinese, - globalGetString(R.string.subject_math) to R.id.subject_math, - globalGetString(R.string.subject_english) to R.id.subject_english, - globalGetString(R.string.subject_biology) to R.id.subject_biology, - globalGetString(R.string.subject_geography) to R.id.subject_geography, - globalGetString(R.string.subject_history) to R.id.subject_history, - globalGetString(R.string.subject_physics) to R.id.subject_physics, - globalGetString(R.string.subject_chemistry) to R.id.subject_chemistry, - globalGetString(R.string.subject_law) to R.id.subject_law, - globalGetString(R.string.subject_other) to R.id.subject_other - ) - - fun getSubjectName(id: Int): String { - return when (id) { - R.id.subject_chinese -> globalGetString(R.string.subject_chinese) - R.id.subject_math -> globalGetString(R.string.subject_math) - R.id.subject_english -> globalGetString(R.string.subject_english) - R.id.subject_biology -> globalGetString(R.string.subject_biology) - R.id.subject_geography -> globalGetString(R.string.subject_geography) - R.id.subject_history -> globalGetString(R.string.subject_history) - R.id.subject_physics -> globalGetString(R.string.subject_physics) - R.id.subject_chemistry -> globalGetString(R.string.subject_chemistry) - R.id.subject_law -> globalGetString(R.string.subject_law) - R.id.subject_other -> globalGetString(R.string.subject_other) - else -> globalGetString(R.string.subject_unknown) - } - } - - fun getSubjectID(name: String): Int? { - return subjectMap[name] - } -} - -fun globalGetString(resID: Int): String { - return ToDoApp.context.resources.getString(resID) -} - -fun String.toEditable(): Editable? { - return Editable.Factory.getInstance().newEditable(this) -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/Utils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/Utils.kt deleted file mode 100644 index 6d8d67f..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/utils/Utils.kt +++ /dev/null @@ -1,36 +0,0 @@ -package cn.super12138.todo.utils - -import android.content.Context -import android.os.SystemClock -import android.view.View -import android.widget.Toast -import cn.super12138.todo.ToDoApp - -private var clickInterval = 380L -private var lastTime = 0L - -/** - * 延迟点击 - */ -fun View.setOnIntervalClickListener(onIntervalClickListener: (View) -> Unit) { - this.setOnClickListener { - if (SystemClock.elapsedRealtime() - lastTime > clickInterval) { - lastTime = SystemClock.elapsedRealtime() - onIntervalClickListener.invoke(it) - } - } -} - -/** - * Toast - */ -fun String.showToast( - context: Context = ToDoApp.context, - duration: Int = Toast.LENGTH_SHORT -) { - Toast.makeText(context, this, duration).show() -} - -fun Int.showToast(context: Context = ToDoApp.context, duration: Int = Toast.LENGTH_SHORT) { - Toast.makeText(context, this, duration).show() -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/VersionUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/VersionUtils.kt deleted file mode 100644 index 56c6dc9..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/utils/VersionUtils.kt +++ /dev/null @@ -1,21 +0,0 @@ -package cn.super12138.todo.utils - -import android.content.Context -import android.os.Build - -object VersionUtils { - /** - * 获取应用版本号 - * @return 版本名称(版本代码) - */ - fun getAppVersion(context: Context): String { - val pkgInfo = context.packageManager.getPackageInfo(context.packageName, 0) - val verName = pkgInfo.versionName - val verCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - pkgInfo.longVersionCode.toInt() - } else { - pkgInfo.versionCode - } - return "$verName ($verCode)" - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt deleted file mode 100644 index 6d4d4bb..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt +++ /dev/null @@ -1,16 +0,0 @@ -package cn.super12138.todo.utils - -import android.view.HapticFeedbackConstants -import android.view.View -import cn.super12138.todo.constant.GlobalValues - -object VibrationUtils { - fun performHapticFeedback( - view: View?, - hapticFeedbackConstants: Int = HapticFeedbackConstants.KEYBOARD_TAP - ) { - if (GlobalValues.hapticFeedback) { - view?.performHapticFeedback(hapticFeedbackConstants) - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPDelegates.kt b/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPDelegates.kt deleted file mode 100644 index b00405a..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPDelegates.kt +++ /dev/null @@ -1,14 +0,0 @@ -package cn.super12138.todo.utils.sp - -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - -class SPDelegates(private val key: String, private val default: T) : ReadWriteProperty { - override fun getValue(thisRef: Any?, property: KProperty<*>): T { - return SPUtils.getValue(key, default) - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { - SPUtils.putValue(key, value) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPUtils.kt deleted file mode 100644 index dfbf247..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPUtils.kt +++ /dev/null @@ -1,36 +0,0 @@ -package cn.super12138.todo.utils.sp - -import android.content.Context -import android.content.SharedPreferences -import cn.super12138.todo.ToDoApp -import cn.super12138.todo.constant.Constants - -object SPUtils { - private val sp: SharedPreferences by lazy { - ToDoApp.context.getSharedPreferences(Constants.SP_NAME, Context.MODE_PRIVATE) - } - - fun getValue(name: String, default: T): T = with(sp) { - val res: Any = when (default) { - is Long -> getLong(name, default) - is String -> getString(name, default).orEmpty() - is Int -> getInt(name, default) - is Boolean -> getBoolean(name, default) - is Float -> getFloat(name, default) - else -> throw java.lang.IllegalArgumentException() - } - @Suppress("UNCHECKED_CAST") - res as T - } - - fun putValue(name: String, value: T) = with(sp.edit()) { - when (value) { - is Long -> putLong(name, value) - is String -> putString(name, value) - is Int -> putInt(name, value) - is Boolean -> putBoolean(name, value) - is Float -> putFloat(name, value) - else -> throw IllegalArgumentException("This type can't be saved into Preferences") - }.apply() - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/BaseActivity.kt b/app/src/main/kotlin/cn/super12138/todo/views/BaseActivity.kt deleted file mode 100644 index 384583d..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/BaseActivity.kt +++ /dev/null @@ -1,45 +0,0 @@ -package cn.super12138.todo.views - -import android.os.Build -import android.os.Bundle -import android.view.WindowManager -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate -import androidx.viewbinding.ViewBinding -import cn.super12138.todo.constant.GlobalValues - -abstract class BaseActivity : AppCompatActivity() { - lateinit var binding: T - override fun onCreate(savedInstanceState: Bundle?) { - /*if (GlobalValues.springFestivalTheme) { - setTheme(R.style.Theme_SpringFestival) - }*/ - // enableEdgeToEdge(navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT)) - super.onCreate(savedInstanceState) - - // 确保 Navigation Bar 区域会被显示 - // WindowCompat.setDecorFitsSystemWindows(window, false) - - binding = getViewBinding() - setContentView(binding.root) - - // 深色模式 - when (GlobalValues.darkMode) { - "0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) - - "1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - - "2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - } - - // 适配刘海屏 - val lp = window.attributes - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - lp.layoutInDisplayCutoutMode = - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - } - window.attributes = lp - } - - abstract fun getViewBinding(): T -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/BaseFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/BaseFragment.kt deleted file mode 100644 index 2e58b76..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/BaseFragment.kt +++ /dev/null @@ -1,50 +0,0 @@ -package cn.super12138.todo.views - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.viewbinding.ViewBinding -import com.google.android.material.color.MaterialColors -import com.google.android.material.transition.MaterialSharedAxis - -abstract class BaseFragment : Fragment() { - private var _binding: T? = null - protected val binding get() = _binding!! - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true) - returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false) - exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true) - reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - _binding = getViewBinding(inflater, container, false) - return binding.root - } - - // https://github.com/material-components/material-components-android/issues/1984#issuecomment-1089710991 - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - // Overlap colors. - view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.colorBackground)) - } - - abstract fun getViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - attachToRoot: Boolean - ): T - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashActivity.kt b/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashActivity.kt deleted file mode 100644 index b8cfce2..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashActivity.kt +++ /dev/null @@ -1,51 +0,0 @@ -package cn.super12138.todo.views.activities - -import android.os.Build -import android.os.Bundle -import cn.super12138.todo.databinding.ActivityCrashBinding -import cn.super12138.todo.utils.VersionUtils -import cn.super12138.todo.utils.VibrationUtils -import cn.super12138.todo.views.BaseActivity -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Locale - -class CrashActivity : BaseActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val crashLogs = intent.getStringExtra("crash_logs") - - val deviceBrand = Build.BRAND - val deviceModel = Build.MODEL - val sdkLevel = Build.VERSION.SDK_INT - val currentDateTime = Calendar.getInstance().time - val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) - val formattedDateTime = formatter.format(currentDateTime) - - val deviceInfo = StringBuilder().apply { - append("ToDo version: ").append(VersionUtils.getAppVersion(this@CrashActivity)) - .append('\n') - append("Brand: ").append("").append(deviceBrand).append('\n') - append("Model: ").append(deviceModel).append('\n') - append("Device SDK: ").append(sdkLevel).append('\n').append('\n') - append("Crash time: ").append(formattedDateTime).append('\n').append('\n') - append("======beginning of crash======").append('\n') - } - - binding.crashLog.text = StringBuilder().apply { - append(deviceInfo) - append(crashLogs) - } - - binding.exitApp.setOnClickListener { - VibrationUtils.performHapticFeedback(it) - - this.finishAffinity() - } - } - - override fun getViewBinding(): ActivityCrashBinding { - return ActivityCrashBinding.inflate(layoutInflater) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashHandler.kt b/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashHandler.kt deleted file mode 100644 index 07325db..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashHandler.kt +++ /dev/null @@ -1,29 +0,0 @@ -package cn.super12138.todo.views.activities - -import android.content.Context -import android.content.Intent -import android.os.Process -import kotlin.system.exitProcess - -class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandler { - - private val defaultUEH = Thread.getDefaultUncaughtExceptionHandler() - - override fun uncaughtException(thread: Thread, ex: Throwable) { - val stackTrace = ex.stackTraceToString() - - // 启动新的 Activity 来显示崩溃日志 - val intent = Intent(context, CrashActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - putExtra("crash_logs", stackTrace) - } - context.startActivity(intent) - - // 传递异常给默认的异常处理器 - defaultUEH?.uncaughtException(thread, ex) - - // 杀掉崩溃的应用程序进程 - Process.killProcess(Process.myPid()) - exitProcess(10) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/activities/MainActivity.kt b/app/src/main/kotlin/cn/super12138/todo/views/activities/MainActivity.kt deleted file mode 100644 index e718af2..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/activities/MainActivity.kt +++ /dev/null @@ -1,66 +0,0 @@ -package cn.super12138.todo.views.activities -// 2023.11.18立项 -import android.app.ComponentCaller -import android.content.Intent -import android.os.Bundle -import android.view.WindowManager -import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.fragment.app.Fragment -import androidx.fragment.app.commit -import cn.super12138.todo.R -import cn.super12138.todo.constant.GlobalValues -import cn.super12138.todo.databinding.ActivityMainBinding -import cn.super12138.todo.views.BaseActivity -import cn.super12138.todo.views.fragments.SettingsParentFragment -import cn.super12138.todo.views.fragments.welcome.WelcomeFragment -import de.raphaelebner.roomdatabasebackup.core.RoomBackup - -class MainActivity : BaseActivity() { - lateinit var roomBackup: RoomBackup - override fun onCreate(savedInstanceState: Bundle?) { - installSplashScreen() - super.onCreate(savedInstanceState) - - if (!GlobalValues.welcomePage) { - startFragment(WelcomeFragment()) - } - - when (GlobalValues.secureMode) { - true -> window.setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE - ) - - false -> window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - } - handleIntent(intent) - - roomBackup = RoomBackup(this) - } - - override fun onNewIntent(intent: Intent, caller: ComponentCaller) { - super.onNewIntent(intent, caller) - handleIntent(intent) - } - - override fun getViewBinding(): ActivityMainBinding { - return ActivityMainBinding.inflate(layoutInflater) - } - - fun startFragment(fragment: Fragment, args: (Bundle.() -> Unit)? = null) { - supportFragmentManager.commit { - addToBackStack(System.currentTimeMillis().toString()) - hide(supportFragmentManager.fragments.last()) - add( - R.id.app_container, - fragment.apply { args?.let { arguments = Bundle().apply(it) } } - ) - } - } - - private fun handleIntent(intent: Intent) { - when (intent.action) { - Intent.ACTION_APPLICATION_PREFERENCES -> startFragment(SettingsParentFragment()) - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/adapters/AllTasksAdapter.kt b/app/src/main/kotlin/cn/super12138/todo/views/adapters/AllTasksAdapter.kt deleted file mode 100644 index 260d503..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/adapters/AllTasksAdapter.kt +++ /dev/null @@ -1,85 +0,0 @@ -package cn.super12138.todo.views.adapters - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.LinearLayout -import android.widget.TextView -import androidx.core.content.ContextCompat -import androidx.fragment.app.FragmentManager -import androidx.recyclerview.widget.RecyclerView -import cn.super12138.todo.R -import cn.super12138.todo.ToDoApp -import cn.super12138.todo.constant.Constants.DEFAULT_VIEW_TYPE -import cn.super12138.todo.constant.Constants.EMPTY_VIEW_TYPE -import cn.super12138.todo.logic.model.ToDo -import cn.super12138.todo.utils.VibrationUtils -import cn.super12138.todo.views.fragments.InfoBottomSheet - -class AllTasksAdapter( - private val todoList: MutableList, - private val fragmentManager: FragmentManager -) : RecyclerView.Adapter() { - // 默认待办项 - inner class DefaultViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val todoContext: TextView = view.findViewById(R.id.todo_content) - val todoSubject: TextView = view.findViewById(R.id.todo_subject) - val itemBackground: LinearLayout = view.findViewById(R.id.item_background) - val checkBtn: Button = view.findViewById(R.id.check_item_btn) - } - - // 空项目提示 - inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return if (viewType == EMPTY_VIEW_TYPE) { // 如果列表是空的 - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_empty, parent, false) - EmptyViewHolder(view) - } else { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_todo, parent, false) - DefaultViewHolder(view) - } - } - - override fun getItemViewType(position: Int): Int { - return if (todoList.isEmpty()) EMPTY_VIEW_TYPE else DEFAULT_VIEW_TYPE - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - // 判断当前的holder是不是待办项目的holder - if (holder is DefaultViewHolder) { - val todo = todoList[position] - holder.apply { - checkBtn.visibility = View.GONE - todoContext.text = todo.content - todoSubject.text = todo.subject - } - - if (todo.state == 1) { - holder.itemBackground.background = - ContextCompat.getDrawable(ToDoApp.context, R.drawable.bg_item_complete) - } else { - holder.itemBackground.background = null - } - - holder.itemView.setOnClickListener { - VibrationUtils.performHapticFeedback(it) - - val infoBottomSheet = InfoBottomSheet.newInstance( - todo.content, - todo.subject, - todo.state, - todo.uuid - ) - infoBottomSheet.show(fragmentManager, InfoBottomSheet.TAG) - } - } - } - - override fun getItemCount(): Int { - return if (todoList.isEmpty()) 1 else todoList.size - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/adapters/ToDoAdapter.kt b/app/src/main/kotlin/cn/super12138/todo/views/adapters/ToDoAdapter.kt deleted file mode 100644 index 99c7537..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/adapters/ToDoAdapter.kt +++ /dev/null @@ -1,108 +0,0 @@ -package cn.super12138.todo.views.adapters - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.TextView -import androidx.fragment.app.FragmentManager -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelStoreOwner -import androidx.recyclerview.widget.RecyclerView -import cn.super12138.todo.R -import cn.super12138.todo.constant.Constants.DEFAULT_VIEW_TYPE -import cn.super12138.todo.constant.Constants.EMPTY_VIEW_TYPE -import cn.super12138.todo.constant.GlobalValues -import cn.super12138.todo.logic.model.ToDo -import cn.super12138.todo.utils.VibrationUtils -import cn.super12138.todo.utils.showToast -import cn.super12138.todo.views.fragments.ToDoBottomSheet -import cn.super12138.todo.views.viewmodels.ProgressViewModel -import cn.super12138.todo.views.viewmodels.ToDoViewModel - -class ToDoAdapter( - private val todoList: MutableList, - private val viewModelStoreOwner: ViewModelStoreOwner, - private val fragmentManager: FragmentManager -) : RecyclerView.Adapter() { - // 默认待办项 - inner class DefaultViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val todoContext: TextView = view.findViewById(R.id.todo_content) - val todoSubject: TextView = view.findViewById(R.id.todo_subject) - val checkToDoBtn: Button = view.findViewById(R.id.check_item_btn) - } - - // 空项目提示 - inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view) - - // 判断列表是否为空,为空显示空项目提示 - override fun getItemViewType(position: Int): Int { - return if (todoList.isEmpty()) EMPTY_VIEW_TYPE else DEFAULT_VIEW_TYPE - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return if (viewType == EMPTY_VIEW_TYPE) { // 如果列表是空的 - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_empty, parent, false) - EmptyViewHolder(view) - } else { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_todo, parent, false) - DefaultViewHolder(view) - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - // 判断当前的holder是不是待办项目的holder - if (holder is DefaultViewHolder) { - val todo = todoList[position] - holder.todoContext.text = todo.content - holder.todoSubject.text = todo.subject - - val progressViewModel = - ViewModelProvider(viewModelStoreOwner)[ProgressViewModel::class.java] - val todoViewModel = - ViewModelProvider(viewModelStoreOwner)[ToDoViewModel::class.java] - - holder.checkToDoBtn.setOnClickListener { - VibrationUtils.performHapticFeedback(it) - - if (position >= todoList.size) { - return@setOnClickListener - } - - todoList.removeAt(position) - notifyItemRemoved(position) - notifyItemRangeChanged(position, todoList.size) - - todoViewModel.updateTaskState(todo.uuid) - - progressViewModel.updateProgress() - } - - holder.itemView.setOnClickListener { - if (GlobalValues.devMode) { - VibrationUtils.performHapticFeedback(it) - "Current position: $position".showToast() - } - } - - holder.itemView.setOnLongClickListener { - val toDoBottomSheet = ToDoBottomSheet.newInstance( - true, - position, - todo.uuid, - todo.state, - todo.subject, - todo.content - ) - toDoBottomSheet.show(fragmentManager, ToDoBottomSheet.TAG) - true - } - } - } - - override fun getItemCount(): Int { - return if (todoList.isEmpty()) 1 else todoList.size - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/AboutFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/AboutFragment.kt deleted file mode 100644 index f5fb487..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/AboutFragment.kt +++ /dev/null @@ -1,85 +0,0 @@ -package cn.super12138.todo.views.fragments - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import cn.super12138.todo.constant.Constants -import cn.super12138.todo.databinding.FragmentAboutBinding -import cn.super12138.todo.utils.VersionUtils -import cn.super12138.todo.utils.VibrationUtils -import cn.super12138.todo.utils.showToast -import cn.super12138.todo.views.BaseFragment - -class AboutFragment : BaseFragment() { - private var clickCount = 0 - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.appVersion.text = VersionUtils.getAppVersion(requireActivity()) - - binding.toolBar.setNavigationOnClickListener { - VibrationUtils.performHapticFeedback(it) - - requireActivity().supportFragmentManager.popBackStack() - } - - binding.checkUpdate.setOnClickListener { - VibrationUtils.performHapticFeedback(it) - - val intent = Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse(Constants.UPDATE_URL) - } - startActivity(intent) - } - - binding.openSource.setOnClickListener { - VibrationUtils.performHapticFeedback(it) - - val intent = Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse(Constants.REPO_GITHUB_URL) - } - startActivity(intent) - } - - - binding.developerInfo.setOnClickListener { - VibrationUtils.performHapticFeedback(it) - - val intent = Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse(Constants.AUTHOR_GITHUB_URL) - } - startActivity(intent) - } - - binding.appVersion.setOnClickListener { - clickCount++ - when (clickCount) { - 5 -> { - clickCount = 0 - // GlobalValues.springFestivalTheme = !GlobalValues.springFestivalTheme - "🍂".showToast() - /*when (java.util.Calendar.getInstance(Locale.getDefault()) - .get(java.util.Calendar.MONTH) + 1) { - 3, 4, 5 -> - 6, 7, 8 -> SUMMER - 9, 10, 11 -> AUTUMN - 12, 1, 2 -> WINTER - else -> -12 - }*/ - } - } - } - } - - override fun getViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - attachToRoot: Boolean - ): FragmentAboutBinding { - return FragmentAboutBinding.inflate(inflater, container, attachToRoot) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/AllTasksFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/AllTasksFragment.kt deleted file mode 100644 index 0e69f0a..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/AllTasksFragment.kt +++ /dev/null @@ -1,53 +0,0 @@ -package cn.super12138.todo.views.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer -import androidx.recyclerview.widget.LinearLayoutManager -import cn.super12138.todo.ToDoApp -import cn.super12138.todo.databinding.FragmentAllTasksBinding -import cn.super12138.todo.utils.VibrationUtils -import cn.super12138.todo.views.BaseFragment -import cn.super12138.todo.views.adapters.AllTasksAdapter -import cn.super12138.todo.views.viewmodels.AllTasksViewModel -import me.zhanghai.android.fastscroll.FastScrollerBuilder - -class AllTasksFragment : BaseFragment() { - private val viewModel by viewModels() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val todoListAll = viewModel.todoListAll - val layoutManager = LinearLayoutManager(ToDoApp.context) - binding.allTasksList.layoutManager = layoutManager - val adapter = AllTasksAdapter(todoListAll, childFragmentManager) - binding.allTasksList.adapter = adapter - - FastScrollerBuilder(binding.allTasksList).apply { - useMd2Style() - build() - } - - binding.toolBar.setNavigationOnClickListener { - VibrationUtils.performHapticFeedback(it) - - requireActivity().supportFragmentManager.popBackStack() - } - - viewModel.refreshData.observe(viewLifecycleOwner, Observer { - binding.allTasksList.adapter?.notifyItemRangeChanged(0, todoListAll.size + 1) - }) - } - - override fun getViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - attachToRoot: Boolean - ): FragmentAllTasksBinding { - return FragmentAllTasksBinding.inflate(inflater, container, attachToRoot) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/InfoBottomSheet.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/InfoBottomSheet.kt deleted file mode 100644 index a1f151a..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/InfoBottomSheet.kt +++ /dev/null @@ -1,81 +0,0 @@ -package cn.super12138.todo.views.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import cn.super12138.todo.R -import cn.super12138.todo.constant.Constants -import cn.super12138.todo.constant.GlobalValues -import cn.super12138.todo.databinding.BottomSheetInfoBinding -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.bottomsheet.BottomSheetDialogFragment - -class InfoBottomSheet : BottomSheetDialogFragment() { - - private lateinit var binding: BottomSheetInfoBinding - private lateinit var todoContent: String - private lateinit var todoSubject: String - private var todoState: Int = 0 - private lateinit var todoUUID: String - - companion object { - const val TAG = Constants.TAG_INFO_BOTTOM_SHEET - fun newInstance( - todoContent: String, - todoSubject: String, - todoState: Int, - todoUUID: String - ) = InfoBottomSheet().apply { - arguments = Bundle().apply { - putString(Constants.BUNDLE_TODO_CONTENT, todoContent) - putString(Constants.BUNDLE_TODO_SUBJECT, todoSubject) - putInt(Constants.BUNDLE_TODO_STATE, todoState) - putString(Constants.BUNDLE_TODO_UUID, todoUUID) - } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - arguments?.let { - todoContent = it.getString(Constants.BUNDLE_TODO_CONTENT, "") - todoSubject = it.getString(Constants.BUNDLE_TODO_SUBJECT, "") - todoState = it.getInt(Constants.BUNDLE_TODO_STATE, 0) - todoUUID = it.getString(Constants.BUNDLE_TODO_UUID, "") - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = BottomSheetInfoBinding.inflate(inflater, container, false) - - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val bottomSheetDialog = dialog as BottomSheetDialog - bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED - bottomSheetDialog.dismissWithAnimation = true - - binding.todoContentInfo.text = todoContent - binding.todoSubjectInfo.text = String.format(getString(R.string.info_subject), todoSubject) - if (todoState == 0) { - binding.todoState.text = getString(R.string.info_state_incomplete) - } else { - binding.todoState.text = getString(R.string.info_state_complete) - } - if (GlobalValues.devMode) { - binding.todoUuid.apply { - visibility = View.VISIBLE - text = String.format(getString(R.string.info_uuid), todoUUID) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/MainFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/MainFragment.kt deleted file mode 100644 index f21b23b..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/MainFragment.kt +++ /dev/null @@ -1,41 +0,0 @@ -package cn.super12138.todo.views.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import cn.super12138.todo.R -import cn.super12138.todo.databinding.FragmentMainBinding -import cn.super12138.todo.utils.VibrationUtils -import cn.super12138.todo.views.BaseFragment -import cn.super12138.todo.views.activities.MainActivity - -class MainFragment : BaseFragment() { - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.toolBar.setOnMenuItemClickListener { menuItem -> - when (menuItem.itemId) { - R.id.item_settings -> { - (requireActivity() as MainActivity).startFragment(SettingsParentFragment()) - - VibrationUtils.performHapticFeedback(binding.toolBar) - - true - } - - else -> false - } - } - - // setSupportActionBar(binding.toolbar) - } - - override fun getViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - attachToRoot: Boolean - ): FragmentMainBinding { - return FragmentMainBinding.inflate(inflater, container, attachToRoot) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ProgressFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/ProgressFragment.kt deleted file mode 100644 index ebeed5f..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ProgressFragment.kt +++ /dev/null @@ -1,52 +0,0 @@ -package cn.super12138.todo.views.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer -import cn.super12138.todo.R -import cn.super12138.todo.databinding.FragmentProgressBinding -import cn.super12138.todo.views.BaseFragment -import cn.super12138.todo.views.viewmodels.ProgressViewModel - - -class ProgressFragment : BaseFragment() { - private val viewModel: ProgressViewModel by viewModels({ requireActivity() }) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - viewModel.progress.observe(viewLifecycleOwner, Observer { value -> - binding.progressBar.setProgressCompat(value, true) - }) - - viewModel.totalCount.observe(viewLifecycleOwner, Observer { total -> - binding.totalCount.text = total.toString() - }) - - viewModel.completeCount.observe(viewLifecycleOwner, Observer { complete -> - binding.completeCount.text = complete.toString() - }) - - viewModel.remainCount.observe(viewLifecycleOwner, Observer { remain -> - if (remain == 0) { - binding.remainCount.visibility = View.GONE - } else { - binding.remainCount.visibility = View.VISIBLE - binding.remainCount.text = - String.format(getString(R.string.remain_text), remain.toString()) - } - }) - - viewModel.updateProgress() - } - - override fun getViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - attachToRoot: Boolean - ): FragmentProgressBinding { - return FragmentProgressBinding.inflate(inflater, container, attachToRoot) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsFragment.kt deleted file mode 100644 index ccbd0f3..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsFragment.kt +++ /dev/null @@ -1,195 +0,0 @@ -package cn.super12138.todo.views.fragments - -import android.content.Context -import android.content.Intent -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.graphics.drawable.Drawable -import android.os.Bundle -import androidx.appcompat.app.AppCompatDelegate -import androidx.preference.ListPreference -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import androidx.preference.SwitchPreferenceCompat -import cn.super12138.todo.R -import cn.super12138.todo.constant.Constants -import cn.super12138.todo.constant.GlobalValues -import cn.super12138.todo.logic.dao.ToDoRoomDB -import cn.super12138.todo.utils.VibrationUtils -import cn.super12138.todo.views.activities.MainActivity -import cn.super12138.todo.views.fragments.welcome.WelcomeFragment -import com.google.android.material.snackbar.Snackbar -import de.raphaelebner.roomdatabasebackup.core.RoomBackup -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import kotlin.system.exitProcess - -class SettingsFragment : PreferenceFragmentCompat() { - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.preferences, rootKey) - val mainActivity = requireActivity() as MainActivity - val roomBackup = mainActivity.roomBackup - - findPreference(Constants.PREF_DARK_MODE)?.apply { - setOnPreferenceClickListener { - VibrationUtils.performHapticFeedback(view) - true - } - setOnPreferenceChangeListener { _, newValue -> - when (newValue) { - "0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) - - "1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - - "2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - } - activity?.recreate() - true - } - } - - findPreference(Constants.PREF_SECURE_MODE)?.apply { - setOnPreferenceChangeListener { _, _ -> - VibrationUtils.performHapticFeedback(view) - - - view?.let { - Snackbar.make(it, R.string.need_restart_app, Snackbar.LENGTH_LONG) - .setAction(R.string.restart_app_now) { - VibrationUtils.performHapticFeedback(view) - restartApp(context) - } - .show() - } - - true - } - } - - findPreference(Constants.PREF_HAPTIC_FEEDBACK)?.apply { - setOnPreferenceChangeListener { _, _ -> - VibrationUtils.performHapticFeedback(view) - - true - } - } - - findPreference(Constants.PREF_BACKUP_DB)?.apply { - setOnPreferenceClickListener { - VibrationUtils.performHapticFeedback(view) - val simpleDateFormat = - SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()) - val formattedDate = simpleDateFormat.format(Date()) - roomBackup - .database(ToDoRoomDB.getDatabase(requireContext())) - .enableLogDebug(GlobalValues.devMode) - .backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG) - .customBackupFileName("ToDo-DataBase-$formattedDate.sqlite3") - .apply { - onCompleteListener { success, _, exitCode -> - if (success) { - view?.let { it1 -> - Snackbar.make( - it1, R.string.tips_backup_success, Snackbar.LENGTH_LONG - ).show() - } - } else { - view?.let { it1 -> - Snackbar.make( - it1, getString( - R.string.tips_backup_failed, - exitCode - ), Snackbar.LENGTH_LONG - ).show() - } - } - } - } - .backup() - true - } - } - - findPreference(Constants.PREF_RESTORE_DB)?.apply { - setOnPreferenceClickListener { - VibrationUtils.performHapticFeedback(view) - - roomBackup - .database(ToDoRoomDB.getDatabase(requireContext())) - .enableLogDebug(GlobalValues.devMode) - .backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG) - .apply { - onCompleteListener { success, _, exitCode -> - if (success) { - view?.let { it1 -> - Snackbar.make( - it1, - R.string.tips_restore_success, - Snackbar.LENGTH_LONG - ) - .setAction(R.string.restart_app_now) { - VibrationUtils.performHapticFeedback(view) - restartApp(context) - } - .show() - } - - } else { - view?.let { it1 -> - Snackbar.make( - it1, getString( - R.string.tips_restore_failed, - exitCode - ), Snackbar.LENGTH_LONG - ).show() - } - } - } - } - .restore() - true - } - } - - findPreference(Constants.PREF_ALL_TASKS)?.apply { - setOnPreferenceClickListener { - mainActivity.startFragment(AllTasksFragment()) - VibrationUtils.performHapticFeedback(view) - true - } - } - - findPreference(Constants.PREF_REENTER_WELCOME_ACTIVITY)?.apply { - setOnPreferenceClickListener { - mainActivity.startFragment(WelcomeFragment()) - VibrationUtils.performHapticFeedback(view) - true - } - } - - findPreference(Constants.PREF_ABOUT)?.apply { - setOnPreferenceClickListener { - mainActivity.startFragment(AboutFragment()) - VibrationUtils.performHapticFeedback(view) - true - } - } - } - - override fun setDivider(divider: Drawable?) { - super.setDivider(ColorDrawable(Color.TRANSPARENT)) - } - - override fun setDividerHeight(height: Int) { - super.setDividerHeight(0) - } - - private fun restartApp(restartContext: Context) { - val intent = Intent(restartContext, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - restartContext.startActivity(intent) - exitProcess(0) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsParentFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsParentFragment.kt deleted file mode 100644 index 87b0c26..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsParentFragment.kt +++ /dev/null @@ -1,29 +0,0 @@ -package cn.super12138.todo.views.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import cn.super12138.todo.databinding.FragmentSettingsBinding -import cn.super12138.todo.utils.VibrationUtils -import cn.super12138.todo.views.BaseFragment - -class SettingsParentFragment : BaseFragment() { - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.toolBar.setNavigationOnClickListener { - VibrationUtils.performHapticFeedback(it) - - requireActivity().supportFragmentManager.popBackStack() - } - } - - override fun getViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - attachToRoot: Boolean - ): FragmentSettingsBinding { - return FragmentSettingsBinding.inflate(inflater, container, attachToRoot) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoBottomSheet.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoBottomSheet.kt deleted file mode 100644 index 0138b2d..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoBottomSheet.kt +++ /dev/null @@ -1,192 +0,0 @@ -package cn.super12138.todo.views.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.viewModels -import cn.super12138.todo.R -import cn.super12138.todo.constant.Constants -import cn.super12138.todo.constant.GlobalValues -import cn.super12138.todo.databinding.BottomSheetTodoBinding -import cn.super12138.todo.logic.dao.ToDoRoom -import cn.super12138.todo.logic.model.ToDo -import cn.super12138.todo.utils.TextUtils -import cn.super12138.todo.utils.VibrationUtils -import cn.super12138.todo.utils.showToast -import cn.super12138.todo.utils.toEditable -import cn.super12138.todo.views.viewmodels.ProgressViewModel -import cn.super12138.todo.views.viewmodels.ToDoViewModel -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import java.util.UUID - -class ToDoBottomSheet : BottomSheetDialogFragment() { - private lateinit var binding: BottomSheetTodoBinding - - private val progressViewModel: ProgressViewModel by viewModels({ requireActivity() }) - private val todoViewModel: ToDoViewModel by viewModels({ requireActivity() }) - - private var editMode: Boolean = false - private var todoState: Int = 0 - private var todoPosition: Int = 0 - private lateinit var todoUUID: String - private lateinit var todoOrigSubject: String - private lateinit var todoOrigContent: String - - companion object { - const val TAG = Constants.TAG_TODO_BOTTOM_SHEET - - fun newInstance( - editMode: Boolean, - todoPosition: Int, - todoUUID: String, - todoState: Int, - todoSubject: String, - todoContent: String, - ) = ToDoBottomSheet().apply { - arguments = Bundle().apply { - putBoolean(Constants.BUNDLE_EDIT_MODE, editMode) - putInt(Constants.BUNDLE_POSITION, todoPosition) - putString(Constants.BUNDLE_TODO_UUID, todoUUID) - putInt(Constants.BUNDLE_TODO_STATE, todoState) - putString(Constants.BUNDLE_TODO_SUBJECT, todoSubject) - putString(Constants.BUNDLE_TODO_CONTENT, todoContent) - } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - arguments?.let { - editMode = it.getBoolean(Constants.BUNDLE_EDIT_MODE, false) - todoPosition = it.getInt(Constants.BUNDLE_POSITION, 0) - todoOrigContent = it.getString(Constants.BUNDLE_TODO_CONTENT, "") - todoOrigSubject = it.getString(Constants.BUNDLE_TODO_SUBJECT, "") - todoState = it.getInt(Constants.BUNDLE_TODO_STATE, 0) - todoUUID = it.getString(Constants.BUNDLE_TODO_UUID, "") - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = BottomSheetTodoBinding.inflate(inflater, container, false) - - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - // BottomSheet 基本参数设置 - val bottomSheetDialog = dialog as BottomSheetDialog - bottomSheetDialog.behavior.apply { - state = BottomSheetBehavior.STATE_EXPANDED - saveFlags = BottomSheetBehavior.SAVE_ALL - } - bottomSheetDialog.dismissWithAnimation = true - - val todoList = todoViewModel.todoList - - binding.todoSubject.setOnCheckedStateChangeListener { _, _ -> - VibrationUtils.performHapticFeedback(binding.todoSubject) - } - - // 编辑模式 - if (editMode) { - binding.btnCancel.visibility = View.GONE - binding.btnDelete.visibility = View.VISIBLE - binding.todoSheetTitle.text = getString(R.string.update_task) - binding.todoContent.editText?.text = todoOrigContent.toEditable() - binding.btnSave.text = getString(R.string.update) - - TextUtils.getSubjectID(todoOrigSubject)?.let { binding.todoSubject.check(it) } - - if (GlobalValues.devMode) { - binding.todoUuid.apply { - text = "UUID: $todoUUID" - visibility = View.VISIBLE - } - } - } - - binding.btnSave.setOnClickListener { - VibrationUtils.performHapticFeedback(it) - - val todoContent = binding.todoContent.editText?.text.toString() - // 内容判空 - if (todoContent.isEmpty()) { - binding.todoContent.error = - getString(R.string.content_cannot_be_empty) - return@setOnClickListener - } else { - // 开发者模式 - if (todoContent == Constants.STRING_DEV_MODE) { - if (GlobalValues.devMode) { - GlobalValues.devMode = false - } else { - GlobalValues.devMode = true - "Dev Mode".showToast() - } - } else { - // 随机 UUID - val randomUUID = UUID.randomUUID().toString() - // 待办学科 - val todoSubject = TextUtils.getSubjectName(binding.todoSubject.checkedChipId) - - // 更新待办 - if (editMode) { - todoViewModel.updateTask( - todoPosition, - ToDoRoom( - todoUUID, - todoState, - todoSubject, - todoContent - ) - ) - todoViewModel.refreshData.value = 1 - } else { - // 添加到 RecyclerView - todoList.add( - ToDo(randomUUID, 0, todoContent, todoSubject) - ) - - // 插入数据库 - todoViewModel.insertTask( - ToDoRoom( - randomUUID, - 0, - todoSubject, - todoContent - ) - ) - progressViewModel.updateProgress() - todoViewModel.addData.value = 1 - } - } - } - dismiss() - } - - binding.btnCancel.setOnClickListener { - VibrationUtils.performHapticFeedback(it) - - dismiss() - } - - binding.btnDelete.setOnClickListener { - VibrationUtils.performHapticFeedback(it) - - todoViewModel.deleteTask(todoPosition, todoUUID) - progressViewModel.updateProgress() - todoViewModel.removeData.value = 1 - - dismiss() - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoFragment.kt deleted file mode 100644 index 051252e..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoFragment.kt +++ /dev/null @@ -1,122 +0,0 @@ -package cn.super12138.todo.views.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updateLayoutParams -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import cn.super12138.todo.R -import cn.super12138.todo.ToDoApp -import cn.super12138.todo.databinding.FragmentTodoBinding -import cn.super12138.todo.logic.Repository -import cn.super12138.todo.utils.VibrationUtils -import cn.super12138.todo.views.adapters.ToDoAdapter -import cn.super12138.todo.views.viewmodels.ProgressViewModel -import cn.super12138.todo.views.viewmodels.ToDoViewModel -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.launch -import me.zhanghai.android.fastscroll.FastScrollerBuilder - -class ToDoFragment : Fragment() { - private val progressViewModel: ProgressViewModel by viewModels({ requireActivity() }) - private val todoViewModel: ToDoViewModel by viewModels({ requireActivity() }) - private lateinit var binding: FragmentTodoBinding - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentTodoBinding.inflate(inflater, container, false) - - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - ViewCompat.setOnApplyWindowInsetsListener(binding.addItem) { v, windowInsets -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.updateLayoutParams { - leftMargin = if (insets.left == 0) 16 else insets.left - bottomMargin = if (insets.bottom == 0) 48 else insets.bottom + 32 - rightMargin = if (insets.right == 0) 48 else insets.right + 32 - } - WindowInsetsCompat.CONSUMED - } - - FastScrollerBuilder(binding.todoList).apply { - useMd2Style() - build() - } - - val layoutManager = LinearLayoutManager(ToDoApp.context) - binding.todoList.layoutManager = layoutManager - val todoList = todoViewModel.todoList - val adapter = ToDoAdapter(todoList, requireActivity(), parentFragmentManager) - binding.todoList.adapter = adapter - - binding.addItem.setOnClickListener { - VibrationUtils.performHapticFeedback(it) - - val toDoBottomSheet = ToDoBottomSheet() - toDoBottomSheet.show(parentFragmentManager, ToDoBottomSheet.TAG) - } - - binding.addItem.setOnLongClickListener { - activity?.let { it1 -> - MaterialAlertDialogBuilder(it1) - .setTitle(R.string.warning) - .setMessage(R.string.delete_confirm) - .setPositiveButton(R.string.ok) { _, _ -> - VibrationUtils.performHapticFeedback(it) - - // 清除 Recycler View - todoList.clear() - // 清除数据库 - lifecycleScope.launch { - Repository.deleteAll() - progressViewModel.updateProgress() - } - // 通知数据更改 - binding.todoList.adapter?.notifyDataSetChanged() - } - .setNegativeButton(R.string.cancel, null) - .show() - } - true - } - - binding.todoList.addOnScrollListener(object : RecyclerView.OnScrollListener() { - val fab = binding.addItem - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - // 列表下滑,隐藏FAB - if (dy > 0 && fab.isShown) { - fab.hide() - } - // 列表上滑,显示FAB - if (dy < 0 && !fab.isShown) { - fab.show() - } - } - }) - - todoViewModel.addData.observe(viewLifecycleOwner) { - binding.todoList.adapter?.notifyItemInserted(todoList.size + 1) - } - - todoViewModel.removeData.observe(viewLifecycleOwner) { - binding.todoList.adapter?.notifyItemRemoved(todoList.size + 1) - } - - todoViewModel.refreshData.observe(viewLifecycleOwner) { - binding.todoList.adapter?.notifyItemRangeChanged(0, todoList.size + 1) - } - } -} diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/WelcomeFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/WelcomeFragment.kt deleted file mode 100644 index 7fc68e8..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/WelcomeFragment.kt +++ /dev/null @@ -1,161 +0,0 @@ -package cn.super12138.todo.views.fragments.welcome - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.OnBackPressedCallback -import androidx.appcompat.content.res.AppCompatResources -import androidx.fragment.app.Fragment -import androidx.fragment.app.commit -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import cn.super12138.todo.R -import cn.super12138.todo.constant.GlobalValues -import cn.super12138.todo.databinding.FragmentWelcomeBinding -import cn.super12138.todo.utils.VibrationUtils -import cn.super12138.todo.utils.setOnIntervalClickListener -import cn.super12138.todo.views.BaseFragment -import cn.super12138.todo.views.activities.MainActivity -import cn.super12138.todo.views.fragments.MainFragment -import cn.super12138.todo.views.fragments.welcome.pages.IntroPage -import cn.super12138.todo.views.fragments.welcome.pages.ProgressPage -import cn.super12138.todo.views.fragments.welcome.pages.ToDoBtnPage -import cn.super12138.todo.views.fragments.welcome.pages.ToDoItemPage -import cn.super12138.todo.views.viewmodels.WelcomeViewModel -import kotlinx.coroutines.launch - -class WelcomeFragment : BaseFragment() { - private val viewModel by viewModels() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val previousBtn = binding.previousBtn - val nextBtn = binding.nextBtn - val centerBtn = binding.centerBtn - - val currentPage = viewModel.currentPage // 0: Intro 1: Progress 2: ToDo Btn 3: ToDo Item - - // 返回回调 - val callback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (currentPage.value == 0) { - requireActivity().finishAffinity() - } else { - childFragmentManager.popBackStack() - viewModel.decreasePage() - } - } - } - - - centerBtn.setOnIntervalClickListener { - VibrationUtils.performHapticFeedback(it) - - centerBtn.hide() - nextBtn.show() - previousBtn.show() - - if (currentPage.value == 3) { - GlobalValues.welcomePage = true - (requireActivity() as MainActivity).startFragment(MainFragment()) - } else { - viewModel.setCurrentPage(1) - nextPage(1) - } - } - - previousBtn.setOnIntervalClickListener { - VibrationUtils.performHapticFeedback(it) - - childFragmentManager.popBackStack() - - viewModel.decreasePage() - } - - nextBtn.setOnIntervalClickListener { - VibrationUtils.performHapticFeedback(it) - - nextPage(currentPage.value + 1) - - callback.isEnabled = false - viewModel.increasePage() - } - - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.currentPage.collect { page -> - when (page) { - 0 -> { - centerBtn.apply { - text = getString(R.string.start) - icon = AppCompatResources.getDrawable( - requireContext(), - R.drawable.ic_arrow_forward - ) - show() - } - nextBtn.hide() - previousBtn.hide() - } - - in 1..2 -> { - centerBtn.hide() - previousBtn.show() - nextBtn.show() - } - - 3 -> { - centerBtn.apply { - text = getString(R.string.enter_app) - icon = AppCompatResources.getDrawable( - requireContext(), - R.drawable.ic_focus - ) - show() - } - previousBtn.show() - nextBtn.hide() - } - - else -> { - if (page > 3) viewModel.setCurrentPage(3) - - if (page < 0) viewModel.setCurrentPage(0) - } - } - } - } - } - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback) - } - - private fun getCurrentPage(pageIndex: Int): Fragment { - return when (pageIndex) { - 0 -> IntroPage() - 1 -> ProgressPage() - 2 -> ToDoBtnPage() - 3 -> ToDoItemPage() - else -> IntroPage() - } - } - - private fun nextPage(page: Int) { - childFragmentManager.commit { - addToBackStack(System.currentTimeMillis().toString()) - hide(childFragmentManager.fragments.last()) - add(R.id.welcome_page_container, getCurrentPage(page)) - } - } - - override fun getViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - attachToRoot: Boolean - ): FragmentWelcomeBinding { - return FragmentWelcomeBinding.inflate(inflater, container, attachToRoot) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/IntroPage.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/IntroPage.kt deleted file mode 100644 index 515a337..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/IntroPage.kt +++ /dev/null @@ -1,16 +0,0 @@ -package cn.super12138.todo.views.fragments.welcome.pages - -import android.view.LayoutInflater -import android.view.ViewGroup -import cn.super12138.todo.databinding.FragmentWelcomeIntroBinding -import cn.super12138.todo.views.BaseFragment - -class IntroPage : BaseFragment() { - override fun getViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - attachToRoot: Boolean - ): FragmentWelcomeIntroBinding { - return FragmentWelcomeIntroBinding.inflate(inflater, container, attachToRoot) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ProgressPage.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ProgressPage.kt deleted file mode 100644 index 588ba0d..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ProgressPage.kt +++ /dev/null @@ -1,16 +0,0 @@ -package cn.super12138.todo.views.fragments.welcome.pages - -import android.view.LayoutInflater -import android.view.ViewGroup -import cn.super12138.todo.databinding.FragmentWelcomeProgressBinding -import cn.super12138.todo.views.BaseFragment - -class ProgressPage : BaseFragment() { - override fun getViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - attachToRoot: Boolean - ): FragmentWelcomeProgressBinding { - return FragmentWelcomeProgressBinding.inflate(inflater, container, attachToRoot) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoBtnPage.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoBtnPage.kt deleted file mode 100644 index d4ea77c..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoBtnPage.kt +++ /dev/null @@ -1,32 +0,0 @@ -package cn.super12138.todo.views.fragments.welcome.pages - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import cn.super12138.todo.databinding.FragmentWelcomeTodoBtnBinding -import cn.super12138.todo.utils.VibrationUtils -import cn.super12138.todo.views.BaseFragment - -class ToDoBtnPage : BaseFragment() { - override fun getViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - attachToRoot: Boolean - ): FragmentWelcomeTodoBtnBinding { - return FragmentWelcomeTodoBtnBinding.inflate(inflater, container, attachToRoot) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - - binding.addItem.setOnClickListener { - VibrationUtils.performHapticFeedback(it) - } - - binding.addItem.setOnLongClickListener { - true - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoItemPage.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoItemPage.kt deleted file mode 100644 index baa168a..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoItemPage.kt +++ /dev/null @@ -1,29 +0,0 @@ -package cn.super12138.todo.views.fragments.welcome.pages - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import cn.super12138.todo.databinding.FragmentWelcomeTodoItemBinding -import cn.super12138.todo.utils.VibrationUtils -import cn.super12138.todo.views.BaseFragment - -class ToDoItemPage : BaseFragment() { - override fun getViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - attachToRoot: Boolean - ): FragmentWelcomeTodoItemBinding { - return FragmentWelcomeTodoItemBinding.inflate(inflater, container, attachToRoot) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.checkItemBtn.setOnClickListener { - VibrationUtils.performHapticFeedback(it) - } - binding.todoItem.setOnLongClickListener { - true - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/AllTasksViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/AllTasksViewModel.kt deleted file mode 100644 index eeac4f6..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/AllTasksViewModel.kt +++ /dev/null @@ -1,29 +0,0 @@ -package cn.super12138.todo.views.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import cn.super12138.todo.logic.Repository -import cn.super12138.todo.logic.model.ToDo -import kotlinx.coroutines.launch - -class AllTasksViewModel : ViewModel() { - val todoListAll = ArrayList() - val refreshData = MutableLiveData(0) - - init { - loadToDos() - } - - private fun loadToDos() { - viewModelScope.launch { - val todos = Repository.getAll() - for (todo in todos) { - todoListAll.add(ToDo(todo.uuid, todo.state, todo.content, todo.subject)) - } - if (todoListAll.size > 0) { - refreshData.value = 1 - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ProgressViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ProgressViewModel.kt deleted file mode 100644 index ab45d6a..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ProgressViewModel.kt +++ /dev/null @@ -1,29 +0,0 @@ -package cn.super12138.todo.views.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import cn.super12138.todo.logic.Repository -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -class ProgressViewModel : ViewModel() { - val totalCount: MutableLiveData = MutableLiveData() - val completeCount: MutableLiveData = MutableLiveData() - val remainCount: MutableLiveData = MutableLiveData() - val progress: MutableLiveData = MutableLiveData() - - fun updateProgress() { - viewModelScope.launch { - delay(30) - val total = Repository.getAll().size - val complete = Repository.getAllComplete().size - - val calcProgress = (complete.toDouble() / total.toDouble()) * 100 - progress.postValue(calcProgress.toInt()) - completeCount.postValue(complete) - totalCount.postValue(total) - remainCount.postValue(total - complete) - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ToDoViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ToDoViewModel.kt deleted file mode 100644 index 6111382..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ToDoViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -package cn.super12138.todo.views.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import cn.super12138.todo.logic.Repository -import cn.super12138.todo.logic.dao.ToDoRoom -import cn.super12138.todo.logic.model.ToDo -import kotlinx.coroutines.launch - -class ToDoViewModel : ViewModel() { - val addData = MutableLiveData(0) - val removeData = MutableLiveData(0) - val refreshData = MutableLiveData(0) - - val todoList = ArrayList() - - init { - loadTasks() - } - - private fun loadTasks() { - viewModelScope.launch { - val todos = Repository.getAllIncomplete() - for (todo in todos) { - todoList.add(ToDo(todo.uuid, todo.state, todo.content, todo.subject)) - } - if (todoList.size > 0) { - refreshData.value = 1 - } - } - } - - fun deleteTask(position: Int, uuid: String) { - todoList.removeAt(position) - viewModelScope.launch { - Repository.deleteByUUID(uuid) - } - } - - fun insertTask(todo: ToDoRoom) { - viewModelScope.launch { - Repository.insert(todo) - } - } - - fun updateTaskState(uuid: String) { - viewModelScope.launch { - Repository.updateStateByUUID(uuid) - } - } - - fun updateTask(position: Int, todo: ToDoRoom) { - todoList.removeAt(position) - todoList.add( - position, - ToDo( - todo.uuid, - todo.state, - todo.content, - todo.subject - ) - ) - viewModelScope.launch { - Repository.update(todo) - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/WelcomeViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/WelcomeViewModel.kt deleted file mode 100644 index e8d1f7a..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/WelcomeViewModel.kt +++ /dev/null @@ -1,22 +0,0 @@ -package cn.super12138.todo.views.viewmodels - -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow - -class WelcomeViewModel : ViewModel() { - private val _currentPage = MutableStateFlow(0) - val currentPage = _currentPage.asStateFlow() - - fun setCurrentPage(page: Int) { - _currentPage.value = page - } - - fun increasePage() { - _currentPage.value += 1 - } - - fun decreasePage() { - _currentPage.value -= 1 - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionHelper.kt b/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionHelper.kt deleted file mode 100644 index 3226c09..0000000 --- a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionHelper.kt +++ /dev/null @@ -1,129 +0,0 @@ -package de.raphaelebner.roomdatabasebackup.core - -import android.annotation.SuppressLint -import android.content.SharedPreferences -import java.io.* -import java.security.NoSuchAlgorithmException -import java.security.spec.InvalidKeySpecException -import java.security.spec.KeySpec -import javax.crypto.SecretKey -import javax.crypto.SecretKeyFactory -import javax.crypto.spec.PBEKeySpec -import javax.crypto.spec.SecretKeySpec - -/** - * MIT License - * - * Copyright (c) 2024 Raphael Ebner - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - * associated documentation files (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, publish, distribute, - * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT - * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -class AESEncryptionHelper { - - companion object { - private const val BACKUP_SECRET_KEY = "backupsecretkey" - private const val TAG = "debug_AESEncryptionHelper" - } - - /** - * This method will convert a file to ByteArray - * @param file : the Path where the file is located - * @return ByteArray of the file - */ - @Throws(Exception::class) - fun readFile(file: File): ByteArray { - val fileContents = file.readBytes() - val inputBuffer = BufferedInputStream(FileInputStream(file)) - inputBuffer.read(fileContents) - inputBuffer.close() - return fileContents - } - - /** - * This method will convert a ByteArray to a file, and saves it to the path - * @param fileData : the ByteArray - * @param file : the path where the ByteArray should be saved - */ - @Throws(Exception::class) - fun saveFile(fileData: ByteArray, file: File) { - val bos = BufferedOutputStream(FileOutputStream(file, false)) - bos.write(fileData) - bos.flush() - bos.close() - } - - /** - * This method will convert a random password, saved in sharedPreferences to a SecretKey - * @param sharedPref : the sharedPref, to fetch / save the key - * @param iv : the encryption nonce - * @return SecretKey - */ - @SuppressLint("ApplySharedPref") - fun getSecretKey(sharedPref: SharedPreferences, iv: ByteArray): SecretKey { - - // get key: String from sharedpref - var password = sharedPref.getString(BACKUP_SECRET_KEY, null) - - // If no key is stored in shared pref, create one and save it - if (password == null) { - // generate random string - val stringLength = 15 - val charset = ('a'..'z') + ('A'..'Z') + ('1'..'9') - password = (1..stringLength).map { charset.random() }.joinToString("") - - val secretKey = generateSecretKey(password, iv) - // the key can be saved plain, because i am using EncryptedSharedPreferences - val editor = sharedPref.edit() - editor.putString(BACKUP_SECRET_KEY, password) - // I use .commit because when using .apply the needed app restart is faster then apply - // and the preferences wont be saved - editor.commit() - - return secretKey - } - - // generate secretKey, and return it - return generateSecretKey(password, iv) - } - - /** - * This method will convert a custom password to a SecretKey - * @param encryptPassword : the custom user password as String - * @param iv : the encryption nonce - * @return SecretKey - */ - fun getSecretKeyWithCustomPw(encryptPassword: String, iv: ByteArray): SecretKey { - // generate secretKey, and return it - return generateSecretKey(encryptPassword, iv) - } - - /** - * Function to generate a 128 bit key from the given password and iv - * @param password - * @param iv - * @return Secret key - * @throws NoSuchAlgorithmException - * @throws InvalidKeySpecException - */ - @Throws(NoSuchAlgorithmException::class, InvalidKeySpecException::class) - private fun generateSecretKey(password: String, iv: ByteArray?): SecretKey { - // convert random string to secretKey - val spec: KeySpec = PBEKeySpec(password.toCharArray(), iv, 65536, 128) // AES-128 - val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") - val key = secretKeyFactory.generateSecret(spec).encoded - return SecretKeySpec(key, "AES") - } -} diff --git a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionManager.kt b/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionManager.kt deleted file mode 100644 index 7611362..0000000 --- a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionManager.kt +++ /dev/null @@ -1,114 +0,0 @@ -package de.raphaelebner.roomdatabasebackup.core - -import android.content.SharedPreferences -import java.nio.ByteBuffer -import java.security.InvalidAlgorithmParameterException -import java.security.InvalidKeyException -import java.security.NoSuchAlgorithmException -import java.security.SecureRandom -import java.security.spec.InvalidKeySpecException -import javax.crypto.BadPaddingException -import javax.crypto.Cipher -import javax.crypto.IllegalBlockSizeException -import javax.crypto.NoSuchPaddingException -import javax.crypto.spec.GCMParameterSpec - -/** - * Encryption / Decryption service using the AES algorithm - * example for nullbeans.com - * https://nullbeans.com/how-to-encrypt-decrypt-files-byte-arrays-in-java-using-aes-gcm/#Generating_an_AES_key - */ -class AESEncryptionManager { - - private val aesEncryptionHelper = AESEncryptionHelper() - - /** - * This method will encrypt the given data - * @param sharedPref : the sharedPref, to fetch the key - * @param data : the data that will be encrypted - * @return Encrypted data in a byte array - */ - @Throws( - NoSuchPaddingException::class, - NoSuchAlgorithmException::class, - InvalidAlgorithmParameterException::class, - InvalidKeyException::class, - BadPaddingException::class, - IllegalBlockSizeException::class, - InvalidKeySpecException::class - ) - fun encryptData(sharedPref: SharedPreferences, encryptPassword: String?, data: ByteArray): ByteArray { - - //Prepare the nonce - val secureRandom = SecureRandom() - - //Noonce should be 12 bytes - val iv = ByteArray(12) - secureRandom.nextBytes(iv) - - //Prepare your key/password - val secretKey = if (encryptPassword != null) { aesEncryptionHelper.getSecretKeyWithCustomPw(encryptPassword, iv) - } else aesEncryptionHelper.getSecretKey(sharedPref, iv) - - val cipher = Cipher.getInstance("AES/GCM/NoPadding") - val parameterSpec = GCMParameterSpec(128, iv) - - //Encryption mode on! - cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec) - - //Encrypt the data - val encryptedData = cipher.doFinal(data) - - //Concatenate everything and return the final data - val byteBuffer = ByteBuffer.allocate(4 + iv.size + encryptedData.size) - byteBuffer.putInt(iv.size) - byteBuffer.put(iv) - byteBuffer.put(encryptedData) - return byteBuffer.array() - } - - /** - * This method will decrypt the given data - * @param sharedPref : the sharedPref, to fetch the key - * @param encryptedData : the data that will be decrypted - * @return decrypted data in a byte array - */ - @Throws( - NoSuchPaddingException::class, - NoSuchAlgorithmException::class, - InvalidAlgorithmParameterException::class, - InvalidKeyException::class, - BadPaddingException::class, - IllegalBlockSizeException::class, - InvalidKeySpecException::class - ) - fun decryptData(sharedPref: SharedPreferences, encryptPassword: String?, encryptedData: ByteArray): ByteArray { - - - //Wrap the data into a byte buffer to ease the reading process - val byteBuffer = ByteBuffer.wrap(encryptedData) - val noonceSize = byteBuffer.int - - //Make sure that the file was encrypted properly - require(!(noonceSize < 12 || noonceSize >= 16)) { "Nonce size is incorrect. Make sure that the incoming data is an AES encrypted file." } - val iv = ByteArray(noonceSize) - byteBuffer[iv] - - //Prepare your key/password - val secretKey = if (encryptPassword != null) { aesEncryptionHelper.getSecretKeyWithCustomPw(encryptPassword, iv) - } else aesEncryptionHelper.getSecretKey(sharedPref, iv) - - //get the rest of encrypted data - val cipherBytes = ByteArray(byteBuffer.remaining()) - byteBuffer[cipherBytes] - val cipher = Cipher.getInstance("AES/GCM/NoPadding") - val parameterSpec = GCMParameterSpec(128, iv) - - //Encryption mode on! - cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec) - - //Encrypt the data - return cipher.doFinal(cipherBytes) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/OnCompleteListener.kt b/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/OnCompleteListener.kt deleted file mode 100644 index 43dd44a..0000000 --- a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/OnCompleteListener.kt +++ /dev/null @@ -1,78 +0,0 @@ -package de.raphaelebner.roomdatabasebackup.core - -import de.raphaelebner.roomdatabasebackup.core.RoomBackup.Companion.BACKUP_FILE_LOCATION_CUSTOM_FILE - -/** - * MIT License - * - * Copyright (c) 2024 Raphael Ebner - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - * associated documentation files (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, publish, distribute, - * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT - * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -interface OnCompleteListener { - fun onComplete(success: Boolean, message: String, exitCode: Int) - - companion object { - - /** Other Error */ - const val EXIT_CODE_ERROR = 1 - - /** Error while choosing backup to restore. Maybe no file selected */ - const val EXIT_CODE_ERROR_BACKUP_FILE_CHOOSER = 2 - - /** Error while choosing backup file to create. Maybe no file selected */ - const val EXIT_CODE_ERROR_BACKUP_FILE_CREATOR = 3 - - /** - * [BACKUP_FILE_LOCATION_CUSTOM_FILE] is set but [RoomBackup.backupLocationCustomFile] is - * not set - */ - const val EXIT_CODE_ERROR_BACKUP_LOCATION_FILE_MISSING = 4 - - /** [RoomBackup.backupLocation] is not set */ - const val EXIT_CODE_ERROR_BACKUP_LOCATION_MISSING = 5 - - /** Restore dialog for internal/external storage was canceled by user */ - const val EXIT_CODE_ERROR_BY_USER_CANCELED = 6 - - /** Cannot decrypt provided backup file */ - const val EXIT_CODE_ERROR_DECRYPTION_ERROR = 7 - - /** Cannot encrypt database backup */ - const val EXIT_CODE_ERROR_ENCRYPTION_ERROR = 8 - - /** - * You tried to restore a encrypted backup but [RoomBackup.backupIsEncrypted] is set to - * false - */ - const val EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED = 9 - - /** No backups to restore are available in internal/external sotrage */ - const val EXIT_CODE_ERROR_RESTORE_NO_BACKUPS_AVAILABLE = 10 - - /** No room database to backup is provided */ - const val EXIT_CODE_ERROR_ROOM_DATABASE_MISSING = 11 - - /** Storage permissions not granted for custom dialog */ - const val EXIT_CODE_ERROR_STORAGE_PERMISSONS_NOT_GRANTED = 12 - - /** Cannot decrypt provided backup file because the password is incorrect */ - const val EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD = 13 - - /** No error, action successful */ - const val EXIT_CODE_SUCCESS = 0 - } -} diff --git a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/RoomBackup.kt b/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/RoomBackup.kt deleted file mode 100644 index a6b7018..0000000 --- a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/RoomBackup.kt +++ /dev/null @@ -1,853 +0,0 @@ -package de.raphaelebner.roomdatabasebackup.core - -import android.app.Activity -import android.app.AlertDialog -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.os.Build -import android.util.Log -import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.activity.result.contract.ActivityResultContracts -import androidx.activity.result.contract.ActivityResultContracts.CreateDocument -import androidx.room.RoomDatabase -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey -import kotlinx.coroutines.runBlocking -import java.io.BufferedOutputStream -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Locale -import javax.crypto.BadPaddingException - -/** - * MIT License - * - * Copyright (c) 2024 Raphael Ebner - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - * associated documentation files (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, publish, distribute, - * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT - * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -class RoomBackup(var context: Context) { - - companion object { - private const val SHARED_PREFS = "de.raphaelebner.roomdatabasebackup" - private var TAG = "debug_RoomBackup" - private lateinit var INTERNAL_BACKUP_PATH: File - private lateinit var TEMP_BACKUP_PATH: File - private lateinit var TEMP_BACKUP_FILE: File - private lateinit var EXTERNAL_BACKUP_PATH: File - private lateinit var DATABASE_FILE: File - - private var currentProcess: Int? = null - private const val PROCESS_BACKUP = 1 - private const val PROCESS_RESTORE = 2 - private var backupFilename: String? = null - - /** Code for internal backup location, used for [backupLocation] */ - const val BACKUP_FILE_LOCATION_INTERNAL = 1 - - /** Code for external backup location, used for [backupLocation] */ - const val BACKUP_FILE_LOCATION_EXTERNAL = 2 - - /** Code for custom backup location dialog, used for [backupLocation] */ - const val BACKUP_FILE_LOCATION_CUSTOM_DIALOG = 3 - - /** Code for custom backup file location, used for [backupLocation] */ - const val BACKUP_FILE_LOCATION_CUSTOM_FILE = 4 - } - - private lateinit var sharedPreferences: SharedPreferences - private lateinit var dbName: String - - private var roomDatabase: RoomDatabase? = null - private var enableLogDebug: Boolean = false - private var restartIntent: Intent? = null - private var onCompleteListener: OnCompleteListener? = null - private var customRestoreDialogTitle: String = "Choose file to restore" - private var customBackupFileName: String? = null - private var backupIsEncrypted: Boolean = false - private var maxFileCount: Int? = null - private var encryptPassword: String? = null - private var backupLocation: Int = BACKUP_FILE_LOCATION_INTERNAL - private var backupLocationCustomFile: File? = null - - /** - * Set RoomDatabase instance - * - * @param roomDatabase RoomDatabase - */ - fun database(roomDatabase: RoomDatabase): RoomBackup { - this.roomDatabase = roomDatabase - return this - } - - /** - * Set LogDebug enabled / disabled - * - * @param enableLogDebug Boolean - */ - fun enableLogDebug(enableLogDebug: Boolean): RoomBackup { - this.enableLogDebug = enableLogDebug - return this - } - - /** - * Set Intent in which to boot after App restart - * - * @param restartIntent Intent - */ - fun restartApp(restartIntent: Intent): RoomBackup { - this.restartIntent = restartIntent - restartApp() - return this - } - - /** - * Set onCompleteListener, to run code when tasks completed - * - * @param onCompleteListener OnCompleteListener - */ - fun onCompleteListener(onCompleteListener: OnCompleteListener): RoomBackup { - this.onCompleteListener = onCompleteListener - return this - } - - /** - * Set onCompleteListener, to run code when tasks completed - * - * @param listener (success: Boolean, message: String) -> Unit - */ - fun onCompleteListener( - listener: (success: Boolean, message: String, exitCode: Int) -> Unit - ): RoomBackup { - this.onCompleteListener = - object : OnCompleteListener { - override fun onComplete(success: Boolean, message: String, exitCode: Int) { - listener(success, message, exitCode) - } - } - return this - } - - /** - * Set custom log tag, for detailed debugging - * - * @param customLogTag String - */ - fun customLogTag(customLogTag: String): RoomBackup { - TAG = customLogTag - return this - } - - /** - * Set custom Restore Dialog Title, default = "Choose file to restore" - * - * @param customRestoreDialogTitle String - */ - fun customRestoreDialogTitle(customRestoreDialogTitle: String): RoomBackup { - this.customRestoreDialogTitle = customRestoreDialogTitle - return this - } - - /** - * Set custom Backup File Name, default = "$dbName-$currentTime.sqlite3" - * - * @param customBackupFileName String - */ - fun customBackupFileName(customBackupFileName: String): RoomBackup { - this.customBackupFileName = customBackupFileName - return this - } - - /** - * Set you backup location. Available values see: [BACKUP_FILE_LOCATION_INTERNAL], - * [BACKUP_FILE_LOCATION_EXTERNAL], [BACKUP_FILE_LOCATION_CUSTOM_DIALOG] or - * [BACKUP_FILE_LOCATION_CUSTOM_FILE] - * - * @param backupLocation Int, default = [BACKUP_FILE_LOCATION_INTERNAL] - */ - fun backupLocation(backupLocation: Int): RoomBackup { - this.backupLocation = backupLocation - return this - } - - /** - * Set a custom file where to save or restore a backup. can be used for backup and restore - * - * Only available if [backupLocation] is set to [BACKUP_FILE_LOCATION_CUSTOM_FILE] - * - * @param backupLocationCustomFile File - */ - fun backupLocationCustomFile(backupLocationCustomFile: File): RoomBackup { - this.backupLocationCustomFile = backupLocationCustomFile - return this - } - - /** - * Set file encryption to true / false can be used for backup and restore - * - * @param backupIsEncrypted Boolean, default = false - */ - fun backupIsEncrypted(backupIsEncrypted: Boolean): RoomBackup { - this.backupIsEncrypted = backupIsEncrypted - return this - } - - /** - * Set max backup files count if fileCount is > maxFileCount the oldest backup file will be - * deleted is for both internal and external storage - * - * @param maxFileCount Int, default = null - */ - fun maxFileCount(maxFileCount: Int): RoomBackup { - this.maxFileCount = maxFileCount - return this - } - - /** - * Set custom backup encryption password - * - * @param encryptPassword String - */ - fun customEncryptPassword(encryptPassword: String): RoomBackup { - this.encryptPassword = encryptPassword - return this - } - - /** Init vars, and return true if no error occurred */ - private fun initRoomBackup(): Boolean { - if (roomDatabase == null) { - if (enableLogDebug) Log.d(TAG, "roomDatabase is missing") - onCompleteListener?.onComplete( - false, - "roomDatabase is missing", - OnCompleteListener.EXIT_CODE_ERROR_ROOM_DATABASE_MISSING - ) - // throw IllegalArgumentException("roomDatabase is not initialized") - return false - } - - // Create or retrieve the Master Key for encryption/decryption - val masterKeyAlias = - MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() - - if (backupLocation !in - listOf( - BACKUP_FILE_LOCATION_INTERNAL, - BACKUP_FILE_LOCATION_EXTERNAL, - BACKUP_FILE_LOCATION_CUSTOM_DIALOG, - BACKUP_FILE_LOCATION_CUSTOM_FILE - ) - ) { - if (enableLogDebug) Log.d(TAG, "backupLocation is missing") - onCompleteListener?.onComplete( - false, - "backupLocation is missing", - OnCompleteListener.EXIT_CODE_ERROR_BACKUP_LOCATION_MISSING - ) - return false - } - - if (backupLocation == BACKUP_FILE_LOCATION_CUSTOM_FILE && backupLocationCustomFile == null - ) { - if (enableLogDebug) - Log.d( - TAG, - "backupLocation is set to custom backup file, but no file is defined" - ) - onCompleteListener?.onComplete( - false, - "backupLocation is set to custom backup file, but no file is defined", - OnCompleteListener.EXIT_CODE_ERROR_BACKUP_LOCATION_FILE_MISSING - ) - return false - } - - // Initialize/open an instance of EncryptedSharedPreferences - // Encryption key is stored in plain text in an EncryptedSharedPreferences --> it is saved - // encrypted - sharedPreferences = - EncryptedSharedPreferences.create( - context, - SHARED_PREFS, - masterKeyAlias, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) - - dbName = roomDatabase!!.openHelper.databaseName!! - INTERNAL_BACKUP_PATH = File("${context.filesDir}/databasebackup/") - TEMP_BACKUP_PATH = File("${context.filesDir}/databasebackup-temp/") - TEMP_BACKUP_FILE = File("$TEMP_BACKUP_PATH/tempbackup.sqlite3") - EXTERNAL_BACKUP_PATH = File(context.getExternalFilesDir("backup")!!.toURI()) - DATABASE_FILE = File(context.getDatabasePath(dbName).toURI()) - - // Create internal and temp backup directory if does not exist - try { - INTERNAL_BACKUP_PATH.mkdirs() - TEMP_BACKUP_PATH.mkdirs() - } catch (_: FileAlreadyExistsException) { - } catch (_: IOException) { - } - - if (enableLogDebug) { - Log.d(TAG, "DatabaseName: $dbName") - Log.d(TAG, "Database Location: $DATABASE_FILE") - Log.d(TAG, "INTERNAL_BACKUP_PATH: $INTERNAL_BACKUP_PATH") - Log.d(TAG, "EXTERNAL_BACKUP_PATH: $EXTERNAL_BACKUP_PATH") - if (backupLocationCustomFile != null) - Log.d(TAG, "backupLocationCustomFile: $backupLocationCustomFile") - } - return true - } - - /** restart App with custom Intent */ - private fun restartApp() { - restartIntent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(restartIntent) - if (context is Activity) { - (context as Activity).finish() - } - Runtime.getRuntime().exit(0) - } - - /** - * Start Backup process, and set onComplete Listener to success, if no error occurred, else - * onComplete Listener success is false and error message is passed - * - * if custom storage ist selected, the [openBackupfileCreator] will be launched - */ - fun backup() { - if (enableLogDebug) Log.d(TAG, "Starting Backup ...") - val success = initRoomBackup() - if (!success) return - - // Needed for storage permissions request - currentProcess = PROCESS_BACKUP - - // Create name for backup file, if no custom name is set: Database name + currentTime + - // .sqlite3 - var filename = - if (customBackupFileName == null) "$dbName-${getTime()}.sqlite3" - else customBackupFileName as String - // Add .aes extension to filename, if file is encrypted - if (backupIsEncrypted) filename += ".aes" - if (enableLogDebug) Log.d(TAG, "backupFilename: $filename") - - when (backupLocation) { - BACKUP_FILE_LOCATION_INTERNAL -> { - val backupFile = File("$INTERNAL_BACKUP_PATH/$filename") - doBackup(backupFile) - } - - BACKUP_FILE_LOCATION_EXTERNAL -> { - val backupFile = File("$EXTERNAL_BACKUP_PATH/$filename") - doBackup(backupFile) - } - - BACKUP_FILE_LOCATION_CUSTOM_DIALOG -> { - backupFilename = filename - permissionRequestLauncher.launch(arrayOf()) - return - } - - BACKUP_FILE_LOCATION_CUSTOM_FILE -> { - doBackup(backupLocationCustomFile!!) - } - - else -> return - } - } - - /** - * This method will do the backup action - * - * @param destination File - */ - private fun doBackup(destination: File) { - // Close the database - roomDatabase!!.close() - roomDatabase = null - if (backupIsEncrypted) { - val encryptedBytes = encryptBackup() ?: return - val bos = BufferedOutputStream(FileOutputStream(destination, false)) - bos.write(encryptedBytes) - bos.flush() - bos.close() - } else { - // Copy current database to save location (/files dir) - DATABASE_FILE.copyTo(destination) - } - - // If maxFileCount is set and is reached, delete oldest file - if (maxFileCount != null) { - val deleted = deleteOldBackup() - if (!deleted) return - } - - if (enableLogDebug) - Log.d(TAG, "Backup done, encrypted($backupIsEncrypted) and saved to $destination") - onCompleteListener?.onComplete(true, "success", OnCompleteListener.EXIT_CODE_SUCCESS) - } - - /** - * This method will do the backup action - * - * @param destination OutputStream - */ - private fun doBackup(destination: OutputStream) { - // Close the database - roomDatabase!!.close() - roomDatabase = null - if (backupIsEncrypted) { - val encryptedBytes = encryptBackup() ?: return - destination.write(encryptedBytes) - } else { - // Copy current database to save location (/files dir) - DATABASE_FILE.inputStream().use { input -> - destination.use { output -> - input.copyTo(output) - } - } - } - - // If maxFileCount is set and is reached, delete oldest file - if (maxFileCount != null) { - val deleted = deleteOldBackup() - if (!deleted) return - } - if (enableLogDebug) - Log.d(TAG, "Backup done, encrypted($backupIsEncrypted) and saved to $destination") - onCompleteListener?.onComplete(true, "success", OnCompleteListener.EXIT_CODE_SUCCESS) - } - - /** - * Encrypts the current Database and return it's content as ByteArray. The original Database is - * not encrypted only a current copy of this database - * - * @return encrypted backup as ByteArray - */ - private fun encryptBackup(): ByteArray? { - try { - // Copy database you want to backup to temp directory - DATABASE_FILE.copyTo(TEMP_BACKUP_FILE) - - // encrypt temp file, and save it to backup location - val encryptDecryptBackup = AESEncryptionHelper() - val fileData = encryptDecryptBackup.readFile(TEMP_BACKUP_FILE) - - val aesEncryptionManager = AESEncryptionManager() - val encryptedBytes = - aesEncryptionManager.encryptData(sharedPreferences, encryptPassword, fileData) - - // Delete temp file - TEMP_BACKUP_FILE.delete() - - return encryptedBytes - } catch (e: Exception) { - if (enableLogDebug) Log.d(TAG, "error during encryption: ${e.message}") - onCompleteListener?.onComplete( - false, - "error during encryption", - OnCompleteListener.EXIT_CODE_ERROR_ENCRYPTION_ERROR - ) - return null - } - } - - /** - * Start Restore process, and set onComplete Listener to success, if no error occurred, else - * onComplete Listener success is false and error message is passed - * - * if internal or external storage is selected, this function shows a list of all available - * backup files in a MaterialAlertDialog and calls [restoreSelectedInternalExternalFile] to - * restore selected file - * - * if custom storage ist selected, the [openBackupfileChooser] will be launched - */ - fun restore() { - if (enableLogDebug) Log.d(TAG, "Starting Restore ...") - val success = initRoomBackup() - if (!success) return - - // Needed for storage permissions request - currentProcess = PROCESS_RESTORE - - // Path of Backup Directory - val backupDirectory: File - - when (backupLocation) { - BACKUP_FILE_LOCATION_INTERNAL -> { - backupDirectory = INTERNAL_BACKUP_PATH - } - - BACKUP_FILE_LOCATION_EXTERNAL -> { - backupDirectory = File("$EXTERNAL_BACKUP_PATH/") - } - - BACKUP_FILE_LOCATION_CUSTOM_DIALOG -> { - permissionRequestLauncher.launch(arrayOf()) - return - } - - BACKUP_FILE_LOCATION_CUSTOM_FILE -> { - Log.d( - TAG, - "backupLocationCustomFile!!.exists()? : ${backupLocationCustomFile!!.exists()}" - ) - doRestore(backupLocationCustomFile!!) - return - } - - else -> return - } - - // All Files in an Array of type File - val arrayOfFiles = backupDirectory.listFiles() - - // If array is null or empty show "error" and return - if (arrayOfFiles.isNullOrEmpty()) { - if (enableLogDebug) Log.d(TAG, "No backups available to restore") - onCompleteListener?.onComplete( - false, - "No backups available", - OnCompleteListener.EXIT_CODE_ERROR_RESTORE_NO_BACKUPS_AVAILABLE - ) - Toast.makeText(context, "No backups available to restore", Toast.LENGTH_SHORT).show() - return - } - - // Sort Array: lastModified - arrayOfFiles.sortBy { it.lastModified() } - - // New empty MutableList of String - val mutableListOfFilesAsString = mutableListOf() - - // Add each filename to mutablelistOfFilesAsString - runBlocking { - for (i in arrayOfFiles.indices) { - mutableListOfFilesAsString.add(arrayOfFiles[i].name) - } - } - - // Convert MutableList to Array - val filesStringArray = mutableListOfFilesAsString.toTypedArray() - - // Show MaterialAlertDialog, with all available files, and on click Listener - - AlertDialog.Builder(context) - .setTitle(customRestoreDialogTitle) - .setItems(filesStringArray) { _, which -> - restoreSelectedInternalExternalFile(filesStringArray[which]) - } - .setOnCancelListener { - if (enableLogDebug) Log.d(TAG, "Restore dialog canceled") - onCompleteListener?.onComplete( - false, - "Restore dialog canceled", - OnCompleteListener.EXIT_CODE_ERROR_BY_USER_CANCELED - ) - } - .show() - } - - /** - * This method will do the restore action - * - * @param source File - */ - private fun doRestore(source: File) { - // Close the database - roomDatabase!!.close() - roomDatabase = null - val fileExtension = source.extension - if (backupIsEncrypted) { - source.copyTo(TEMP_BACKUP_FILE) - val decryptedBytes = decryptBackup() ?: return - val bos = BufferedOutputStream(FileOutputStream(DATABASE_FILE, false)) - bos.write(decryptedBytes) - bos.flush() - bos.close() - } else { - if (fileExtension == "aes") { - if (enableLogDebug) - Log.d( - TAG, - "Cannot restore database, it is encrypted. Maybe you forgot to add the property .fileIsEncrypted(true)" - ) - onCompleteListener?.onComplete( - false, - "cannot restore database, see Log for more details (if enabled)", - OnCompleteListener.EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED - ) - return - } - // Copy back database and replace current database - source.copyTo(DATABASE_FILE) - } - - if (enableLogDebug) - Log.d(TAG, "Restore done, decrypted($backupIsEncrypted) and restored from $source") - onCompleteListener?.onComplete(true, "success", OnCompleteListener.EXIT_CODE_SUCCESS) - } - - /** - * This method will do the restore action - * - * @param source InputStream - */ - private fun doRestore(source: InputStream) { - if (backupIsEncrypted) { - // Save inputstream to temp file - source.use { input -> - TEMP_BACKUP_FILE.outputStream().use { output -> input.copyTo(output) } - } - // Decrypt tempfile and write to database file - val decryptedBytes = decryptBackup() ?: return - - // Close the database if decryption is succesfull - roomDatabase!!.close() - roomDatabase = null - - val bos = BufferedOutputStream(FileOutputStream(DATABASE_FILE, false)) - bos.write(decryptedBytes) - bos.flush() - bos.close() - } else { - // Close the database - roomDatabase!!.close() - roomDatabase = null - - // Copy back database and replace current database - source.use { input -> - DATABASE_FILE.outputStream().use { output -> input.copyTo(output) } - } - } - - if (enableLogDebug) - Log.d(TAG, "Restore done, decrypted($backupIsEncrypted) and restored from $source") - onCompleteListener?.onComplete(true, "success", OnCompleteListener.EXIT_CODE_SUCCESS) - } - - /** - * Restores the selected file from internal or external storage - * - * @param filename String - */ - private fun restoreSelectedInternalExternalFile(filename: String) { - if (enableLogDebug) Log.d(TAG, "Restore selected file...") - - when (backupLocation) { - BACKUP_FILE_LOCATION_INTERNAL -> { - doRestore(File("$INTERNAL_BACKUP_PATH/$filename")) - } - - BACKUP_FILE_LOCATION_EXTERNAL -> { - doRestore(File("$EXTERNAL_BACKUP_PATH/$filename")) - } - - else -> return - } - } - - /** - * Decrypts the [TEMP_BACKUP_FILE] and return it's content as ByteArray. A valid encrypted - * backup file must be present on [TEMP_BACKUP_FILE] - * - * @return decrypted backup as ByteArray - */ - private fun decryptBackup(): ByteArray? { - try { - // Decrypt temp file, and save it to database location - val encryptDecryptBackup = AESEncryptionHelper() - val fileData = encryptDecryptBackup.readFile(TEMP_BACKUP_FILE) - - val aesEncryptionManager = AESEncryptionManager() - val decryptedBytes = - aesEncryptionManager.decryptData(sharedPreferences, encryptPassword, fileData) - - // Delete tem file - TEMP_BACKUP_FILE.delete() - - return decryptedBytes - } catch (e: BadPaddingException) { - if (enableLogDebug) Log.d(TAG, "error during decryption (wrong password): ${e.message}") - onCompleteListener?.onComplete( - false, - "error during decryption (wrong password) see Log for more details (if enabled)", - OnCompleteListener.EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD - ) - return null - } catch (e: Exception) { - if (enableLogDebug) Log.d(TAG, "error during decryption: ${e.message}") - onCompleteListener?.onComplete( - false, - "error during decryption see Log for more details (if enabled)", - OnCompleteListener.EXIT_CODE_ERROR_DECRYPTION_ERROR - ) - return null - } - } - - /** - * Opens the [ActivityResultContracts.RequestMultiplePermissions] and prompts the user to grant - * storage permissions - * - * If granted backup or restore process starts - */ - private val permissionRequestLauncher = - (context as ComponentActivity).registerForActivityResult( - ActivityResultContracts.RequestMultiplePermissions() - ) { permissions -> - permissions.entries.forEach { - if (!it.value) { - onCompleteListener?.onComplete( - false, - "storage permissions are required, please allow!", - OnCompleteListener.EXIT_CODE_ERROR_STORAGE_PERMISSONS_NOT_GRANTED - ) - return@registerForActivityResult - } - } - when (currentProcess) { - PROCESS_BACKUP -> { - backupFilename?.let { openBackupfileCreator.launch(it) } - } - - PROCESS_RESTORE -> { - openBackupfileChooser.launch(arrayOf("application/octet-stream")) - } - } - } - - /** - * Opens the [ActivityResultContracts.OpenDocument] and prompts the user to open a document for - * restoring a backup file - */ - private val openBackupfileChooser = - (context as ComponentActivity).registerForActivityResult( - ActivityResultContracts.OpenDocument() - ) { result -> - if (result != null) { - val inputstream = context.contentResolver.openInputStream(result)!! - doRestore(inputstream) - return@registerForActivityResult - } - onCompleteListener?.onComplete( - false, - "failure", - OnCompleteListener.EXIT_CODE_ERROR_BACKUP_FILE_CHOOSER - ) - } - - /** - * Opens the [ActivityResultContracts.CreateDocument] and prompts the user to select a path for - * creating the new backup file - */ - private val openBackupfileCreator = - (context as ComponentActivity).registerForActivityResult( - CreateDocument("application/octet-stream") - ) { result -> - if (result != null) { - val out = context.contentResolver.openOutputStream(result)!! - doBackup(out) - return@registerForActivityResult - } - onCompleteListener?.onComplete( - false, - "failure", - OnCompleteListener.EXIT_CODE_ERROR_BACKUP_FILE_CREATOR - ) - } - - /** @return current time formatted as String */ - private fun getTime(): String { - - val currentTime = Calendar.getInstance().time - - val sdf = - if (Build.VERSION.SDK_INT <= 28) { - SimpleDateFormat("yyyy-MM-dd-HH_mm_ss", Locale.getDefault()) - } else { - SimpleDateFormat("yyyy-MM-dd-HH:mm:ss", Locale.getDefault()) - } - - return sdf.format(currentTime) - } - - /** - * If maxFileCount is set, and reached, all old files will be deleted. Only if - * [BACKUP_FILE_LOCATION_INTERNAL] or [BACKUP_FILE_LOCATION_EXTERNAL] - * - * @return true if old files deleted or nothing to do - */ - private fun deleteOldBackup(): Boolean { - // Path of Backup Directory - - val backupDirectory: File = - when (backupLocation) { - BACKUP_FILE_LOCATION_INTERNAL -> { - INTERNAL_BACKUP_PATH - } - - BACKUP_FILE_LOCATION_EXTERNAL -> { - File("$EXTERNAL_BACKUP_PATH/") - } - - BACKUP_FILE_LOCATION_CUSTOM_DIALOG -> { - // In custom backup location no backups will be removed - return true - } - - else -> return true - } - - // All Files in an Array of type File - val arrayOfFiles = backupDirectory.listFiles() - - // If array is null or empty nothing to do and return - if (arrayOfFiles.isNullOrEmpty()) { - if (enableLogDebug) Log.d(TAG, "") - onCompleteListener?.onComplete( - false, - "maxFileCount: Failed to get list of backups", - OnCompleteListener.EXIT_CODE_ERROR - ) - return false - } else if (arrayOfFiles.size > maxFileCount!!) { - // Sort Array: lastModified - arrayOfFiles.sortBy { it.lastModified() } - - // Get count of files to delete - val fileCountToDelete = arrayOfFiles.size - maxFileCount!! - - for (i in 1..fileCountToDelete) { - // Delete all old files (i-1 because array starts a 0) - arrayOfFiles[i - 1].delete() - - if (enableLogDebug) - Log.d(TAG, "maxFileCount reached: ${arrayOfFiles[i - 1]} deleted") - } - } - return true - } -} diff --git a/app/src/main/play_store_512.png b/app/src/main/play_store_512.png deleted file mode 100644 index a1a9f37..0000000 Binary files a/app/src/main/play_store_512.png and /dev/null differ diff --git a/app/src/main/res/drawable/bg_item_complete.xml b/app/src/main/res/drawable/bg_item_complete.xml deleted file mode 100644 index fc21ed9..0000000 --- a/app/src/main/res/drawable/bg_item_complete.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_about.xml b/app/src/main/res/drawable/ic_about.xml deleted file mode 100644 index 1e1faf7..0000000 --- a/app/src/main/res/drawable/ic_about.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml deleted file mode 100644 index a9503fd..0000000 --- a/app/src/main/res/drawable/ic_add.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_all_tasks.xml b/app/src/main/res/drawable/ic_all_tasks.xml deleted file mode 100644 index 8c3bd45..0000000 --- a/app/src/main/res/drawable/ic_all_tasks.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml deleted file mode 100644 index cd06f30..0000000 --- a/app/src/main/res/drawable/ic_arrow_back.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_forward.xml b/app/src/main/res/drawable/ic_arrow_forward.xml deleted file mode 100644 index 9e27d98..0000000 --- a/app/src/main/res/drawable/ic_arrow_forward.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_backup.xml b/app/src/main/res/drawable/ic_backup.xml deleted file mode 100644 index 59eaff5..0000000 --- a/app/src/main/res/drawable/ic_backup.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml deleted file mode 100644 index 5623ef0..0000000 --- a/app/src/main/res/drawable/ic_check.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_dark_mode.xml b/app/src/main/res/drawable/ic_dark_mode.xml deleted file mode 100644 index dedc9af..0000000 --- a/app/src/main/res/drawable/ic_dark_mode.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_exit.xml b/app/src/main/res/drawable/ic_exit.xml deleted file mode 100644 index 2660d94..0000000 --- a/app/src/main/res/drawable/ic_exit.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_focus.xml b/app/src/main/res/drawable/ic_focus.xml deleted file mode 100644 index 39fe24b..0000000 --- a/app/src/main/res/drawable/ic_focus.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml deleted file mode 100644 index f32ae57..0000000 --- a/app/src/main/res/drawable/ic_github.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml deleted file mode 100644 index 7b158f0..0000000 --- a/app/src/main/res/drawable/ic_launcher.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_next.xml b/app/src/main/res/drawable/ic_next.xml deleted file mode 100644 index 0f71f1d..0000000 --- a/app/src/main/res/drawable/ic_next.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_person.xml b/app/src/main/res/drawable/ic_person.xml deleted file mode 100644 index ea2ea4a..0000000 --- a/app/src/main/res/drawable/ic_person.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_previous.xml b/app/src/main/res/drawable/ic_previous.xml deleted file mode 100644 index 2e0b029..0000000 --- a/app/src/main/res/drawable/ic_previous.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_restore.xml b/app/src/main/res/drawable/ic_restore.xml deleted file mode 100644 index 1698029..0000000 --- a/app/src/main/res/drawable/ic_restore.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_screenshot.xml b/app/src/main/res/drawable/ic_screenshot.xml deleted file mode 100644 index f07d596..0000000 --- a/app/src/main/res/drawable/ic_screenshot.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml deleted file mode 100644 index 09641d1..0000000 --- a/app/src/main/res/drawable/ic_settings.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_update.xml b/app/src/main/res/drawable/ic_update.xml deleted file mode 100644 index 14c24e7..0000000 --- a/app/src/main/res/drawable/ic_update.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_vibration.xml b/app/src/main/res/drawable/ic_vibration.xml deleted file mode 100644 index 90f6ccf..0000000 --- a/app/src/main/res/drawable/ic_vibration.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/splash_screen.xml b/app/src/main/res/drawable/splash_screen.xml deleted file mode 100644 index c3c4443..0000000 --- a/app/src/main/res/drawable/splash_screen.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_main.xml b/app/src/main/res/layout-land/fragment_main.xml deleted file mode 100644 index cf9c5d5..0000000 --- a/app/src/main/res/layout-land/fragment_main.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-small/fragment_main.xml b/app/src/main/res/layout-small/fragment_main.xml deleted file mode 100644 index e6b7a61..0000000 --- a/app/src/main/res/layout-small/fragment_main.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_crash.xml b/app/src/main/res/layout/activity_crash.xml deleted file mode 100644 index 43436f4..0000000 --- a/app/src/main/res/layout/activity_crash.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 546db6c..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_info.xml b/app/src/main/res/layout/bottom_sheet_info.xml deleted file mode 100644 index 044071e..0000000 --- a/app/src/main/res/layout/bottom_sheet_info.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_todo.xml b/app/src/main/res/layout/bottom_sheet_todo.xml deleted file mode 100644 index 4c89ffd..0000000 --- a/app/src/main/res/layout/bottom_sheet_todo.xml +++ /dev/null @@ -1,194 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -