Reorganize and group modules

This commit is contained in:
MM20
2022-12-13 17:37:26 +01:00
parent bac24baad2
commit 3f8880a90a
995 changed files with 501 additions and 298 deletions
+1
View File
@@ -0,0 +1 @@
/build
+56
View File
@@ -0,0 +1,56 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "de.mm20.launcher2.widgets"
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.constraintlayout)
implementation(libs.bundles.androidx.lifecycle)
implementation(libs.koin.android)
implementation(project(":data:weather"))
implementation(project(":data:calendar"))
implementation(project(":services:music"))
implementation(project(":core:ktx"))
implementation(project(":core:base"))
implementation(project(":core:preferences"))
implementation(project(":core:database"))
implementation(project(":core:crashreporter"))
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
/
</manifest>
@@ -0,0 +1,8 @@
package de.mm20.launcher2.widgets
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val widgetsModule = module {
single<WidgetRepository> { WidgetRepositoryImpl(androidContext(), get()) }
}
@@ -0,0 +1,205 @@
package de.mm20.launcher2.widgets
import android.app.Activity
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Build
import de.mm20.launcher2.database.entities.WidgetEntity
import de.mm20.launcher2.ktx.tryStartActivity
sealed class Widget {
abstract fun loadLabel(context: Context): String
abstract fun toDatabaseEntity(position: Int = -1): WidgetEntity
open val isConfigurable: Boolean = false
open fun configure(context: Activity, appWidgetHost: AppWidgetHost) {}
companion object {
fun fromDatabaseEntity(context: Context, entity: WidgetEntity): Widget? {
if (entity.type == WidgetType.INTERNAL.value) {
return when (entity.data) {
"weather" -> WeatherWidget
"music" -> MusicWidget
"calendar" -> CalendarWidget
"favorites" -> FavoritesWidget
else -> null
}
} else {
val widgetId = entity.data.toIntOrNull() ?: return null
val widgetInfo =
AppWidgetManager.getInstance(context).getAppWidgetInfo(widgetId) ?: return null
return ExternalWidget(
height = entity.height,
widgetId = widgetId,
widgetProviderInfo = widgetInfo
)
}
}
}
}
object WeatherWidget : Widget() {
override fun loadLabel(context: Context): String {
return context.getString(R.string.widget_name_weather)
}
override fun toDatabaseEntity(position: Int): WidgetEntity {
return WidgetEntity(
type = WidgetType.INTERNAL.value,
data = "weather",
height = -1,
position = position
)
}
override val isConfigurable: Boolean = true
override fun configure(context: Activity, appWidgetHost: AppWidgetHost) {
val intent = Intent()
intent.component = ComponentName(
context.getPackageName(),
"de.mm20.launcher2.ui.settings.SettingsActivity"
)
intent.putExtra(
"de.mm20.launcher2.settings.ROUTE",
"settings/widgets/weather"
)
context.tryStartActivity(intent)
}
}
object MusicWidget : Widget() {
override fun loadLabel(context: Context): String {
return context.getString(R.string.widget_name_music)
}
override fun toDatabaseEntity(position: Int): WidgetEntity {
return WidgetEntity(
type = WidgetType.INTERNAL.value,
data = "music",
height = -1,
position = position
)
}
override val isConfigurable: Boolean = true
override fun configure(context: Activity, appWidgetHost: AppWidgetHost) {
val intent = Intent()
intent.component = ComponentName(
context.getPackageName(),
"de.mm20.launcher2.ui.settings.SettingsActivity"
)
intent.putExtra(
"de.mm20.launcher2.settings.ROUTE",
"settings/widgets/music"
)
context.tryStartActivity(intent)
}
}
object CalendarWidget : Widget() {
override fun loadLabel(context: Context): String {
return context.getString(R.string.widget_name_calendar)
}
override fun toDatabaseEntity(position: Int): WidgetEntity {
return WidgetEntity(
type = WidgetType.INTERNAL.value,
data = "calendar",
height = -1,
position = position
)
}
override val isConfigurable: Boolean = true
override fun configure(context: Activity, appWidgetHost: AppWidgetHost) {
val intent = Intent()
intent.component = ComponentName(
context.getPackageName(),
"de.mm20.launcher2.ui.settings.SettingsActivity"
)
intent.putExtra(
"de.mm20.launcher2.settings.ROUTE",
"settings/widgets/calendar"
)
context.tryStartActivity(intent)
}
}
object FavoritesWidget : Widget() {
override fun loadLabel(context: Context): String {
return context.getString(R.string.widget_name_favorites)
}
override fun toDatabaseEntity(position: Int): WidgetEntity {
return WidgetEntity(
type = WidgetType.INTERNAL.value,
data = "favorites",
height = -1,
position = position
)
}
override val isConfigurable: Boolean = true
override fun configure(context: Activity, appWidgetHost: AppWidgetHost) {
val intent = Intent()
intent.component = ComponentName(
context.getPackageName(),
"de.mm20.launcher2.ui.settings.SettingsActivity"
)
intent.putExtra(
"de.mm20.launcher2.settings.ROUTE",
"settings/favorites"
)
context.tryStartActivity(intent)
}
}
class ExternalWidget(
var height: Int,
val widgetId: Int,
val widgetProviderInfo: AppWidgetProviderInfo
) : Widget() {
override fun loadLabel(context: Context): String {
return widgetProviderInfo.loadLabel(context.packageManager)
}
override fun toDatabaseEntity(position: Int): WidgetEntity {
return WidgetEntity(
type = WidgetType.THIRD_PARTY.value,
data = widgetId.toString(),
height = height,
position = position
)
}
override val isConfigurable: Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
widgetProviderInfo.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE != 0
} else {
false
}
override fun configure(context: Activity, appWidgetHost: AppWidgetHost) {
appWidgetHost.startAppWidgetConfigureActivityForResult(
context,
widgetId,
0,
0,
null
)
}
}
enum class WidgetType(val value: String) {
INTERNAL("internal"),
THIRD_PARTY("3rdparty")
}
@@ -0,0 +1,161 @@
package de.mm20.launcher2.widgets
import android.content.Context
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.WidgetEntity
import de.mm20.launcher2.ktx.jsonObjectOf
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.json.JSONArray
import org.json.JSONException
import java.io.File
interface WidgetRepository {
fun getWidgets(): Flow<List<Widget>>
fun getInternalWidgets(): List<Widget>
fun saveWidgets(widgets: List<Widget>)
fun addWidget(widget: Widget, position: Int)
fun removeWidget(widget: Widget)
fun setWidgetHeight(widget: Widget, newHeight: Int)
fun isWeatherWidgetEnabled(): Flow<Boolean>
fun isMusicWidgetEnabled(): Flow<Boolean>
fun isCalendarWidgetEnabled(): Flow<Boolean>
fun isFavoritesWidgetEnabled(): Flow<Boolean>
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
}
internal class WidgetRepositoryImpl(
private val context: Context,
private val database: AppDatabase,
) : WidgetRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
override fun getWidgets(): Flow<List<Widget>> {
return database.widgetDao()
.getWidgets()
.map { it.mapNotNull { Widget.fromDatabaseEntity(context, it) } }
}
override fun getInternalWidgets(): List<Widget> {
return listOf(WeatherWidget, MusicWidget, CalendarWidget, FavoritesWidget)
}
override fun saveWidgets(widgets: List<Widget>) {
scope.launch {
withContext(Dispatchers.IO) {
database.widgetDao()
.updateWidgets(widgets.mapIndexed { i, widget -> widget.toDatabaseEntity(i) })
}
}
}
override fun addWidget(widget: Widget, position: Int) {
scope.launch {
withContext(Dispatchers.IO) {
database.widgetDao()
.insert(widget.toDatabaseEntity(position))
}
}
}
override fun removeWidget(widget: Widget) {
scope.launch {
withContext(Dispatchers.IO) {
val ent = widget.toDatabaseEntity()
database.widgetDao().deleteWidget(
ent.type,
ent.data
)
}
}
}
override fun setWidgetHeight(widget: Widget, newHeight: Int) {
scope.launch {
withContext(Dispatchers.IO) {
val ent = widget.toDatabaseEntity()
database.widgetDao().updateHeight(
ent.type,
ent.data,
newHeight
)
}
}
}
override fun isWeatherWidgetEnabled(): Flow<Boolean> {
return database.widgetDao().exists("internal", "weather")
}
override fun isMusicWidgetEnabled(): Flow<Boolean> {
return database.widgetDao().exists("internal", "music")
}
override fun isCalendarWidgetEnabled(): Flow<Boolean> {
return database.widgetDao().exists("internal", "calendar")
}
override fun isFavoritesWidgetEnabled(): Flow<Boolean> {
return database.widgetDao().exists("internal", "favorites")
}
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) {
val dao = database.backupDao()
var page = 0
do {
val widgets = dao.exportWidgets(limit = 100, offset = page * 100)
val jsonArray = JSONArray()
for (widget in widgets) {
if (widget.type != WidgetType.INTERNAL.value) continue
jsonArray.put(
jsonObjectOf(
"data" to widget.data,
"position" to widget.position,
)
)
}
val file = File(toDir, "widgets.${page.toString().padStart(4, '0')}")
file.bufferedWriter().use {
it.write(jsonArray.toString())
}
page++
} while (widgets.size == 100)
}
override suspend fun import(fromDir: File) = withContext(Dispatchers.IO) {
val dao = database.backupDao()
dao.wipeWidgets()
val files = fromDir.listFiles { _, name -> name.startsWith("widgets.") } ?: return@withContext
for (file in files) {
val widgets = mutableListOf<WidgetEntity>()
try {
val jsonArray = JSONArray(file.inputStream().reader().readText())
for (i in 0 until jsonArray.length()) {
val json = jsonArray.getJSONObject(i)
val entity = WidgetEntity(
type = WidgetType.INTERNAL.value,
position = json.getInt("position"),
data = json.getString("data"),
height = -1,
)
widgets.add(entity)
}
dao.importWidgets(widgets)
} catch (e: JSONException) {
CrashReporter.logException(e)
}
}
}
}