diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9cf3a2f5..9a215297 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -136,6 +136,7 @@ dependencies { implementation(project(":modules-power")) implementation(project(":modules-wifi")) implementation(project(":modules-apps")) + implementation(project(":modules-clipboard")) addDI() addCoroutines() diff --git a/app/src/main/java/eu/darken/octi/main/ui/dashboard/DashboardVM.kt b/app/src/main/java/eu/darken/octi/main/ui/dashboard/DashboardVM.kt index 0a462913..caece575 100644 --- a/app/src/main/java/eu/darken/octi/main/ui/dashboard/DashboardVM.kt +++ b/app/src/main/java/eu/darken/octi/main/ui/dashboard/DashboardVM.kt @@ -31,6 +31,9 @@ import eu.darken.octi.module.core.ModuleData import eu.darken.octi.module.core.ModuleManager import eu.darken.octi.modules.apps.core.AppsInfo import eu.darken.octi.modules.apps.ui.dashboard.DeviceAppsVH +import eu.darken.octi.modules.clipboard.ClipboardHandler +import eu.darken.octi.modules.clipboard.ClipboardInfo +import eu.darken.octi.modules.clipboard.ClipboardVH import eu.darken.octi.modules.meta.core.MetaInfo import eu.darken.octi.modules.power.core.PowerInfo import eu.darken.octi.modules.power.ui.dashboard.DevicePowerVH @@ -60,6 +63,7 @@ class DashboardVM @Inject constructor( private val syncSettings: SyncSettings, private val upgradeRepo: UpgradeRepo, private val webpageTool: WebpageTool, + private val clipboardHandler: ClipboardHandler, ) : ViewModel3(dispatcherProvider = dispatcherProvider) { val dashboardEvents = SingleLiveEvent() @@ -205,6 +209,7 @@ class DashboardVM @Inject constructor( is PowerInfo -> (moduleData as ModuleData).createVHItem() is WifiInfo -> (moduleData as ModuleData).createVHItem(missingPermissions) is AppsInfo -> (moduleData as ModuleData).createVHItem() + is ClipboardInfo -> (moduleData as ModuleData).createVHItem() else -> { log(TAG, WARN) { "Unsupported module data: ${moduleData.data}" } null @@ -265,10 +270,20 @@ class DashboardVM @Inject constructor( } ) + private fun ModuleData.createVHItem() = ClipboardVH.Item( + data = this, + isOurDevice = deviceId == syncSettings.deviceId, + onClearClicked = { launch { clipboardHandler.setSharedClipboard(ClipboardInfo()) } }, + onPasteClicked = { launch { clipboardHandler.shareCurrentOSClipboard() } } + .takeIf { deviceId == syncSettings.deviceId }, + onCopyClicked = { launch { clipboardHandler.setOSClipboard(data) } } + ) + companion object { private val INFO_ORDER = listOf( PowerInfo::class, WifiInfo::class, + ClipboardInfo::class, AppsInfo::class, ) private const val DEVICE_LIMIT = 3 diff --git a/app/src/main/java/eu/darken/octi/main/ui/dashboard/items/perdevice/PerDeviceModuleAdapter.kt b/app/src/main/java/eu/darken/octi/main/ui/dashboard/items/perdevice/PerDeviceModuleAdapter.kt index 1543724f..f5265403 100644 --- a/app/src/main/java/eu/darken/octi/main/ui/dashboard/items/perdevice/PerDeviceModuleAdapter.kt +++ b/app/src/main/java/eu/darken/octi/main/ui/dashboard/items/perdevice/PerDeviceModuleAdapter.kt @@ -12,6 +12,7 @@ import eu.darken.octi.common.lists.modular.ModularAdapter import eu.darken.octi.common.lists.modular.mods.DataBinderMod import eu.darken.octi.common.lists.modular.mods.TypedVHCreatorMod import eu.darken.octi.modules.apps.ui.dashboard.DeviceAppsVH +import eu.darken.octi.modules.clipboard.ClipboardVH import eu.darken.octi.modules.power.ui.dashboard.DevicePowerVH import eu.darken.octi.modules.wifi.ui.dashboard.DeviceWifiVH import javax.inject.Inject @@ -30,6 +31,7 @@ class PerDeviceModuleAdapter @Inject constructor() : modules.add(TypedVHCreatorMod({ data[it] is DevicePowerVH.Item }) { DevicePowerVH(it) }) modules.add(TypedVHCreatorMod({ data[it] is DeviceWifiVH.Item }) { DeviceWifiVH(it) }) modules.add(TypedVHCreatorMod({ data[it] is DeviceAppsVH.Item }) { DeviceAppsVH(it) }) + modules.add(TypedVHCreatorMod({ data[it] is ClipboardVH.Item }) { ClipboardVH(it) }) } abstract class BaseVH( diff --git a/app/src/main/java/eu/darken/octi/module/core/GeneralModuleSettings.kt b/app/src/main/java/eu/darken/octi/module/core/GeneralModuleSettings.kt index a307a483..9a4a8247 100644 --- a/app/src/main/java/eu/darken/octi/module/core/GeneralModuleSettings.kt +++ b/app/src/main/java/eu/darken/octi/module/core/GeneralModuleSettings.kt @@ -9,6 +9,7 @@ import eu.darken.octi.common.datastore.PreferenceScreenData import eu.darken.octi.common.datastore.PreferenceStoreMapper import eu.darken.octi.common.debug.logging.logTag import eu.darken.octi.modules.apps.core.AppsSettings +import eu.darken.octi.modules.clipboard.ClipboardSettings import eu.darken.octi.modules.power.core.PowerSettings import eu.darken.octi.modules.wifi.core.WifiSettings import javax.inject.Inject @@ -20,6 +21,7 @@ class GeneralModuleSettings @Inject constructor( private val powerSettings: PowerSettings, private val wifiSettings: WifiSettings, private val appsSettings: AppsSettings, + private val clipboardSettings: ClipboardSettings, ) : PreferenceScreenData { private val Context.dataStore by preferencesDataStore(name = "module_settings") @@ -30,6 +32,7 @@ class GeneralModuleSettings @Inject constructor( powerSettings.isEnabled, wifiSettings.isEnabled, appsSettings.isEnabled, + clipboardSettings.isEnabled, ) companion object { diff --git a/app/src/main/java/eu/darken/octi/modules/clipboard/ClipboardVH.kt b/app/src/main/java/eu/darken/octi/modules/clipboard/ClipboardVH.kt new file mode 100644 index 00000000..1839151b --- /dev/null +++ b/app/src/main/java/eu/darken/octi/modules/clipboard/ClipboardVH.kt @@ -0,0 +1,61 @@ +package eu.darken.octi.modules.clipboard + +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.view.isGone +import eu.darken.octi.R +import eu.darken.octi.databinding.DashboardDeviceClipboardItemBinding +import eu.darken.octi.main.ui.dashboard.items.perdevice.PerDeviceModuleAdapter +import eu.darken.octi.module.core.ModuleData + + +class ClipboardVH(parent: ViewGroup) : + PerDeviceModuleAdapter.BaseVH( + R.layout.dashboard_device_clipboard_item, + parent + ) { + + override val viewBinding = lazy { DashboardDeviceClipboardItemBinding.bind(itemView) } + + override val onBindData: DashboardDeviceClipboardItemBinding.( + item: Item, + payloads: List + ) -> Unit = { item, _ -> + val clip = item.data.data + + secondary.text = when (clip.type) { + ClipboardInfo.Type.EMPTY -> getString(R.string.general_empty_label) + ClipboardInfo.Type.SIMPLE_TEXT -> clip.data.utf8() + else -> clip.data.toString() + }.let { "\"$it\"" } + + pasteAction.apply { + setOnClickListener { + item.onPasteClicked?.invoke() + Toast.makeText(context, R.string.module_clipboard_copied_os_to_octi, Toast.LENGTH_SHORT).show() + } + setOnLongClickListener { + item.onClearClicked() + true + } + isGone = item.onPasteClicked == null + } + + copyAction.setOnClickListener { + it.requestFocus() + item.onCopyClicked(clip) + Toast.makeText(context, R.string.module_clipboard_copied_octi_to_os, Toast.LENGTH_SHORT).show() + } + } + + data class Item( + val data: ModuleData, + val isOurDevice: Boolean, + val onClearClicked: (() -> Unit), + val onPasteClicked: (() -> Unit)?, + val onCopyClicked: ((ClipboardInfo) -> Unit), + ) : PerDeviceModuleAdapter.Item { + override val stableId: Long = data.moduleId.hashCode().toLong() + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_content_copy_24.xml b/app/src/main/res/drawable/ic_baseline_content_copy_24.xml new file mode 100644 index 00000000..89f03149 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_content_copy_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_content_paste_24.xml b/app/src/main/res/drawable/ic_baseline_content_paste_24.xml new file mode 100644 index 00000000..262a911f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_content_paste_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_content_paste_go_24.xml b/app/src/main/res/drawable/ic_baseline_content_paste_go_24.xml new file mode 100644 index 00000000..2cfa65eb --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_content_paste_go_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/layout/dashboard_device_clipboard_item.xml b/app/src/main/res/layout/dashboard_device_clipboard_item.xml new file mode 100644 index 00000000..e59d8269 --- /dev/null +++ b/app/src/main/res/layout/dashboard_device_clipboard_item.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 57f09359..4b818ce6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,6 +13,7 @@ Create account Link to existing account N/A + Empty Dismiss Maybe later Manage @@ -194,6 +195,12 @@ Device limit reached You currently have %1$d synced devices. To view more than %2$d devices, upgrade to Octi Pro or remove devices. Display the battery status of your synced devices. + Apps module Information about installed apps. + + Clipboard module + Copy and paste text between devices. + Copied Octi to system clipboard + Copied system to Octi clipboard \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_module.xml b/app/src/main/res/xml/preferences_module.xml index f68a3fc4..494578c4 100644 --- a/app/src/main/res/xml/preferences_module.xml +++ b/app/src/main/res/xml/preferences_module.xml @@ -19,4 +19,10 @@ android:summary="@string/module_apps_desc" android:title="@string/module_apps_label" /> + + \ No newline at end of file diff --git a/module-core/src/main/java/eu/darken/octi/module/core/BaseModuleCache.kt b/module-core/src/main/java/eu/darken/octi/module/core/BaseModuleCache.kt index dfc41926..062de147 100644 --- a/module-core/src/main/java/eu/darken/octi/module/core/BaseModuleCache.kt +++ b/module-core/src/main/java/eu/darken/octi/module/core/BaseModuleCache.kt @@ -101,8 +101,13 @@ abstract class BaseModuleCache constructor( null } - uncachedData?.toModuleData().also { - log(tag, VERBOSE) { "get(id=$deviceId): $it" } + try { + uncachedData?.toModuleData().also { + log(tag, VERBOSE) { "get(id=$deviceId): $it" } + } + } catch (e: Exception) { + log(tag, ERROR) { "Failed to deserialize: ${e.asLog()}\nraw=$uncachedData" } + null } } diff --git a/modules-clipboard/.gitignore b/modules-clipboard/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/modules-clipboard/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/modules-clipboard/build.gradle.kts b/modules-clipboard/build.gradle.kts new file mode 100644 index 00000000..acf31deb --- /dev/null +++ b/modules-clipboard/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("kotlin-android") + id("kotlin-kapt") + id("kotlin-parcelize") +} + +apply(plugin = "dagger.hilt.android.plugin") + +android { + namespace = "eu.darken.octi.modules.clipboard" + compileSdk = ProjectConfig.compileSdk + + defaultConfig { + minSdk = ProjectConfig.minSdk + targetSdk = ProjectConfig.targetSdk + } + + setupModuleBuildTypes() + + setupCompileOptions() + + setupKotlinOptions() + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + tasks.withType { + useJUnitPlatform() + } + } +} + +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:${Versions.Desugar.core}") + + implementation(project(":app-common")) + testImplementation(project(":app-common-test")) + implementation(project(":module-core")) + implementation(project(":sync-core")) + + addAndroidCore() + addDI() + addCoroutines() + addSerialization() + addIO() + addTesting() +} \ No newline at end of file diff --git a/modules-clipboard/proguard-rules.pro b/modules-clipboard/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/modules-clipboard/proguard-rules.pro @@ -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. +# +# 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 \ No newline at end of file diff --git a/modules-clipboard/src/androidTest/java/eu/darken/octi/modules/clipboard/ExampleInstrumentedTest.kt b/modules-clipboard/src/androidTest/java/eu/darken/octi/modules/clipboard/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..e383366f --- /dev/null +++ b/modules-clipboard/src/androidTest/java/eu/darken/octi/modules/clipboard/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package eu.darken.octi.modules.clipboard + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("eu.darken.octi.modules.clipboard", appContext.packageName) + } +} \ No newline at end of file diff --git a/modules-clipboard/src/main/AndroidManifest.xml b/modules-clipboard/src/main/AndroidManifest.xml new file mode 100644 index 00000000..44008a43 --- /dev/null +++ b/modules-clipboard/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardCache.kt b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardCache.kt new file mode 100644 index 00000000..f9c91d9c --- /dev/null +++ b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardCache.kt @@ -0,0 +1,30 @@ +package eu.darken.octi.modules.clipboard + +import android.content.Context +import com.squareup.moshi.Moshi +import dagger.hilt.android.qualifiers.ApplicationContext +import eu.darken.octi.common.coroutine.DispatcherProvider +import eu.darken.octi.common.debug.logging.logTag +import eu.darken.octi.module.core.BaseModuleCache +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ClipboardCache @Inject constructor( + @ApplicationContext private val context: Context, + dispatcherProvider: DispatcherProvider, + appsSerializer: ClipboardSerializer, + moshi: Moshi, +) : BaseModuleCache( + moduleId = ClipboardModule.MODULE_ID, + tag = TAG, + dispatcherProvider = dispatcherProvider, + context = context, + moduleSerializer = appsSerializer, + moshi = moshi, +) { + + companion object { + val TAG = logTag("Module", "Clipboard", "Cache") + } +} \ No newline at end of file diff --git a/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardHandler.kt b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardHandler.kt new file mode 100644 index 00000000..4ba4b2a0 --- /dev/null +++ b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardHandler.kt @@ -0,0 +1,96 @@ +package eu.darken.octi.modules.clipboard + +import android.content.ClipData +import android.content.ClipDescription +import android.content.ClipboardManager +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import eu.darken.octi.common.collections.toByteString +import eu.darken.octi.common.coroutine.AppScope +import eu.darken.octi.common.datastore.value +import eu.darken.octi.common.debug.logging.Logging.Priority.WARN +import eu.darken.octi.common.debug.logging.log +import eu.darken.octi.common.debug.logging.logTag +import eu.darken.octi.common.flow.DynamicStateFlow +import eu.darken.octi.common.flow.replayingShare +import eu.darken.octi.common.flow.setupCommonEventHandlers +import eu.darken.octi.module.core.ModuleInfoSource +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import okio.ByteString.Companion.decodeBase64 +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ClipboardHandler @Inject constructor( + @AppScope private val appScope: CoroutineScope, + @ApplicationContext private val context: Context, + private val settings: ClipboardSettings, + private val serializer: ClipboardSerializer, +) : ModuleInfoSource { + + private val currentClipboard = DynamicStateFlow(TAG, appScope) { + try { + settings.lastClipboard.value()?.decodeBase64()?.let { serializer.deserialize(it) } ?: ClipboardInfo() + } catch (e: Exception) { + ClipboardInfo() + } + } + + private val cm by lazy { context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager } + + override val info: Flow = currentClipboard.flow + .setupCommonEventHandlers(TAG) { "info" } + .replayingShare(appScope) + + suspend fun setSharedClipboard(info: ClipboardInfo) { + log(TAG) { "setSharedClipboard(info=$info)" } + currentClipboard.updateBlocking { info } + settings.lastClipboard.value(serializer.serialize(info).base64()) + } + + suspend fun setOSClipboard(info: ClipboardInfo) { + log(TAG) { "setOSClipboard(info=$info)" } + if (info.type != ClipboardInfo.Type.EMPTY) { + info.toClipData()?.let { cm.setPrimaryClip(it) } + } else { + cm.clearPrimaryClip() + } + } + + suspend fun shareCurrentOSClipboard() { + log(TAG) { "shareCurrentOSClipboard()" } + val info = if (cm.hasPrimaryClip()) { + cm.primaryClip?.toClipboardInfo() + } else { + null + } + + log(TAG) { "shareCurrentOSClipboard(): $info" } + setSharedClipboard(info ?: ClipboardInfo()) + } + + private fun ClipboardInfo.toClipData(): ClipData? = when (type) { + ClipboardInfo.Type.EMPTY -> null + ClipboardInfo.Type.SIMPLE_TEXT -> ClipData( + "Simple text", + arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN), + ClipData.Item(data.utf8()) + ) + } + + private fun ClipData.toClipboardInfo(): ClipboardInfo? = when { + itemCount > 0 -> ClipboardInfo( + type = ClipboardInfo.Type.SIMPLE_TEXT, + data = getItemAt(0).text.toString().toByteString(), + ) + else -> { + log(TAG, WARN) { "Failed to convert: $this" } + null + } + } + + companion object { + internal val TAG = logTag("Module", "Clipboard", "InfoSource") + } +} \ No newline at end of file diff --git a/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardInfo.kt b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardInfo.kt new file mode 100644 index 00000000..36f645ab --- /dev/null +++ b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardInfo.kt @@ -0,0 +1,24 @@ +package eu.darken.octi.modules.clipboard + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import okio.ByteString + +@JsonClass(generateAdapter = true) +data class ClipboardInfo( + @Json(name = "type") val type: Type = Type.EMPTY, + @Json(name = "data") val data: ByteString = ByteString.EMPTY, +) { + + init { + if (data.size > 32 * 1024) throw java.lang.IllegalArgumentException("Size limit exceeded (>32KB)") + } + + @JsonClass(generateAdapter = false) + enum class Type { + @Json(name = "EMPTY") EMPTY, + @Json(name = "SIMPLE_TEXT") SIMPLE_TEXT, + ; + } + +} \ No newline at end of file diff --git a/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardModule.kt b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardModule.kt new file mode 100644 index 00000000..815ec76e --- /dev/null +++ b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardModule.kt @@ -0,0 +1,31 @@ +package eu.darken.octi.modules.clipboard + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import eu.darken.octi.module.core.ModuleId +import eu.darken.octi.module.core.ModuleRepo +import eu.darken.octi.module.core.ModuleSync + +@InstallIn(SingletonComponent::class) +@Module +class ClipboardModule { + + @Provides + @IntoSet + fun sync(sync: ClipboardSync): ModuleSync = sync + + @Provides + @IntoSet + fun repo(repo: ClipboardRepo): ModuleRepo = repo + + @Provides + @IntoSet + fun moduleId(): ModuleId = MODULE_ID + + companion object { + val MODULE_ID = ModuleId("eu.darken.octi.module.core.clipboard") + } +} \ No newline at end of file diff --git a/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardRepo.kt b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardRepo.kt new file mode 100644 index 00000000..eba565b0 --- /dev/null +++ b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardRepo.kt @@ -0,0 +1,33 @@ +package eu.darken.octi.modules.clipboard + +import eu.darken.octi.common.coroutine.AppScope +import eu.darken.octi.common.coroutine.DispatcherProvider +import eu.darken.octi.common.debug.logging.logTag +import eu.darken.octi.module.core.BaseModuleRepo +import kotlinx.coroutines.CoroutineScope +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ClipboardRepo @Inject constructor( + @AppScope private val scope: CoroutineScope, + dispatcherProvider: DispatcherProvider, + appsSettings: ClipboardSettings, + appsInfoSource: ClipboardHandler, + appsSync: ClipboardSync, + appsCache: ClipboardCache, +) : BaseModuleRepo( + tag = TAG, + moduleId = ClipboardModule.MODULE_ID, + scope = scope, + dispatcherProvider = dispatcherProvider, + moduleSettings = appsSettings, + infoSource = appsInfoSource, + moduleSync = appsSync, + moduleCache = appsCache, +) { + + companion object { + val TAG = logTag("Module", "Clipboard", "Repo") + } +} \ No newline at end of file diff --git a/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardSerializer.kt b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardSerializer.kt new file mode 100644 index 00000000..3cc108b1 --- /dev/null +++ b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardSerializer.kt @@ -0,0 +1,27 @@ +package eu.darken.octi.modules.clipboard + +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import dagger.Reusable +import eu.darken.octi.common.debug.logging.logTag +import eu.darken.octi.common.serialization.fromJson +import eu.darken.octi.common.serialization.toByteString +import eu.darken.octi.module.core.ModuleSerializer +import okio.ByteString +import javax.inject.Inject + +@Reusable +class ClipboardSerializer @Inject constructor( + private val moshi: Moshi, +) : ModuleSerializer { + + private val adapter by lazy { moshi.adapter() } + + override fun serialize(item: ClipboardInfo): ByteString = adapter.toByteString(item) + + override fun deserialize(raw: ByteString): ClipboardInfo = adapter.fromJson(raw)!! + + companion object { + val TAG = logTag("Module", "Clipboard", "Serializer") + } +} \ No newline at end of file diff --git a/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardSettings.kt b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardSettings.kt new file mode 100644 index 00000000..a1b1df55 --- /dev/null +++ b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardSettings.kt @@ -0,0 +1,39 @@ +package eu.darken.octi.modules.clipboard + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import com.squareup.moshi.Moshi +import dagger.hilt.android.qualifiers.ApplicationContext +import eu.darken.octi.common.datastore.PreferenceScreenData +import eu.darken.octi.common.datastore.PreferenceStoreMapper +import eu.darken.octi.common.datastore.createValue +import eu.darken.octi.common.debug.logging.logTag +import eu.darken.octi.module.core.ModuleSettings +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ClipboardSettings @Inject constructor( + @ApplicationContext private val context: Context, + private val moshi: Moshi, +) : PreferenceScreenData, ModuleSettings { + + private val Context.dataStore by preferencesDataStore(name = "module_clipboard_settings") + + override val dataStore: DataStore + get() = context.dataStore + + override val isEnabled = dataStore.createValue("module.clipboard.enabled", true) + + override val mapper: PreferenceStoreMapper = PreferenceStoreMapper( + isEnabled + ) + + val lastClipboard = dataStore.createValue("module.clipboard.last", null as String?) + + companion object { + internal val TAG = logTag("Module", "Clipboard", "Settings") + } +} \ No newline at end of file diff --git a/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardSync.kt b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardSync.kt new file mode 100644 index 00000000..e6c67bb0 --- /dev/null +++ b/modules-clipboard/src/main/java/eu/darken/octi/modules/clipboard/ClipboardSync.kt @@ -0,0 +1,29 @@ +package eu.darken.octi.modules.clipboard + +import eu.darken.octi.common.coroutine.DispatcherProvider +import eu.darken.octi.common.debug.logging.logTag +import eu.darken.octi.module.core.BaseModuleSync +import eu.darken.octi.sync.core.SyncManager +import eu.darken.octi.sync.core.SyncSettings +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ClipboardSync @Inject constructor( + dispatcherProvider: DispatcherProvider, + syncSettings: SyncSettings, + syncManager: SyncManager, + appsSerializer: ClipboardSerializer, +) : BaseModuleSync( + tag = TAG, + moduleId = ClipboardModule.MODULE_ID, + dispatcherProvider = dispatcherProvider, + syncSettings = syncSettings, + syncManager = syncManager, + moduleSerializer = appsSerializer, +) { + + companion object { + val TAG = logTag("Module", "Clipboard", "Sync") + } +} \ No newline at end of file diff --git a/modules-clipboard/src/test/java/eu/darken/octi/modules/clipboard/ExampleUnitTest.kt b/modules-clipboard/src/test/java/eu/darken/octi/modules/clipboard/ExampleUnitTest.kt new file mode 100644 index 00000000..818fb9c1 --- /dev/null +++ b/modules-clipboard/src/test/java/eu/darken/octi/modules/clipboard/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package eu.darken.octi.modules.clipboard + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 1d25f23e..87110458 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,3 +10,4 @@ include ':modules-power' include ':modules-meta' include ':modules-wifi' include ':modules-apps' +include ':modules-clipboard'