diff --git a/core/common/src/main/java/com/shifthackz/aisdv1/core/common/extensions/AppExtensions.kt b/core/common/src/main/java/com/shifthackz/aisdv1/core/common/extensions/AppExtensions.kt index f32dc5aa..db9aaad0 100644 --- a/core/common/src/main/java/com/shifthackz/aisdv1/core/common/extensions/AppExtensions.kt +++ b/core/common/src/main/java/com/shifthackz/aisdv1/core/common/extensions/AppExtensions.kt @@ -7,7 +7,7 @@ import android.content.Intent import android.net.Uri import android.provider.Settings import android.widget.Toast - +import androidx.annotation.StringRes fun Context.isAppInForeground(): Boolean { val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager @@ -18,6 +18,10 @@ fun Context.isAppInForeground(): Boolean { } } +fun Context.showToast(@StringRes resId: Int) { + resources.getString(resId).let(::showToast) +} + fun Context.showToast(text: String) { Toast.makeText(this, text, Toast.LENGTH_LONG).show() } diff --git a/core/common/src/main/java/com/shifthackz/aisdv1/core/common/schedulers/SchedulersProvider.kt b/core/common/src/main/java/com/shifthackz/aisdv1/core/common/schedulers/SchedulersProvider.kt index f904069f..2e7bea78 100755 --- a/core/common/src/main/java/com/shifthackz/aisdv1/core/common/schedulers/SchedulersProvider.kt +++ b/core/common/src/main/java/com/shifthackz/aisdv1/core/common/schedulers/SchedulersProvider.kt @@ -1,6 +1,7 @@ package com.shifthackz.aisdv1.core.common.schedulers import io.reactivex.rxjava3.core.Scheduler +import io.reactivex.rxjava3.schedulers.Schedulers import java.util.concurrent.Executor interface SchedulersProvider { @@ -8,4 +9,11 @@ interface SchedulersProvider { val ui: Scheduler val computation: Scheduler val singleThread: Executor + + fun byToken(token: SchedulersToken): Scheduler = when (token) { + SchedulersToken.MAIN_THREAD -> ui + SchedulersToken.IO_THREAD -> io + SchedulersToken.COMPUTATION -> computation + SchedulersToken.SINGLE_THREAD -> Schedulers.from(singleThread) + } } diff --git a/core/common/src/main/java/com/shifthackz/aisdv1/core/common/schedulers/SchedulersToken.kt b/core/common/src/main/java/com/shifthackz/aisdv1/core/common/schedulers/SchedulersToken.kt new file mode 100644 index 00000000..c674d7c5 --- /dev/null +++ b/core/common/src/main/java/com/shifthackz/aisdv1/core/common/schedulers/SchedulersToken.kt @@ -0,0 +1,8 @@ +package com.shifthackz.aisdv1.core.common.schedulers + +enum class SchedulersToken(val type: String) { + MAIN_THREAD("Main thread"), + IO_THREAD("IO thread"), + COMPUTATION("Computation"), + SINGLE_THREAD("Single thread"), +} diff --git a/core/localization/src/main/res/values-ru/strings.xml b/core/localization/src/main/res/values-ru/strings.xml index ecf330b6..726aaa23 100644 --- a/core/localization/src/main/res/values-ru/strings.xml +++ b/core/localization/src/main/res/values-ru/strings.xml @@ -16,6 +16,8 @@ Применить Закрыть Далее + ✅ Успешно + ❌ Ошибка Два Три @@ -153,7 +155,8 @@ Конфигурация Выберите SD ML модель Очистить кэш приложения - Дебаг Меню + Режим разработчика + Логи ЛоРА Инверсия текста Инверсия @@ -298,8 +301,19 @@ Чтобы использовать локальную пользовательскую модель, поместите ее в локальную папку в памяти телефона: Download/SDAi/model Окончательная структура папок должна быть такой: + Отладка QA тест-кейсы + Work Manager API + Режим разработчика разблокирован! + Посмотреть логи + Очистить все логи + Перезапустить последнюю txt2img задачу + Перезапустить последнюю img2img задачу + Отменить все задачи + Разрешить прерывание генерации + Поток Внести битый Base64 в БД + Лог файл пуст. Здесь пусто… Добавьте что-нибуть на %1$s сервере в: \n\n%2$s @@ -330,4 +344,9 @@ Отсутствует разрешение. Разрешите права на %1$s в настройках. + + UI поток + I/O Поток + Вычислительный + Однопоточность diff --git a/core/localization/src/main/res/values-tr/strings.xml b/core/localization/src/main/res/values-tr/strings.xml index b65fb353..1c4a97ae 100644 --- a/core/localization/src/main/res/values-tr/strings.xml +++ b/core/localization/src/main/res/values-tr/strings.xml @@ -16,6 +16,8 @@ Uygula Kapalı Sonraki + ✅ Başarılı + ❌ Başarısızlık İki Üç @@ -153,7 +155,8 @@ Sunucu Kurulumu SD Modeli seçin Uygulama önbelleğini temizle - Hata Ayıklama Menüsü + Geliştirici modu + Günlükler LoRA Metin İnversiyon İnversiyon @@ -298,8 +301,19 @@ Yerel özel modeli kullanmak için telefonunuzun depolama alanındaki yerel klasöre yerleştirin: Download/SDAi/model Son klasör yapısı şu şekilde olmalıdır:: + Hata ayıklama + Work Manager API QA işlemleri + Geliştirici modu kilidi açıldı! + Günlükleri görüntüle + Tüm günlükleri temizle + Son txt2img görevini yeniden başlat + Son img2img görevini yeniden başlat + Tüm çalışanları iptal et + Oluşturmayı kesmeye izin ver + İşlem zamanlayıcısı Kötü Base64\'ü DB\'ye yerleştirin + Herhangi bir günlük bulunamadı. Burada hiçbir şey… %1$s sunucusuna biraz içerik ekleyin: \n\n%2$s @@ -330,4 +344,9 @@ Eksik izinler. Lütfen uygulama ayarlarında %1$s iznini verin. + + Ana İş Parçacığı + G/Ç İş Parçacığı + Hesaplama + Tek İş Parçacığı diff --git a/core/localization/src/main/res/values-uk/strings.xml b/core/localization/src/main/res/values-uk/strings.xml index 8435199d..8b901cd4 100644 --- a/core/localization/src/main/res/values-uk/strings.xml +++ b/core/localization/src/main/res/values-uk/strings.xml @@ -16,6 +16,8 @@ Застосувати Закрити Далі + ✅ Успіх + ❌ Помилка Два Три @@ -153,7 +155,8 @@ Конфігурація Оберіть SD ML модель Очистити кеш додатку - Дебаг Меню + Меню розробника + Логи ЛоРА Інверсія тексту Інверсія @@ -298,8 +301,19 @@ Щоб використовувати локальну спеціальну модель, помістіть її в локальну папку в пам’яті телефону: Download/SDAi/model Остаточна структура папок має бути такою: + Відладка + Work Manager API QA тест-кейси + Режим розробника розблоковано! + Дивитися логи + Видалити всі логи + Перезапуск останнього txt2img завдання + Перезапуск останнього img2img завдання + Скасувати всі завдання + Дозволити переривання генерації + Потік Внести битий Base64 в БД + Лог файл пустий. Тут порожньо… Додайте щось на %1$s сервері до: \n\n%2$s @@ -330,4 +344,9 @@ Додаток не має дозволів. Довольте права на %1$s в налаштуваннях. + + UI Потік + I/O Потік + Обчислення + Один потік diff --git a/core/localization/src/main/res/values-zh/strings.xml b/core/localization/src/main/res/values-zh/strings.xml index 078471ee..0849958f 100644 --- a/core/localization/src/main/res/values-zh/strings.xml +++ b/core/localization/src/main/res/values-zh/strings.xml @@ -21,6 +21,8 @@ 应用 关闭 下一步 + ✅ 成功 + ❌ 失败 @@ -191,7 +193,8 @@ 配置 选择SD ML模型 清除应用缓存 - 调试菜单 + 开发者模式 + 日志 LoRA 超网络 H-Net @@ -360,8 +363,19 @@ 最终的文件夹结构应该是: + 调试 + 工作管理器 API QA操作 + 开发者模式已解锁! + 查看日志 + 清除所有日志 + 重新启动最后一个 txt2img 任务 + 重新启动最后一个 img2img 任务 + 取消所有工作程序 + 允许中断生成 + 进程调度程序 在数据库中插入错误的Base64 + 未找到日志。 这里什么都没有… @@ -396,4 +410,9 @@ 缺少权限。 请在应用程序设置中允许 %1$s 权限。 + + 主线程 + I/O 线程 + 计算 + 单线程 diff --git a/core/localization/src/main/res/values/strings.xml b/core/localization/src/main/res/values/strings.xml index b9f9e542..bd1b8e31 100755 --- a/core/localization/src/main/res/values/strings.xml +++ b/core/localization/src/main/res/values/strings.xml @@ -20,6 +20,8 @@ Apply Close Next + ✅ Success + ❌ Failure Two Three @@ -171,7 +173,8 @@ Configuration Select SD ML Model Clear app cache - Debug Menu + Developer mode + Logs LoRA Hypernetworks H-Net @@ -320,8 +323,20 @@ To use local custom model, place it to local folder in your phone storage: Download/SDAi/model The final folder structure should be: + Debugging + Local Diffusion + Work Manager API QA actions + Developer mode unlocked! + View logs + Clear all logs + Restart last txt2img task + Restart last img2img task + Cancel all workers + Allow to interrupt generation + Process scheduler Insert bad Base64 in DB + No logs found. Nothing here… Add some content on %1$s server to: \n\n%2$s @@ -353,4 +368,9 @@ Missing permissions. Please allow %1$s permission in application settings. + + Main Thread + I/O Thread + Computation + Single Thread diff --git a/data/src/main/java/com/shifthackz/aisdv1/data/preference/PreferenceManagerImpl.kt b/data/src/main/java/com/shifthackz/aisdv1/data/preference/PreferenceManagerImpl.kt index 224449af..1a41e3a5 100644 --- a/data/src/main/java/com/shifthackz/aisdv1/data/preference/PreferenceManagerImpl.kt +++ b/data/src/main/java/com/shifthackz/aisdv1/data/preference/PreferenceManagerImpl.kt @@ -3,6 +3,7 @@ package com.shifthackz.aisdv1.data.preference import android.content.SharedPreferences import com.shifthackz.aisdv1.core.common.extensions.fixUrlSlashes import com.shifthackz.aisdv1.core.common.extensions.shouldUseNewMediaStore +import com.shifthackz.aisdv1.core.common.schedulers.SchedulersToken import com.shifthackz.aisdv1.domain.entity.ColorToken import com.shifthackz.aisdv1.domain.entity.DarkThemeToken import com.shifthackz.aisdv1.domain.entity.FeatureTag @@ -51,6 +52,29 @@ class PreferenceManagerImpl( .apply() .also { onPreferencesChanged() } + override var developerMode: Boolean + get() = preferences.getBoolean(KEY_DEVELOPER_MODE, false) + set(value) = preferences.edit() + .putBoolean(KEY_DEVELOPER_MODE, value) + .apply() + .also { onPreferencesChanged() } + + override var localDiffusionAllowCancel: Boolean + get() = preferences.getBoolean(KEY_ALLOW_LOCAL_DIFFUSION_CANCEL, false) + set(value) = preferences.edit() + .putBoolean(KEY_ALLOW_LOCAL_DIFFUSION_CANCEL, value) + .apply() + .also { onPreferencesChanged() } + + override var localDiffusionSchedulerThread: SchedulersToken + get() = preferences + .getInt(KEY_LOCAL_DIFFUSION_SCHEDULER_THREAD, SchedulersToken.COMPUTATION.ordinal) + .let { SchedulersToken.entries[it] } + set(value) = preferences.edit() + .putInt(KEY_LOCAL_DIFFUSION_SCHEDULER_THREAD, value.ordinal) + .apply() + .also { onPreferencesChanged() } + override var monitorConnectivity: Boolean get() = if (!source.featureTags.contains(FeatureTag.OwnServer)) false else preferences.getBoolean(KEY_MONITOR_CONNECTIVITY, true) @@ -232,6 +256,9 @@ class PreferenceManagerImpl( serverUrl = automatic1111ServerUrl, sdModel = sdModel, demoMode = demoMode, + developerMode = developerMode, + localDiffusionAllowCancel = localDiffusionAllowCancel, + localDiffusionSchedulerThread = localDiffusionSchedulerThread, monitorConnectivity = monitorConnectivity, backgroundGeneration = backgroundGeneration, autoSaveAiResults = autoSaveAiResults, @@ -257,6 +284,9 @@ class PreferenceManagerImpl( const val KEY_SWARM_SERVER_URL = "key_swarm_server_url" const val KEY_SWARM_MODEL = "key_swarm_model" const val KEY_DEMO_MODE = "key_demo_mode" + const val KEY_DEVELOPER_MODE = "key_developer_mode" + const val KEY_ALLOW_LOCAL_DIFFUSION_CANCEL = "key_allow_local_diffusion_cancel" + const val KEY_LOCAL_DIFFUSION_SCHEDULER_THREAD = "key_local_diffusion_scheduler_thread" const val KEY_MONITOR_CONNECTIVITY = "key_monitor_connectivity" const val KEY_AI_AUTO_SAVE = "key_ai_auto_save" const val KEY_SAVE_TO_MEDIA_STORE = "key_save_to_media_store" diff --git a/data/src/main/java/com/shifthackz/aisdv1/data/repository/LocalDiffusionGenerationRepositoryImpl.kt b/data/src/main/java/com/shifthackz/aisdv1/data/repository/LocalDiffusionGenerationRepositoryImpl.kt index 36856cbe..0254b3d8 100644 --- a/data/src/main/java/com/shifthackz/aisdv1/data/repository/LocalDiffusionGenerationRepositoryImpl.kt +++ b/data/src/main/java/com/shifthackz/aisdv1/data/repository/LocalDiffusionGenerationRepositoryImpl.kt @@ -19,8 +19,8 @@ internal class LocalDiffusionGenerationRepositoryImpl( mediaStoreGateway: MediaStoreGateway, base64ToBitmapConverter: Base64ToBitmapConverter, localDataSource: GenerationResultDataSource.Local, - preferenceManager: PreferenceManager, backgroundWorkObserver: BackgroundWorkObserver, + private val preferenceManager: PreferenceManager, private val localDiffusion: LocalDiffusion, private val downloadableLocalDataSource: DownloadableModelDataSource.Local, private val bitmapToBase64Converter: BitmapToBase64Converter, @@ -46,7 +46,7 @@ internal class LocalDiffusionGenerationRepositoryImpl( private fun generate(payload: TextToImagePayload) = localDiffusion .process(payload) - .subscribeOn(schedulersProvider.computation) + .subscribeOn(schedulersProvider.byToken(preferenceManager.localDiffusionSchedulerThread)) .map(BitmapToBase64Converter::Input) .flatMap(bitmapToBase64Converter::invoke) .map(BitmapToBase64Converter.Output::base64ImageString) diff --git a/data/src/test/java/com/shifthackz/aisdv1/data/repository/LocalDiffusionGenerationRepositoryImplTest.kt b/data/src/test/java/com/shifthackz/aisdv1/data/repository/LocalDiffusionGenerationRepositoryImplTest.kt index c92ecaaf..75df885f 100644 --- a/data/src/test/java/com/shifthackz/aisdv1/data/repository/LocalDiffusionGenerationRepositoryImplTest.kt +++ b/data/src/test/java/com/shifthackz/aisdv1/data/repository/LocalDiffusionGenerationRepositoryImplTest.kt @@ -2,6 +2,7 @@ package com.shifthackz.aisdv1.data.repository import android.graphics.Bitmap import com.shifthackz.aisdv1.core.common.schedulers.SchedulersProvider +import com.shifthackz.aisdv1.core.common.schedulers.SchedulersToken import com.shifthackz.aisdv1.core.imageprocessing.Base64ToBitmapConverter import com.shifthackz.aisdv1.core.imageprocessing.BitmapToBase64Converter import com.shifthackz.aisdv1.data.mocks.mockLocalAiModel @@ -61,6 +62,10 @@ class LocalDiffusionGenerationRepositoryImplTest { @Before fun initialize() { + every { + stubPreferenceManager::localDiffusionSchedulerThread.get() + } returns SchedulersToken.COMPUTATION + every { stubBackgroundWorkObserver.hasActiveTasks() } returns false diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/entity/Settings.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/entity/Settings.kt index 723e5dac..c4da9e46 100644 --- a/domain/src/main/java/com/shifthackz/aisdv1/domain/entity/Settings.kt +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/entity/Settings.kt @@ -1,9 +1,14 @@ package com.shifthackz.aisdv1.domain.entity +import com.shifthackz.aisdv1.core.common.schedulers.SchedulersToken + data class Settings( val serverUrl: String = "", val sdModel: String = "", val demoMode: Boolean = false, + val developerMode: Boolean = false, + val localDiffusionAllowCancel: Boolean = false, + val localDiffusionSchedulerThread: SchedulersToken = SchedulersToken.COMPUTATION, val monitorConnectivity: Boolean = false, val backgroundGeneration: Boolean = false, val autoSaveAiResults: Boolean = false, diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/feature/work/BackgroundTaskManager.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/feature/work/BackgroundTaskManager.kt index ecf2f0cc..78199398 100644 --- a/domain/src/main/java/com/shifthackz/aisdv1/domain/feature/work/BackgroundTaskManager.kt +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/feature/work/BackgroundTaskManager.kt @@ -6,4 +6,7 @@ import com.shifthackz.aisdv1.domain.entity.TextToImagePayload interface BackgroundTaskManager { fun scheduleTextToImageTask(payload: TextToImagePayload) fun scheduleImageToImageTask(payload: ImageToImagePayload) + fun retryLastTextToImageTask(): Result + fun retryLastImageToImageTask(): Result + fun cancelAll(): Result } diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/preference/PreferenceManager.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/preference/PreferenceManager.kt index bc52c431..2ea33eef 100644 --- a/domain/src/main/java/com/shifthackz/aisdv1/domain/preference/PreferenceManager.kt +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/preference/PreferenceManager.kt @@ -1,5 +1,6 @@ package com.shifthackz.aisdv1.domain.preference +import com.shifthackz.aisdv1.core.common.schedulers.SchedulersToken import com.shifthackz.aisdv1.domain.entity.Grid import com.shifthackz.aisdv1.domain.entity.ServerSource import com.shifthackz.aisdv1.domain.entity.Settings @@ -10,6 +11,9 @@ interface PreferenceManager { var swarmUiServerUrl: String var swarmUiModel: String var demoMode: Boolean + var developerMode: Boolean + var localDiffusionAllowCancel: Boolean + var localDiffusionSchedulerThread: SchedulersToken var monitorConnectivity: Boolean var autoSaveAiResults: Boolean var saveToMediaStore: Boolean diff --git a/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/LocalDiffusionContract.kt b/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/LocalDiffusionContract.kt index c1dbf677..a59a7afc 100644 --- a/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/LocalDiffusionContract.kt +++ b/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/LocalDiffusionContract.kt @@ -3,6 +3,10 @@ package com.shifthackz.aisdv1.feature.diffusion internal object LocalDiffusionContract { + //region LOGGING + const val TAG = "LocalDiffusion" + //endregion + //region MODELS PATHS const val UNET_MODEL = "unet/model.ort" const val VAE_MODEL = "vae_decoder/model.ort" diff --git a/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/LocalDiffusionImpl.kt b/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/LocalDiffusionImpl.kt index b2698c84..56b3f39d 100644 --- a/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/LocalDiffusionImpl.kt +++ b/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/LocalDiffusionImpl.kt @@ -2,8 +2,11 @@ package com.shifthackz.aisdv1.feature.diffusion import ai.onnxruntime.OnnxTensor import android.graphics.Bitmap +import com.shifthackz.aisdv1.core.common.log.debugLog +import com.shifthackz.aisdv1.core.common.log.errorLog import com.shifthackz.aisdv1.domain.entity.TextToImagePayload import com.shifthackz.aisdv1.domain.feature.diffusion.LocalDiffusion +import com.shifthackz.aisdv1.feature.diffusion.LocalDiffusionContract.TAG import com.shifthackz.aisdv1.feature.diffusion.ai.tokenizer.LocalDiffusionTextTokenizer import com.shifthackz.aisdv1.feature.diffusion.ai.unet.UNet import com.shifthackz.aisdv1.feature.diffusion.environment.OrtEnvironmentProvider @@ -21,15 +24,25 @@ internal class LocalDiffusionImpl( override fun process(payload: TextToImagePayload): Single = Single.create { emitter -> try { + emitter.setCancellable { + debugLog(TAG, "{$TAG} Received cancelable signal.") + interruptGeneration() + } uNet.setCallback(object : UNet.Callback { override fun onStep(maxStep: Int, step: Int) { + debugLog(TAG, "Received step update: ${maxStep}/${step}") statusSubject.onNext(LocalDiffusion.Status(step, maxStep)) } override fun onBuildImage(status: Int, bitmap: Bitmap?) { - if (!emitter.isDisposed) { - bitmap?.let(emitter::onSuccess) ?: emitter.onError(Throwable("Bitmap is null")) - } + bitmap + ?.let(emitter::onSuccess) + ?.also { debugLog("{$TAG} Bitmap built successfully!") } + ?: run { + val t = Throwable("Bitmap is null") + errorLog(t, "{$TAG} Bitmap is null.") + emitter.onError(t) + } } }) @@ -71,15 +84,23 @@ internal class LocalDiffusionImpl( height = payload.height, ) } catch (e: Exception) { - if (!emitter.isDisposed) emitter.onError(e) + errorLog(e, "{$TAG} Caught exception while Local Diffusion process.") + interruptGeneration() + emitter.onError(e) } } // ToDo review method of LocalDiffusion cancellation, now next generation crashes using this approach override fun interrupt() = Completable.fromAction { - tokenizer.close() - uNet.close() + interruptGeneration() } override fun observeStatus() = statusSubject + + private fun interruptGeneration() { + debugLog("{$TAG} Trying to interrupt generation.") + tokenizer.close() + uNet.close() + debugLog("{$TAG} Generation interrupt successful!") + } } diff --git a/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/ai/tokenizer/EnglishTextTokenizer.kt b/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/ai/tokenizer/EnglishTextTokenizer.kt index 9cd3c615..e539317b 100644 --- a/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/ai/tokenizer/EnglishTextTokenizer.kt +++ b/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/ai/tokenizer/EnglishTextTokenizer.kt @@ -8,11 +8,13 @@ import android.text.TextUtils import android.util.JsonReader import android.util.Pair import com.shifthackz.aisdv1.core.common.file.FileProviderDescriptor +import com.shifthackz.aisdv1.core.common.log.debugLog import com.shifthackz.aisdv1.core.common.log.errorLog import com.shifthackz.aisdv1.feature.diffusion.LocalDiffusionContract import com.shifthackz.aisdv1.feature.diffusion.LocalDiffusionContract.KEY_INPUT_IDS import com.shifthackz.aisdv1.feature.diffusion.LocalDiffusionContract.ORT import com.shifthackz.aisdv1.feature.diffusion.LocalDiffusionContract.ORT_KEY_MODEL_FORMAT +import com.shifthackz.aisdv1.feature.diffusion.LocalDiffusionContract.TAG import com.shifthackz.aisdv1.feature.diffusion.ai.extensions.halfCorner import com.shifthackz.aisdv1.feature.diffusion.ai.extensions.toArrays import com.shifthackz.aisdv1.feature.diffusion.environment.LocalModelIdProvider @@ -44,23 +46,32 @@ internal class EnglishTextTokenizer( override val maxLength = 77 override fun initialize() { - if (session != null) return + if (session != null) { + debugLog("{$TAG} {TOKENIZER} {initialize} Session already initialized, skipping...") + return + } val options = OrtSession.SessionOptions() options.addConfigEntry(ORT_KEY_MODEL_FORMAT, ORT) session = ortEnvironmentProvider.get().createSession( "${modelPathPrefix(fileProviderDescriptor, localModelIdProvider)}/${LocalDiffusionContract.TOKENIZER_MODEL}", options ) + debugLog("{$TAG} {TOKENIZER} {initialize} Session created successfully!") if (!isInitMap) { encoder.putAll(loadEncoder()) decoder.putAll(loadDecoder(encoder)) bpeRanks.putAll(loadBpeRanks()) } isInitMap = true + debugLog("{$TAG} {TOKENIZER} {initialize} Tokenizer map initialized successfully!") } override fun decode(ids: IntArray?): String { - if (ids == null) return "" + debugLog("{$TAG} {TOKENIZER} {decode} Trying to decode ${ids?.size ?: "null"} int array...") + if (ids == null) { + debugLog("{$TAG} {TOKENIZER} {decode} Input ids array is null, skipping.") + return "" + } val stringBuilder = StringBuilder() for (value in ids) { if (decoder.containsKey(value)) stringBuilder.append(decoder[value]) @@ -74,10 +85,13 @@ internal class EnglishTextTokenizer( } val ints = IntArray(result.size) for (i in result.indices) ints[i] = result[i] - return String(ints, 0, ints.size) + val resultString = String(ints, 0, ints.size) + debugLog("{$TAG} {TOKENIZER} {decode} Decode was successful!") + return resultString } override fun encode(text: String?): IntArray { + debugLog("{$TAG} {TOKENIZER} {encode} Trying to encode ${text ?: "null"}...") var input = text input = input.toString().lowercase(Locale.getDefault()).halfCorner() val stringList: MutableList = ArrayList() @@ -113,11 +127,16 @@ internal class EnglishTextTokenizer( Arrays.fill(copy, 49407) System.arraycopy(ids, 0, copy, 0, if (ids.size < copy.size) ids.size else copy.size) copy[copy.size - 1] = 49407 + debugLog("{$TAG} {TOKENIZER} {encode} Encode was successful!") return copy } override fun tensor(ids: IntArray?): OnnxTensor? { - if (ids == null) return null + debugLog("{$TAG} {TOKENIZER} {tensor} Trying to tensor ${ids?.size ?: "null"} int array...") + if (ids == null) { + debugLog("{$TAG} {TOKENIZER} {tensor} Input ids array is null, skipping.") + return null + } val inputIds = OnnxTensor.createTensor( ortEnvironmentProvider.get(), IntBuffer.wrap(ids), @@ -128,14 +147,18 @@ internal class EnglishTextTokenizer( val result = session!!.run(input) val lastHiddenState = result[0].value result.close() - return OnnxTensor.createTensor(ortEnvironmentProvider.get(), lastHiddenState) + val tensor = OnnxTensor.createTensor(ortEnvironmentProvider.get(), lastHiddenState) + debugLog("{$TAG} {TOKENIZER} {tensor} Tensor formation was successful!") + return tensor } override fun createUnconditionalInput(text: String?): IntArray = encode(text) override fun close() { + debugLog("{$TAG} {TOKENIZER} {close} Closing session...") session?.close() session = null + debugLog("{$TAG} {TOKENIZER} {close} Session closed successfully!") } private fun bpe(token: String): List { diff --git a/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/ai/unet/UNet.kt b/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/ai/unet/UNet.kt index ba8f13b7..96d427f2 100644 --- a/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/ai/unet/UNet.kt +++ b/feature/diffusion/src/main/java/com/shifthackz/aisdv1/feature/diffusion/ai/unet/UNet.kt @@ -9,6 +9,7 @@ import ai.onnxruntime.providers.NNAPIFlags import android.graphics.Bitmap import android.util.Pair import com.shifthackz.aisdv1.core.common.file.FileProviderDescriptor +import com.shifthackz.aisdv1.core.common.log.debugLog import com.shifthackz.aisdv1.feature.diffusion.LocalDiffusionContract import com.shifthackz.aisdv1.feature.diffusion.LocalDiffusionContract.KEY_ENCODER_HIDDEN_STATES import com.shifthackz.aisdv1.feature.diffusion.LocalDiffusionContract.KEY_LATENT_SAMPLE @@ -16,6 +17,7 @@ import com.shifthackz.aisdv1.feature.diffusion.LocalDiffusionContract.KEY_SAMPLE import com.shifthackz.aisdv1.feature.diffusion.LocalDiffusionContract.KEY_TIME_STEP import com.shifthackz.aisdv1.feature.diffusion.LocalDiffusionContract.ORT import com.shifthackz.aisdv1.feature.diffusion.LocalDiffusionContract.ORT_KEY_MODEL_FORMAT +import com.shifthackz.aisdv1.feature.diffusion.LocalDiffusionContract.TAG import com.shifthackz.aisdv1.feature.diffusion.ai.extensions.duplicate import com.shifthackz.aisdv1.feature.diffusion.ai.extensions.getSizes import com.shifthackz.aisdv1.feature.diffusion.ai.extensions.multipleTensorsByFloat @@ -155,9 +157,17 @@ internal class UNet( width: Int, height: Int, ) { + debugLog("{$TAG} {uNet} {inference} Trying to start inference:") + debugLog("{$TAG} {uNet} {inference} - seed: $seedNum") + debugLog("{$TAG} {uNet} {inference} - numInferenceSteps: $numInferenceSteps") + debugLog("{$TAG} {uNet} {inference} - textEmbeddings: $textEmbeddings") + debugLog("{$TAG} {uNet} {inference} - guidanceScale: $guidanceScale") + debugLog("{$TAG} {uNet} {inference} - batchSize: $batchSize") + debugLog("{$TAG} {uNet} {inference} - size: ${width}x${height}") this.width = width this.height = height val localDiffusionScheduler = EulerAncestralDiscreteLocalDiffusionScheduler() + debugLog("{$TAG} {uNet} {inference} Initialized scheduler: $localDiffusionScheduler") val timeSteps: IntArray = localDiffusionScheduler.setTimeSteps(numInferenceSteps) val seed = if (seedNum <= 0) random.nextLong() else seedNum var latents: LocalDiffusionTensor<*> = generateLatentSample( @@ -167,13 +177,19 @@ internal class UNet( seed, localDiffusionScheduler.initNoiseSigma.toFloat() ) + debugLog("{$TAG} {uNet} {inference} Got latents: ${latents.hashCode()}") val shape = longArrayOf(2, 4, (height / 8).toLong(), (width / 8).toLong()) + debugLog("{$TAG} {uNet} {inference} Got shape: $shape") + debugLog("{$TAG} {uNet} {inference} Starting steps processing! Total : ${timeSteps.size}") for (i in timeSteps.indices) { var latentModelInput: LocalDiffusionTensor<*> = duplicate( latents.tensor.floatBuffer.array(), shape, ) latentModelInput = localDiffusionScheduler.scaleModelInput(latentModelInput, i) + debugLog("{$TAG} {uNet} {inference} {Step_$i} ------------------") + debugLog("{$TAG} {uNet} {inference} {Step_$i} Latent model input: $latentModelInput") + debugLog("{$TAG} {uNet} {inference} {Step_$i} Notifying callback about step.") callback?.onStep(timeSteps.size, i) val input = createUNetModelInput( textEmbeddings, @@ -184,8 +200,11 @@ internal class UNet( longArrayOf(1) ) ) + debugLog("{$TAG} {uNet} {inference} {Step_$i} Got uNet model input: $input") val result = session!!.run(input) + debugLog("{$TAG} {uNet} {inference} {Step_$i} Got result from uNet session: $result") val dataSet = result[0].value as Array3D + debugLog("{$TAG} {uNet} {inference} {Step_$i} Trying to close ORT session in: $result") result.close() val splitTensors: Pair, Array3D> = splitTensor( @@ -194,7 +213,13 @@ internal class UNet( ) val noisePrediction = splitTensors.first val noisePredictionText = splitTensors.second + debugLog("{$TAG} {uNet} {inference} {Step_$i} Got split tensors with prediction:") + debugLog("{$TAG} {uNet} {inference} {Step_$i} - splitTensors: $splitTensors") + debugLog("{$TAG} {uNet} {inference} {Step_$i} - noisePrediction: $noisePrediction") + debugLog("{$TAG} {uNet} {inference} {Step_$i} - noisePredictionText: $noisePredictionText") + debugLog("{$TAG} {uNet} {inference} {Step_$i} Trying to preform guidance...") performGuidance(noisePrediction, noisePredictionText, guidanceScale) + debugLog("{$TAG} {uNet} {inference} {Step_$i} Guidance performed successfully!") latents = localDiffusionScheduler.step( LocalDiffusionTensor( OnnxTensor.createTensor( @@ -207,16 +232,22 @@ internal class UNet( i, latents, ) + debugLog("{$TAG} {uNet} {inference} {Step_$i} Finalized latents: $latents") + debugLog("{$TAG} {uNet} {inference} {Step_$i} ------------------") } callback?.also { clb -> + debugLog("{$TAG} {uNet} {inference} Finalization / Flushing image...") callback?.onStep(timeSteps.size, timeSteps.size) val bitmap = decode(latents) + debugLog("{$TAG} {uNet} {inference} Finalization / Decoded bitmap: ${bitmap.hashCode()}") clb.onBuildImage(0, bitmap) + debugLog("{$TAG} {uNet} {inference} Finalization / Notifying callback and closing session.") close() } } fun decode(latents: LocalDiffusionTensor<*>): Bitmap { + debugLog("{$TAG} {uNet} {decode} Trying to decode latents: ${latents.hashCode()}") val tensor: LocalDiffusionTensor<*> = multipleTensorsByFloat( latents.tensor.floatBuffer.array(), 1.0f / 0.18215f, @@ -225,21 +256,26 @@ internal class UNet( val decoderInput: MutableMap = HashMap() decoderInput[KEY_LATENT_SAMPLE] = tensor.tensor val value: Any = decoder!!.decode(decoderInput.toMap()) - return decoder!!.convertToImage( + val bitmap = decoder!!.convertToImage( value as Array3D, width, height, ) + debugLog("{$TAG} {uNet} {decode} Bitmap generated successfully: ${bitmap.hashCode()}") + return bitmap } fun close() { + debugLog("{$TAG} {uNet} {close} Closing session...") session?.close() decoder?.close() session = null decoder = null + debugLog("{$TAG} {uNet} {close} Session closed successfully!") } fun setCallback(callback: Callback?) { + debugLog("{$TAG} {uNet} Setting new result callback ${callback.hashCode()}") this.callback = callback } diff --git a/feature/work/src/main/java/com/shifthackz/aisdv1/work/BackgroundTaskManagerImpl.kt b/feature/work/src/main/java/com/shifthackz/aisdv1/work/BackgroundTaskManagerImpl.kt index 175a62d3..c1444ec9 100644 --- a/feature/work/src/main/java/com/shifthackz/aisdv1/work/BackgroundTaskManagerImpl.kt +++ b/feature/work/src/main/java/com/shifthackz/aisdv1/work/BackgroundTaskManagerImpl.kt @@ -25,6 +25,33 @@ internal class BackgroundTaskManagerImpl : BackgroundTaskManager { runWork(payload.toByteArray(), Constants.FILE_IMAGE_TO_IMAGE) } + override fun retryLastTextToImageTask(): Result { + try { + val bytes = readPayload(Constants.FILE_TEXT_TO_IMAGE) + ?: return Result.failure(Throwable("Payload is null.")) + runWork(bytes, Constants.FILE_TEXT_TO_IMAGE) + return Result.success(Unit) + } catch (e: Exception) { + return Result.failure(e) + } + } + + override fun retryLastImageToImageTask(): Result { + try { + val bytes = readPayload(Constants.FILE_IMAGE_TO_IMAGE) + ?: return Result.failure(Throwable("Payload is null.")) + runWork(bytes, Constants.FILE_IMAGE_TO_IMAGE) + return Result.success(Unit) + } catch (e: Exception) { + return Result.failure(e) + } + } + + override fun cancelAll(): Result = runCatching { + val workManager: WorkManagerProvider by inject(WorkManagerProvider::class.java) + workManager().cancelAllWork() + } + private inline fun runWork(bytes: ByteArray, fileName: String) { val workManager: WorkManagerProvider by inject(WorkManagerProvider::class.java) val workRequest = OneTimeWorkRequestBuilder() @@ -33,7 +60,7 @@ internal class BackgroundTaskManagerImpl : BackgroundTaskManager { .build() writePayload(bytes, fileName) - workManager().cancelAllWork() + workManager().cancelUniqueWork(Constants.TAG_GENERATION) workManager().enqueueUniqueWork( Constants.TAG_GENERATION, ExistingWorkPolicy.REPLACE, @@ -41,6 +68,16 @@ internal class BackgroundTaskManagerImpl : BackgroundTaskManager { ) } + private fun readPayload(fileName: String): ByteArray? { + val fileProviderDescriptor: FileProviderDescriptor by inject(FileProviderDescriptor::class.java) + val cacheDirectory = File(fileProviderDescriptor.workCacheDirPath) + if (!cacheDirectory.exists()) { + return null + } + val outFile = File(cacheDirectory, fileName) + return outFile.readBytes() + } + private fun writePayload(bytes: ByteArray, fileName: String) { val fileProviderDescriptor: FileProviderDescriptor by inject(FileProviderDescriptor::class.java) val cacheDirectory = File(fileProviderDescriptor.workCacheDirPath) diff --git a/feature/work/src/main/java/com/shifthackz/aisdv1/work/core/CoreGenerationWorker.kt b/feature/work/src/main/java/com/shifthackz/aisdv1/work/core/CoreGenerationWorker.kt index 1dd69c12..d3076dee 100644 --- a/feature/work/src/main/java/com/shifthackz/aisdv1/work/core/CoreGenerationWorker.kt +++ b/feature/work/src/main/java/com/shifthackz/aisdv1/work/core/CoreGenerationWorker.kt @@ -13,6 +13,7 @@ import com.shifthackz.aisdv1.domain.entity.AiGenerationResult import com.shifthackz.aisdv1.domain.entity.ServerSource import com.shifthackz.aisdv1.domain.feature.work.BackgroundWorkObserver import com.shifthackz.aisdv1.domain.preference.PreferenceManager +import com.shifthackz.aisdv1.domain.usecase.generation.InterruptGenerationUseCase import com.shifthackz.aisdv1.domain.usecase.generation.ObserveHordeProcessStatusUseCase import com.shifthackz.aisdv1.domain.usecase.generation.ObserveLocalDiffusionProcessStatusUseCase import io.reactivex.rxjava3.disposables.CompositeDisposable @@ -24,10 +25,11 @@ internal abstract class CoreGenerationWorker( workerParameters: WorkerParameters, pushNotificationManager: PushNotificationManager, activityIntentProvider: ActivityIntentProvider, - preferenceManager: PreferenceManager, + private val preferenceManager: PreferenceManager, private val backgroundWorkObserver: BackgroundWorkObserver, private val observeHordeProcessStatusUseCase: ObserveHordeProcessStatusUseCase, private val observeLocalDiffusionProcessStatusUseCase: ObserveLocalDiffusionProcessStatusUseCase, + private val interruptGenerationUseCase: InterruptGenerationUseCase, ) : NotificationWorker( context = context, workerParameters = workerParameters, @@ -41,6 +43,11 @@ internal abstract class CoreGenerationWorker( override fun onStopped() { super.onStopped() + runCatching { + interruptGenerationUseCase() + .onErrorComplete() + .blockingAwait() + } compositeDisposable.clear() backgroundWorkObserver.postCancelSignal() } @@ -86,7 +93,7 @@ internal abstract class CoreGenerationWorker( body = subTitle, silent = true, progress = status.current to status.total, - canCancel = false, + canCancel = preferenceManager.localDiffusionAllowCancel, ) } } @@ -118,7 +125,6 @@ internal abstract class CoreGenerationWorker( } protected fun handleError(t: Throwable) { - errorLog(t) backgroundWorkObserver.postFailedSignal(t) val title = applicationContext.getString(LocalizationR.string.notification_fail_title) val subTitle = applicationContext.getString(LocalizationR.string.notification_fail_sub_title) diff --git a/feature/work/src/main/java/com/shifthackz/aisdv1/work/di/SdaiWorkerFactory.kt b/feature/work/src/main/java/com/shifthackz/aisdv1/work/di/SdaiWorkerFactory.kt index 8d5dc6ce..68d419d1 100644 --- a/feature/work/src/main/java/com/shifthackz/aisdv1/work/di/SdaiWorkerFactory.kt +++ b/feature/work/src/main/java/com/shifthackz/aisdv1/work/di/SdaiWorkerFactory.kt @@ -10,6 +10,7 @@ import com.shifthackz.aisdv1.core.notification.PushNotificationManager import com.shifthackz.aisdv1.domain.feature.work.BackgroundWorkObserver import com.shifthackz.aisdv1.domain.preference.PreferenceManager import com.shifthackz.aisdv1.domain.usecase.generation.ImageToImageUseCase +import com.shifthackz.aisdv1.domain.usecase.generation.InterruptGenerationUseCase import com.shifthackz.aisdv1.domain.usecase.generation.ObserveHordeProcessStatusUseCase import com.shifthackz.aisdv1.domain.usecase.generation.ObserveLocalDiffusionProcessStatusUseCase import com.shifthackz.aisdv1.domain.usecase.generation.TextToImageUseCase @@ -24,6 +25,7 @@ class SdaiWorkerFactory( private val imageToImageUseCase: ImageToImageUseCase, private val observeHordeProcessStatusUseCase: ObserveHordeProcessStatusUseCase, private val observeLocalDiffusionProcessStatusUseCase: ObserveLocalDiffusionProcessStatusUseCase, + private val interruptGenerationUseCase: InterruptGenerationUseCase, private val fileProviderDescriptor: FileProviderDescriptor, private val activityIntentProvider: ActivityIntentProvider, ) : WorkerFactory() { @@ -44,6 +46,7 @@ class SdaiWorkerFactory( textToImageUseCase = textToImageUseCase, observeHordeProcessStatusUseCase = observeHordeProcessStatusUseCase, observeLocalDiffusionProcessStatusUseCase = observeLocalDiffusionProcessStatusUseCase, + interruptGenerationUseCase = interruptGenerationUseCase, fileProviderDescriptor = fileProviderDescriptor, ) @@ -57,6 +60,7 @@ class SdaiWorkerFactory( imageToImageUseCase = imageToImageUseCase, observeHordeProcessStatusUseCase = observeHordeProcessStatusUseCase, observeLocalDiffusionProcessStatusUseCase = observeLocalDiffusionProcessStatusUseCase, + interruptGenerationUseCase = interruptGenerationUseCase, fileProviderDescriptor = fileProviderDescriptor, ) diff --git a/feature/work/src/main/java/com/shifthackz/aisdv1/work/task/ImageToImageTask.kt b/feature/work/src/main/java/com/shifthackz/aisdv1/work/task/ImageToImageTask.kt index 7dedd982..c6b69bf8 100644 --- a/feature/work/src/main/java/com/shifthackz/aisdv1/work/task/ImageToImageTask.kt +++ b/feature/work/src/main/java/com/shifthackz/aisdv1/work/task/ImageToImageTask.kt @@ -8,6 +8,7 @@ import com.shifthackz.aisdv1.core.notification.PushNotificationManager import com.shifthackz.aisdv1.domain.feature.work.BackgroundWorkObserver import com.shifthackz.aisdv1.domain.preference.PreferenceManager import com.shifthackz.aisdv1.domain.usecase.generation.ImageToImageUseCase +import com.shifthackz.aisdv1.domain.usecase.generation.InterruptGenerationUseCase import com.shifthackz.aisdv1.domain.usecase.generation.ObserveHordeProcessStatusUseCase import com.shifthackz.aisdv1.domain.usecase.generation.ObserveLocalDiffusionProcessStatusUseCase import com.shifthackz.aisdv1.work.Constants @@ -26,6 +27,7 @@ internal class ImageToImageTask( preferenceManager: PreferenceManager, observeHordeProcessStatusUseCase: ObserveHordeProcessStatusUseCase, observeLocalDiffusionProcessStatusUseCase: ObserveLocalDiffusionProcessStatusUseCase, + interruptGenerationUseCase: InterruptGenerationUseCase, private val backgroundWorkObserver: BackgroundWorkObserver, private val imageToImageUseCase: ImageToImageUseCase, private val fileProviderDescriptor: FileProviderDescriptor, @@ -38,6 +40,7 @@ internal class ImageToImageTask( backgroundWorkObserver = backgroundWorkObserver, observeHordeProcessStatusUseCase = observeHordeProcessStatusUseCase, observeLocalDiffusionProcessStatusUseCase = observeLocalDiffusionProcessStatusUseCase, + interruptGenerationUseCase = interruptGenerationUseCase, ) { override val notificationId = NOTIFICATION_IMAGE_TO_IMAGE_FOREGROUND diff --git a/feature/work/src/main/java/com/shifthackz/aisdv1/work/task/TextToImageTask.kt b/feature/work/src/main/java/com/shifthackz/aisdv1/work/task/TextToImageTask.kt index b392fd68..f9e68631 100644 --- a/feature/work/src/main/java/com/shifthackz/aisdv1/work/task/TextToImageTask.kt +++ b/feature/work/src/main/java/com/shifthackz/aisdv1/work/task/TextToImageTask.kt @@ -4,9 +4,12 @@ import android.content.Context import androidx.work.WorkerParameters import com.shifthackz.aisdv1.core.common.appbuild.ActivityIntentProvider import com.shifthackz.aisdv1.core.common.file.FileProviderDescriptor +import com.shifthackz.aisdv1.core.common.log.debugLog +import com.shifthackz.aisdv1.core.common.log.errorLog import com.shifthackz.aisdv1.core.notification.PushNotificationManager import com.shifthackz.aisdv1.domain.feature.work.BackgroundWorkObserver import com.shifthackz.aisdv1.domain.preference.PreferenceManager +import com.shifthackz.aisdv1.domain.usecase.generation.InterruptGenerationUseCase import com.shifthackz.aisdv1.domain.usecase.generation.ObserveHordeProcessStatusUseCase import com.shifthackz.aisdv1.domain.usecase.generation.ObserveLocalDiffusionProcessStatusUseCase import com.shifthackz.aisdv1.domain.usecase.generation.TextToImageUseCase @@ -25,6 +28,7 @@ internal class TextToImageTask( activityIntentProvider: ActivityIntentProvider, observeHordeProcessStatusUseCase: ObserveHordeProcessStatusUseCase, observeLocalDiffusionProcessStatusUseCase: ObserveLocalDiffusionProcessStatusUseCase, + interruptGenerationUseCase: InterruptGenerationUseCase, private val preferenceManager: PreferenceManager, private val backgroundWorkObserver: BackgroundWorkObserver, private val textToImageUseCase: TextToImageUseCase, @@ -38,6 +42,7 @@ internal class TextToImageTask( backgroundWorkObserver = backgroundWorkObserver, observeHordeProcessStatusUseCase = observeHordeProcessStatusUseCase, observeLocalDiffusionProcessStatusUseCase = observeLocalDiffusionProcessStatusUseCase, + interruptGenerationUseCase = interruptGenerationUseCase, ) { override val notificationId: Int = NOTIFICATION_TEXT_TO_IMAGE_FOREGROUND @@ -54,6 +59,7 @@ internal class TextToImageTask( handleError(Throwable("Background process count > 0")) compositeDisposable.clear() preferenceManager.backgroundProcessCount = 0 + debugLog("Background process count > 0! Skipping task.") return Single.just(Result.failure()) } @@ -61,13 +67,16 @@ internal class TextToImageTask( handleStart() backgroundWorkObserver.refreshStatus() backgroundWorkObserver.dismissResult() + debugLog("Starting TextToImageTask!") return try { val file = File(fileProviderDescriptor.workCacheDirPath, Constants.FILE_TEXT_TO_IMAGE) if (!file.exists()) { preferenceManager.backgroundProcessCount-- - handleError(Throwable("File is null.")) + val t = Throwable("File is null.") + handleError(t) compositeDisposable.clear() + errorLog(t, "Payload file does not exist.") return Single.just(Result.failure()) } @@ -76,8 +85,10 @@ internal class TextToImageTask( if (payload == null) { preferenceManager.backgroundProcessCount-- - handleError(Throwable("Payload is null.")) + val t = Throwable("Payload is null.") + handleError(t) compositeDisposable.clear() + errorLog(t, "Payload was failed to read/parse.") return Single.just(Result.failure()) } @@ -89,11 +100,13 @@ internal class TextToImageTask( .map { result -> preferenceManager.backgroundProcessCount-- handleSuccess(result) + debugLog("Generation finished successfully!") Result.success() } .onErrorReturn { t -> preferenceManager.backgroundProcessCount-- handleError(t) + errorLog(t, "Caught exception from TextToImageUseCase!") Result.failure() } .doFinally { compositeDisposable.clear() } @@ -101,6 +114,7 @@ internal class TextToImageTask( preferenceManager.backgroundProcessCount-- handleError(e) compositeDisposable.clear() + errorLog(e, "Caught exception from TextToImageTask worker!") Single.just(Result.failure()) } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/di/ViewModelModule.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/di/ViewModelModule.kt index cbaa637f..a9a1aac4 100755 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/di/ViewModelModule.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/di/ViewModelModule.kt @@ -14,6 +14,7 @@ import com.shifthackz.aisdv1.presentation.screen.home.HomeNavigationViewModel import com.shifthackz.aisdv1.presentation.screen.img2img.ImageToImageViewModel import com.shifthackz.aisdv1.presentation.screen.inpaint.InPaintViewModel import com.shifthackz.aisdv1.presentation.screen.loader.ConfigurationLoaderViewModel +import com.shifthackz.aisdv1.presentation.screen.logger.LoggerViewModel import com.shifthackz.aisdv1.presentation.screen.settings.SettingsViewModel import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupViewModel @@ -50,6 +51,7 @@ val viewModelModule = module { viewModelOf(::WebUiViewModel) viewModelOf(::DonateViewModel) viewModelOf(::BackgroundWorkViewModel) + viewModelOf(::LoggerViewModel) viewModel { parameters -> val launchSource = ServerSetupLaunchSource.fromKey(parameters.get()) diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ModalRenderer.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ModalRenderer.kt index 5cfdcd6b..860d3fd0 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ModalRenderer.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ModalRenderer.kt @@ -27,8 +27,10 @@ import com.shifthackz.aisdv1.presentation.modal.extras.ExtrasScreen import com.shifthackz.aisdv1.presentation.modal.grid.GridBottomSheet import com.shifthackz.aisdv1.presentation.modal.history.InputHistoryScreen import com.shifthackz.aisdv1.presentation.modal.language.LanguageBottomSheet +import com.shifthackz.aisdv1.presentation.modal.ldscheduler.LDSchedulerBottomSheet import com.shifthackz.aisdv1.presentation.modal.tag.EditTagDialog import com.shifthackz.aisdv1.presentation.model.Modal +import com.shifthackz.aisdv1.presentation.screen.debug.DebugMenuIntent import com.shifthackz.aisdv1.presentation.screen.gallery.detail.GalleryDetailIntent import com.shifthackz.aisdv1.presentation.screen.gallery.list.GalleryIntent import com.shifthackz.aisdv1.presentation.screen.inpaint.InPaintIntent @@ -57,6 +59,7 @@ fun ModalRenderer( processIntent(GalleryIntent.DismissDialog) processIntent(GalleryDetailIntent.DismissDialog) processIntent(InPaintIntent.ScreenModal.Dismiss) + processIntent(DebugMenuIntent.DismissModal) } val context = LocalContext.current when (screenModal) { @@ -93,6 +96,13 @@ fun ModalRenderer( titleResId = LocalizationR.string.communicating_local_title, canDismiss = false, step = screenModal.pair, + content = screenModal.canCancel.takeIf { it }?.let { + { + ProgressDialogCancelButton { + processIntent(GenerationMviIntent.Cancel.Generation) + } + } + }, ) is Modal.Image.Single -> GenerationImageResultDialog( @@ -332,5 +342,18 @@ fun ModalRenderer( } ) } + + is Modal.LDScheduler -> ModalBottomSheet( + onDismissRequest = dismiss, + shape = RectangleShape, + ) { + LDSchedulerBottomSheet( + currentScheduler = screenModal.scheduler, + onSelected = { + processIntent(DebugMenuIntent.LocalDiffusionScheduler.Confirm(it)) + dismiss() + } + ) + } } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ldscheduler/LDSchedulerBottomSheer.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ldscheduler/LDSchedulerBottomSheer.kt new file mode 100644 index 00000000..8d5b49eb --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ldscheduler/LDSchedulerBottomSheer.kt @@ -0,0 +1,44 @@ +package com.shifthackz.aisdv1.presentation.modal.ldscheduler + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Construction +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.shifthackz.aisdv1.core.common.schedulers.SchedulersToken +import com.shifthackz.aisdv1.presentation.screen.debug.mapToUi +import com.shifthackz.aisdv1.presentation.widget.item.SettingsItem + +@Composable +@Preview +fun LDSchedulerBottomSheet( + modifier: Modifier = Modifier, + currentScheduler: SchedulersToken = SchedulersToken.COMPUTATION, + onSelected: (SchedulersToken) -> Unit = {}, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .navigationBarsPadding() + .padding(bottom = 16.dp), + ) { + SchedulersToken.entries.forEach { scheduler -> + SettingsItem( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + selected = scheduler == currentScheduler, + text = scheduler.mapToUi(), + showChevron = false, + onClick = { onSelected(scheduler) }, + startIcon = Icons.Default.Construction, + ) + } + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/Modal.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/Modal.kt index 3951ed0c..0e53eb82 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/Modal.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/Modal.kt @@ -2,6 +2,7 @@ package com.shifthackz.aisdv1.presentation.model import android.graphics.Bitmap import androidx.compose.runtime.Immutable +import com.shifthackz.aisdv1.core.common.schedulers.SchedulersToken import com.shifthackz.aisdv1.core.model.UiText import com.shifthackz.aisdv1.domain.entity.AiGenerationResult import com.shifthackz.aisdv1.domain.entity.Grid @@ -37,7 +38,10 @@ sealed interface Modal { data class SelectSdModel(val models: List, val selected: String) : Modal @Immutable - data class Generating(val status: LocalDiffusion.Status? = null) : Modal { + data class Generating( + val canCancel: Boolean = false, + val status: LocalDiffusion.Status? = null, + ) : Modal { val pair: Pair? get() = status?.let { (current, total) -> current to total } } @@ -104,5 +108,7 @@ sealed interface Modal { data object Language : Modal + data class LDScheduler(val scheduler: SchedulersToken) : Modal + data class GalleryGrid(val grid: Grid) : Modal } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/DrawerNavGraph.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/DrawerNavGraph.kt index c0f92edc..b5f0bfbe 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/DrawerNavGraph.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/DrawerNavGraph.kt @@ -1,6 +1,7 @@ package com.shifthackz.aisdv1.presentation.navigation.graph import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DeveloperMode import androidx.compose.material.icons.filled.SettingsEthernet import androidx.compose.material.icons.filled.Web import com.shifthackz.aisdv1.core.model.UiText @@ -23,6 +24,9 @@ fun mainDrawerNavItems(settings: Settings? = null): List = buildList { } add(settingsTab()) add(configuration()) + settings?.developerMode?.takeIf { it }?.let { + add(developerMode()) + } } private fun webUi(source: ServerSource) = NavItem( @@ -45,3 +49,11 @@ private fun configuration() = NavItem( vector = Icons.Default.SettingsEthernet, ), ) + +private fun developerMode() = NavItem( + name = LocalizationR.string.title_debug_menu.asUiText(), + route = Constants.ROUTE_DEBUG, + icon = NavItem.Icon.Vector( + vector = Icons.Default.DeveloperMode, + ) +) diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/MainNavGraph.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/MainNavGraph.kt index a654de47..ef8c49a4 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/MainNavGraph.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/MainNavGraph.kt @@ -10,6 +10,7 @@ import com.shifthackz.aisdv1.presentation.screen.donate.DonateScreen import com.shifthackz.aisdv1.presentation.screen.gallery.detail.GalleryDetailScreen import com.shifthackz.aisdv1.presentation.screen.inpaint.InPaintScreen import com.shifthackz.aisdv1.presentation.screen.loader.ConfigurationLoaderScreen +import com.shifthackz.aisdv1.presentation.screen.logger.LoggerScreen import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupScreen import com.shifthackz.aisdv1.presentation.screen.splash.SplashScreen @@ -65,6 +66,13 @@ fun NavGraphBuilder.mainNavGraph() { route = Constants.ROUTE_DEBUG } ) + addDestination( + ComposeNavigator.Destination(provider[ComposeNavigator::class]) { + LoggerScreen() + }.apply { + route = Constants.ROUTE_LOGGER + } + ) addDestination( ComposeNavigator.Destination(provider[ComposeNavigator::class]) { InPaintScreen() diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouter.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouter.kt index f0b70dd9..7f64a566 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouter.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouter.kt @@ -21,4 +21,6 @@ interface MainRouter : Router { fun navigateToDonate() fun navigateToDebugMenu() + + fun navigateToLogger() } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImpl.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImpl.kt index 873874a2..24a8c52c 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImpl.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImpl.kt @@ -56,8 +56,10 @@ internal class MainRouterImpl( } override fun navigateToDebugMenu() { - if (debugMenuAccessor()) { - effectSubject.onNext(NavigationEffect.Navigate.Route(Constants.ROUTE_DEBUG)) - } + effectSubject.onNext(NavigationEffect.Navigate.Route(Constants.ROUTE_DEBUG)) + } + + override fun navigateToLogger() { + effectSubject.onNext(NavigationEffect.Navigate.Route(Constants.ROUTE_LOGGER)) } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuAccessor.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuAccessor.kt index 5681de85..b080c44b 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuAccessor.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuAccessor.kt @@ -1,19 +1,23 @@ package com.shifthackz.aisdv1.presentation.screen.debug -import com.shifthackz.aisdv1.core.common.appbuild.BuildInfoProvider +import com.shifthackz.aisdv1.domain.preference.PreferenceManager import com.shifthackz.aisdv1.presentation.utils.Constants -class DebugMenuAccessor(private val buildInfoProvider: BuildInfoProvider) { +class DebugMenuAccessor( + private val preferenceManager: PreferenceManager, +) { private var tapCount = 0; operator fun invoke(): Boolean { - if (buildInfoProvider.isDebug) { - tapCount++ - if (tapCount >= Constants.DEBUG_MENU_ACCESS_TAPS) { - tapCount = 0; - return true - } + if (preferenceManager.developerMode) { + return true + } + tapCount++ + if (tapCount >= Constants.DEBUG_MENU_ACCESS_TAPS) { + tapCount = 0 + preferenceManager.developerMode = true + return true } return false } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuEffect.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuEffect.kt new file mode 100644 index 00000000..132c6522 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuEffect.kt @@ -0,0 +1,8 @@ +package com.shifthackz.aisdv1.presentation.screen.debug + +import com.shifthackz.aisdv1.core.model.UiText +import com.shifthackz.android.core.mvi.MviEffect + +sealed interface DebugMenuEffect : MviEffect { + data class Message(val message: UiText) : DebugMenuEffect +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuIntent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuIntent.kt index 22c7e1f3..dae80520 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuIntent.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuIntent.kt @@ -1,8 +1,30 @@ package com.shifthackz.aisdv1.presentation.screen.debug +import com.shifthackz.aisdv1.core.common.schedulers.SchedulersToken import com.shifthackz.android.core.mvi.MviIntent -enum class DebugMenuIntent : MviIntent { - NavigateBack, - InsertBadBase64; +sealed interface DebugMenuIntent : MviIntent { + + data object NavigateBack : DebugMenuIntent + + data object ViewLogs : DebugMenuIntent + + data object ClearLogs : DebugMenuIntent + + data object AllowLocalDiffusionCancel : DebugMenuIntent + + data object InsertBadBase64 : DebugMenuIntent + + sealed interface LocalDiffusionScheduler : DebugMenuIntent { + + data class Confirm(val token: SchedulersToken) : DebugMenuIntent + + data object Request : DebugMenuIntent + } + + enum class WorkManager : DebugMenuIntent { + CancelAll, RestartTxt2Img, RestartImg2Img; + } + + data object DismissModal : DebugMenuIntent } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuMappers.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuMappers.kt new file mode 100644 index 00000000..3d2c705f --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuMappers.kt @@ -0,0 +1,13 @@ +package com.shifthackz.aisdv1.presentation.screen.debug + +import com.shifthackz.aisdv1.core.common.schedulers.SchedulersToken +import com.shifthackz.aisdv1.core.model.UiText +import com.shifthackz.aisdv1.core.model.asUiText +import com.shifthackz.aisdv1.core.localization.R as LocalizationR + +fun SchedulersToken.mapToUi(): UiText = when (this) { + SchedulersToken.MAIN_THREAD -> LocalizationR.string.scheduler_main + SchedulersToken.IO_THREAD -> LocalizationR.string.scheduler_io + SchedulersToken.COMPUTATION -> LocalizationR.string.scheduler_computation + SchedulersToken.SINGLE_THREAD -> LocalizationR.string.scheduler_single_thread +}.asUiText() diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuScreen.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuScreen.kt index 06cded3f..24776091 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuScreen.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuScreen.kt @@ -9,7 +9,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.TextSnippet import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material.icons.filled.CancelScheduleSend +import androidx.compose.material.icons.filled.CleaningServices +import androidx.compose.material.icons.filled.Construction +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.SettingsEthernet import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api @@ -17,25 +23,39 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.shifthackz.aisdv1.core.common.extensions.showToast import com.shifthackz.aisdv1.core.model.asUiText import com.shifthackz.aisdv1.core.ui.MviComponent +import com.shifthackz.aisdv1.presentation.modal.ModalRenderer +import com.shifthackz.aisdv1.presentation.widget.item.SettingsHeader import com.shifthackz.aisdv1.presentation.widget.item.SettingsItem import org.koin.androidx.compose.koinViewModel import com.shifthackz.aisdv1.core.localization.R as LocalizationR @Composable fun DebugMenuScreen() { + val context = LocalContext.current MviComponent( viewModel = koinViewModel(), - ) { _, intentHandler -> + processEffect = { effect -> + when (effect) { + is DebugMenuEffect.Message -> context.showToast( + effect.message.asString(context) + ) + } + } + ) { state, intentHandler -> ScreenContent( modifier = Modifier.fillMaxSize(), + state = state, processIntent = intentHandler, ) } @@ -44,6 +64,7 @@ fun DebugMenuScreen() { @Composable private fun ScreenContent( modifier: Modifier = Modifier, + state: DebugMenuState = DebugMenuState(), processIntent: (DebugMenuIntent) -> Unit = {}, ) { Scaffold( @@ -81,10 +102,74 @@ private fun ScreenContent( .fillMaxWidth() .padding(bottom = 8.dp) - Text( + SettingsHeader( modifier = headerModifier, - text = stringResource(id = LocalizationR.string.debug_section_qa), - style = MaterialTheme.typography.headlineSmall, + text = LocalizationR.string.debug_section_main.asUiText(), + ) + SettingsItem( + modifier = itemModifier, + startIcon = Icons.AutoMirrored.Filled.TextSnippet, + text = LocalizationR.string.debug_action_logger.asUiText(), + onClick = { processIntent(DebugMenuIntent.ViewLogs) }, + ) + SettingsItem( + modifier = itemModifier, + startIcon = Icons.Default.CleaningServices, + text = LocalizationR.string.debug_action_logger_clear.asUiText(), + onClick = { processIntent(DebugMenuIntent.ClearLogs) }, + ) + + SettingsHeader( + modifier = headerModifier, + text = LocalizationR.string.debug_section_work_manager.asUiText(), + ) + SettingsItem( + modifier = itemModifier, + startIcon = Icons.Default.Refresh, + text = LocalizationR.string.debug_action_work_restart_txt2img.asUiText(), + onClick = { processIntent(DebugMenuIntent.WorkManager.RestartTxt2Img) }, + ) + SettingsItem( + modifier = itemModifier, + startIcon = Icons.Default.Refresh, + text = LocalizationR.string.debug_action_work_restart_img2img.asUiText(), + onClick = { processIntent(DebugMenuIntent.WorkManager.RestartImg2Img) }, + ) + SettingsItem( + modifier = itemModifier, + startIcon = Icons.Default.Cancel, + text = LocalizationR.string.debug_action_work_cancel_all.asUiText(), + onClick = { processIntent(DebugMenuIntent.WorkManager.CancelAll) }, + ) + + SettingsHeader( + modifier = headerModifier, + text = LocalizationR.string.debug_section_ld.asUiText(), + ) + SettingsItem( + modifier = itemModifier, + startIcon = Icons.Default.CancelScheduleSend, + text = LocalizationR.string.debug_action_ld_allow_cancel.asUiText(), + onClick = { processIntent(DebugMenuIntent.AllowLocalDiffusionCancel) }, + endValueContent = { + Switch( + modifier = Modifier.padding(horizontal = 8.dp), + checked = state.localDiffusionAllowCancel, + onCheckedChange = { processIntent(DebugMenuIntent.AllowLocalDiffusionCancel) }, + ) + } + ) + SettingsItem( + modifier = itemModifier, + startIcon = Icons.Default.Construction, + text = LocalizationR.string.debug_action_ld_scheduler.asUiText(), + onClick = { processIntent(DebugMenuIntent.LocalDiffusionScheduler.Request) }, + endValueText = state.localDiffusionSchedulerThread.mapToUi(), + ) + + SettingsHeader( + modifier = headerModifier, + text = LocalizationR.string.debug_section_qa.asUiText(), ) SettingsItem( modifier = itemModifier, @@ -93,6 +178,9 @@ private fun ScreenContent( onClick = { processIntent(DebugMenuIntent.InsertBadBase64) }, ) } + ModalRenderer(screenModal = state.screenModal) { + (it as? DebugMenuIntent)?.let(processIntent::invoke) + } } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuState.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuState.kt new file mode 100644 index 00000000..20fa36e4 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuState.kt @@ -0,0 +1,11 @@ +package com.shifthackz.aisdv1.presentation.screen.debug + +import com.shifthackz.aisdv1.core.common.schedulers.SchedulersToken +import com.shifthackz.aisdv1.presentation.model.Modal +import com.shifthackz.android.core.mvi.MviState + +data class DebugMenuState( + val screenModal: Modal = Modal.None, + val localDiffusionAllowCancel: Boolean = false, + val localDiffusionSchedulerThread: SchedulersToken = SchedulersToken.COMPUTATION, +) : MviState diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuViewModel.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuViewModel.kt index fbcbacd3..9336acaf 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuViewModel.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuViewModel.kt @@ -1,29 +1,105 @@ package com.shifthackz.aisdv1.presentation.screen.debug +import com.shifthackz.aisdv1.core.common.file.FileProviderDescriptor +import com.shifthackz.aisdv1.core.common.log.FileLoggingTree import com.shifthackz.aisdv1.core.common.log.errorLog import com.shifthackz.aisdv1.core.common.schedulers.SchedulersProvider import com.shifthackz.aisdv1.core.common.schedulers.subscribeOnMainThread +import com.shifthackz.aisdv1.core.model.asUiText import com.shifthackz.aisdv1.core.viewmodel.MviRxViewModel +import com.shifthackz.aisdv1.domain.feature.work.BackgroundTaskManager +import com.shifthackz.aisdv1.domain.preference.PreferenceManager import com.shifthackz.aisdv1.domain.usecase.debug.DebugInsertBadBase64UseCase +import com.shifthackz.aisdv1.presentation.model.Modal import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter -import com.shifthackz.android.core.mvi.EmptyEffect -import com.shifthackz.android.core.mvi.EmptyState import io.reactivex.rxjava3.kotlin.subscribeBy +import com.shifthackz.aisdv1.core.localization.R as LocalizationR class DebugMenuViewModel( + private val preferenceManager: PreferenceManager, + private val fileProviderDescriptor: FileProviderDescriptor, private val debugInsertBadBase64UseCase: DebugInsertBadBase64UseCase, private val schedulersProvider: SchedulersProvider, private val mainRouter: MainRouter, -) : MviRxViewModel() { + private val backgroundTaskManager: BackgroundTaskManager, +) : MviRxViewModel() { - override val initialState = EmptyState + override val initialState = DebugMenuState() + + init { + !preferenceManager + .observe() + .subscribeOnMainThread(schedulersProvider) + .subscribeBy(::errorLog) { settings -> + updateState { state -> + state.copy( + localDiffusionAllowCancel = settings.localDiffusionAllowCancel, + localDiffusionSchedulerThread = settings.localDiffusionSchedulerThread, + ) + } + } + } override fun processIntent(intent: DebugMenuIntent) { when (intent) { DebugMenuIntent.NavigateBack -> mainRouter.navigateBack() + DebugMenuIntent.InsertBadBase64 -> !debugInsertBadBase64UseCase() .subscribeOnMainThread(schedulersProvider) - .subscribeBy(::errorLog) + .subscribeBy(::onError, ::onSuccess) + + DebugMenuIntent.ClearLogs -> { + try { + FileLoggingTree.clearLog(fileProviderDescriptor) + onSuccess() + } catch (e: Exception) { + onError(e) + } + } + + DebugMenuIntent.ViewLogs -> mainRouter.navigateToLogger() + + DebugMenuIntent.AllowLocalDiffusionCancel -> { + preferenceManager.localDiffusionAllowCancel = !currentState.localDiffusionAllowCancel + } + + DebugMenuIntent.LocalDiffusionScheduler.Request -> updateState { + it.copy(screenModal = Modal.LDScheduler(it.localDiffusionSchedulerThread)) + } + + is DebugMenuIntent.LocalDiffusionScheduler.Confirm -> { + preferenceManager.localDiffusionSchedulerThread = intent.token + } + + DebugMenuIntent.DismissModal -> updateState { + it.copy(screenModal = Modal.None) + } + + DebugMenuIntent.WorkManager.CancelAll -> backgroundTaskManager + .cancelAll() + .handleState() + + DebugMenuIntent.WorkManager.RestartTxt2Img -> backgroundTaskManager + .retryLastTextToImageTask() + .handleState() + + DebugMenuIntent.WorkManager.RestartImg2Img -> backgroundTaskManager + .retryLastImageToImageTask() + .handleState() } } + + private fun Result.handleState() = this.fold( + onSuccess = { onSuccess() }, + onFailure = ::onError, + ) + + private fun onSuccess() { + emitEffect(DebugMenuEffect.Message(LocalizationR.string.success.asUiText())) + } + + private fun onError(t: Throwable) { + errorLog(t) + emitEffect(DebugMenuEffect.Message(LocalizationR.string.failure.asUiText())) + } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerIntent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerIntent.kt new file mode 100644 index 00000000..a1fc441c --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerIntent.kt @@ -0,0 +1,10 @@ +package com.shifthackz.aisdv1.presentation.screen.logger + +import com.shifthackz.android.core.mvi.MviIntent + +sealed interface LoggerIntent : MviIntent { + + data object ReadLogs : LoggerIntent + + data object NavigateBack : LoggerIntent +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerScreen.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerScreen.kt new file mode 100644 index 00000000..def1e935 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerScreen.kt @@ -0,0 +1,202 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package com.shifthackz.aisdv1.presentation.screen.logger + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.filled.ArrowDownward +import androidx.compose.material.icons.filled.ArrowUpward +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.shifthackz.aisdv1.core.ui.MviComponent +import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import com.shifthackz.aisdv1.core.localization.R as LocalizationR + +@Composable +fun LoggerScreen() { + MviComponent( + viewModel = koinViewModel(), + ) { state, processIntent -> + LoggerScreenContent( + state = state, + processIntent = processIntent, + ) + } +} + +@Composable +@Preview +private fun LoggerScreenContent( + state: LoggerState = LoggerState(), + processIntent: (LoggerIntent) -> Unit = {}, +) { + val scrollState = rememberScrollState() + val scope = rememberCoroutineScope() + Scaffold( + topBar = { + CenterAlignedTopAppBar( + navigationIcon = { + IconButton( + onClick = { processIntent(LoggerIntent.NavigateBack) }, + content = { + Icon( + Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = "Back button", + ) + }, + ) + }, + title = { + Text( + text = stringResource(id = LocalizationR.string.title_debug_logger), + style = MaterialTheme.typography.headlineMedium, + ) + }, + actions = { + AnimatedVisibility( + visible = !state.loading, + enter = fadeIn(), + exit = fadeOut(), + ) { + IconButton( + onClick = { + processIntent(LoggerIntent.ReadLogs) + }, + content = { + Icon( + Icons.Default.Refresh, + contentDescription = "Refresh", + ) + }, + ) + } + } + ) + }, + bottomBar = { + AnimatedVisibility( + visible = !state.loading && state.text.isNotBlank(), + enter = fadeIn(), + exit = fadeOut(), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End), + ) { + IconButton( + onClick = { + scope.launch { + scrollState.animateScrollTo(0) + } + }, + content = { + Icon( + Icons.Default.ArrowUpward, + contentDescription = "Down", + ) + }, + ) + IconButton( + onClick = { + scope.launch { + scrollState.animateScrollTo(scrollState.maxValue) + } + }, + content = { + Icon( + Icons.Default.ArrowDownward, + contentDescription = "Down", + ) + }, + ) + } + } + + } + ) { paddingValues -> + val text = if (!state.loading) state.text else "" + val scrollStateHorizontal = rememberScrollState() + if (!state.loading && state.text.isBlank()) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(id = LocalizationR.string.debug_logger_empty), + textAlign = TextAlign.Center, + ) + } + } + Column( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + .verticalScroll(scrollState), + ) { + AnimatedVisibility( + visible = state.loading, + enter = fadeIn(), + exit = fadeOut(), + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + modifier = Modifier + .size(60.dp) + .aspectRatio(1f), + ) + } + } + Text( + modifier = Modifier.horizontalScroll(scrollStateHorizontal), + text = text, + fontFamily = FontFamily.Monospace, + fontSize = 11.sp, + lineHeight = 12.sp, + ) + } + LaunchedEffect(state.text) { + if (!state.loading) { + scrollState.scrollTo(scrollState.maxValue) + } + } + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerState.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerState.kt new file mode 100644 index 00000000..0571411a --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerState.kt @@ -0,0 +1,10 @@ +package com.shifthackz.aisdv1.presentation.screen.logger + +import androidx.compose.runtime.Immutable +import com.shifthackz.android.core.mvi.MviState + +@Immutable +data class LoggerState( + val loading: Boolean = true, + val text: String = "", +) : MviState diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerViewModel.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerViewModel.kt new file mode 100644 index 00000000..21e503f3 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerViewModel.kt @@ -0,0 +1,47 @@ +package com.shifthackz.aisdv1.presentation.screen.logger + +import com.shifthackz.aisdv1.core.common.file.FileProviderDescriptor +import com.shifthackz.aisdv1.core.common.log.FileLoggingTree +import com.shifthackz.aisdv1.core.viewmodel.MviRxViewModel +import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter +import com.shifthackz.android.core.mvi.EmptyEffect +import java.io.File + +class LoggerViewModel( + private val fileProviderDescriptor: FileProviderDescriptor, + private val mainRouter: MainRouter, +) : MviRxViewModel() { + + override val initialState = LoggerState() + + init { + readLogs() + } + + override fun processIntent(intent: LoggerIntent) { + when (intent) { + LoggerIntent.ReadLogs -> readLogs() + LoggerIntent.NavigateBack -> mainRouter.navigateBack() + } + } + + private fun readLogs() { + updateState { it.copy(loading = true, text = "") } + try { + val logFile = File( + fileProviderDescriptor.logsCacheDirPath + + "/" + + FileLoggingTree.LOGGER_FILENAME + ) + val content = logFile.readText() + updateState { + it.copy( + loading = false, + text = content, + ) + } + } catch (e: Exception) { + updateState { it.copy(loading = false) } + } + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsEffect.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsEffect.kt index 664c0cf5..f43aeca4 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsEffect.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsEffect.kt @@ -11,5 +11,7 @@ sealed interface SettingsEffect : MviEffect { data object ShareLogFile : SettingsEffect + data object DeveloperModeUnlocked : SettingsEffect + data class OpenUrl(val url: String) : SettingsEffect } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsIntent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsIntent.kt index ff8b0fbc..be85a4fb 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsIntent.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsIntent.kt @@ -13,6 +13,8 @@ sealed interface SettingsIntent : MviIntent { data object NavigateConfiguration : SettingsIntent + data object NavigateDeveloperMode : SettingsIntent + sealed interface SdModel : SettingsIntent { data object OpenChooser : SdModel diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsScreen.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsScreen.kt index de5bac26..f4757741 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsScreen.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.ColorLens import androidx.compose.material.icons.filled.DarkMode import androidx.compose.material.icons.filled.DeleteForever +import androidx.compose.material.icons.filled.DeveloperMode import androidx.compose.material.icons.filled.DynamicForm import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.FormatColorFill @@ -60,6 +61,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.shifthackz.aisdv1.core.common.extensions.openUrl +import com.shifthackz.aisdv1.core.common.extensions.showToast import com.shifthackz.aisdv1.core.common.math.roundTo import com.shifthackz.aisdv1.core.model.UiText import com.shifthackz.aisdv1.core.model.asUiText @@ -115,6 +117,9 @@ fun SettingsScreen() { } SettingsEffect.ShareLogFile -> ReportProblemEmailComposer().invoke(context) is SettingsEffect.OpenUrl -> context.openUrl(effect.url) + SettingsEffect.DeveloperModeUnlocked -> context.showToast( + LocalizationR.string.debug_action_unlock, + ) } }, applySystemUiColors = false, @@ -297,6 +302,15 @@ private fun ContentSettingsState( style = MaterialTheme.typography.labelMedium, ) } + AnimatedVisibility(visible = !state.loading && state.developerMode) { + SettingsItem( + modifier = itemModifier, + loading = state.loading, + startIcon = Icons.Default.DeveloperMode, + text = LocalizationR.string.title_debug_menu.asUiText(), + onClick = { processIntent(SettingsIntent.NavigateDeveloperMode) }, + ) + } //endregion //region APP SETTINGS diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsState.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsState.kt index e8bb7490..432ec831 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsState.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsState.kt @@ -29,6 +29,7 @@ data class SettingsState( val colorToken: ColorToken = ColorToken.MAUVE, val darkThemeToken: DarkThemeToken = DarkThemeToken.FRAPPE, val galleryGrid: Grid = Grid.Fixed2, + val developerMode: Boolean = false, val appVersion: String = "", ) : MviState { diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModel.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModel.kt index d6bd101b..ff9c9cd7 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModel.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModel.kt @@ -19,6 +19,7 @@ import com.shifthackz.aisdv1.domain.usecase.stabilityai.ObserveStabilityAiCredit import com.shifthackz.aisdv1.presentation.model.Modal import com.shifthackz.aisdv1.presentation.navigation.router.drawer.DrawerRouter import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter +import com.shifthackz.aisdv1.presentation.screen.debug.DebugMenuAccessor import com.shifthackz.aisdv1.presentation.screen.drawer.DrawerIntent import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import io.reactivex.rxjava3.core.Flowable @@ -32,6 +33,7 @@ class SettingsViewModel( private val clearAppCacheUseCase: ClearAppCacheUseCase, private val schedulersProvider: SchedulersProvider, private val preferenceManager: PreferenceManager, + private val debugMenuAccessor: DebugMenuAccessor, private val buildInfoProvider: BuildInfoProvider, private val mainRouter: MainRouter, private val drawerRouter: DrawerRouter, @@ -80,6 +82,7 @@ class SettingsViewModel( colorToken = ColorToken.parse(settings.designColorToken), darkThemeToken = DarkThemeToken.parse(settings.designDarkThemeToken), galleryGrid = settings.galleryGrid, + developerMode = settings.developerMode, appVersion = version, ) } @@ -89,7 +92,9 @@ class SettingsViewModel( override fun processIntent(intent: SettingsIntent) { when (intent) { - SettingsIntent.Action.AppVersion -> mainRouter.navigateToDebugMenu() + SettingsIntent.Action.AppVersion -> if (debugMenuAccessor()) { + emitEffect(SettingsEffect.DeveloperModeUnlocked) + } SettingsIntent.Action.ClearAppCache.Request -> updateState { it.copy(screenModal = Modal.ClearAppCache) @@ -107,6 +112,8 @@ class SettingsViewModel( ServerSetupLaunchSource.SETTINGS ) + SettingsIntent.NavigateDeveloperMode -> mainRouter.navigateToDebugMenu() + SettingsIntent.SdModel.OpenChooser -> updateState { it.copy( screenModal = Modal.SelectSdModel( diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageViewModel.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageViewModel.kt index dbf31dad..6f98575f 100755 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageViewModel.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageViewModel.kt @@ -66,7 +66,7 @@ class TextToImageViewModel( private val progressModal: Modal get() { if (currentState.mode == ServerSource.LOCAL) { - return Modal.Generating() + return Modal.Generating(canCancel = preferenceManager.localDiffusionAllowCancel) } return Modal.Communicating() } @@ -126,14 +126,14 @@ class TextToImageViewModel( } override fun onReceivedHordeStatus(status: HordeProcessStatus) { - if (currentState.screenModal is Modal.Communicating) { - setActiveModal(Modal.Communicating(hordeProcessStatus = status)) - } + (currentState.screenModal as? Modal.Communicating) + ?.copy(hordeProcessStatus = status) + ?.let(::setActiveModal) } override fun onReceivedLocalDiffusionStatus(status: LocalDiffusion.Status) { - if (currentState.screenModal is Modal.Generating) { - setActiveModal(Modal.Generating(status)) - } + (currentState.screenModal as? Modal.Generating) + ?.copy(status = status) + ?.let(::setActiveModal) } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/utils/Constants.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/utils/Constants.kt index 43f101f0..ad1e0095 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/utils/Constants.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/utils/Constants.kt @@ -20,6 +20,7 @@ object Constants { const val ROUTE_GALLERY_DETAIL_FULL = "$ROUTE_GALLERY_DETAIL/{$PARAM_ITEM_ID}" const val ROUTE_SETTINGS = "settings" const val ROUTE_DEBUG = "debug" + const val ROUTE_LOGGER = "logger" const val ROUTE_IN_PAINT = "in_paint" const val ROUTE_DONATE = "donate" diff --git a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImplTest.kt b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImplTest.kt index 64527033..ecec9c5c 100644 --- a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImplTest.kt +++ b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImplTest.kt @@ -1,21 +1,26 @@ package com.shifthackz.aisdv1.presentation.navigation.router.main -import com.shifthackz.aisdv1.core.common.appbuild.BuildInfoProvider +import com.shifthackz.aisdv1.domain.preference.PreferenceManager import com.shifthackz.aisdv1.presentation.navigation.NavigationEffect import com.shifthackz.aisdv1.presentation.screen.debug.DebugMenuAccessor import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import com.shifthackz.aisdv1.presentation.utils.Constants -import io.mockk.every import io.mockk.mockk +import org.junit.Before import org.junit.Test class MainRouterImplTest { - private val stubBuildInfoProvider = mockk() - private val stubDebugMenuAccessor = DebugMenuAccessor(stubBuildInfoProvider) + private val stubPreferenceManager = mockk() + private val stubDebugMenuAccessor = DebugMenuAccessor(stubPreferenceManager) private val router = MainRouterImpl(stubDebugMenuAccessor) + @Before + fun initialize() { + + } + @Test fun `given user navigates back, expected router emits Back event`() { router @@ -105,63 +110,14 @@ class MainRouterImplTest { ) } - @Test - fun `given user tapped hidden menu 6 times, build is debuggable, expected router emits no events`() { - every { - stubBuildInfoProvider.isDebug - } returns true - - val stubObserver = router.observe().test() - - repeat(6) { router.navigateToDebugMenu() } - - stubObserver - .assertNoErrors() - .assertNoValues() - } - @Test fun `given user tapped hidden menu 7 times, build is debuggable, expected router emits Route event with ROUTE_DEBUG route`() { - every { - stubBuildInfoProvider.isDebug - } returns true - val stubObserver = router.observe().test() - repeat(7) { router.navigateToDebugMenu() } + router.navigateToDebugMenu() stubObserver .assertNoErrors() .assertValueAt(0, NavigationEffect.Navigate.Route(Constants.ROUTE_DEBUG)) } - - @Test - fun `given user tapped hidden menu 6 times, build is NOT debuggable, expected router emits no events`() { - every { - stubBuildInfoProvider.isDebug - } returns false - - val stubObserver = router.observe().test() - - repeat(6) { router.navigateToDebugMenu() } - - stubObserver - .assertNoErrors() - .assertNoValues() - } - - @Test - fun `given user tapped hidden menu 7 times, build is NOT debuggable, expected router emits no events`() { - every { - stubBuildInfoProvider.isDebug - } returns false - - val stubObserver = router.observe().test() - - repeat(7) { router.navigateToDebugMenu() } - - stubObserver - .assertNoErrors() - .assertNoValues() - } } diff --git a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuViewModelTest.kt b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuViewModelTest.kt index e501b0f3..4175f057 100644 --- a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuViewModelTest.kt +++ b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/debug/DebugMenuViewModelTest.kt @@ -1,5 +1,9 @@ package com.shifthackz.aisdv1.presentation.screen.debug +import com.shifthackz.aisdv1.core.common.file.FileProviderDescriptor +import com.shifthackz.aisdv1.domain.entity.Settings +import com.shifthackz.aisdv1.domain.feature.work.BackgroundTaskManager +import com.shifthackz.aisdv1.domain.preference.PreferenceManager import com.shifthackz.aisdv1.domain.usecase.debug.DebugInsertBadBase64UseCase import com.shifthackz.aisdv1.presentation.core.CoreViewModelTest import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter @@ -8,19 +12,36 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Flowable +import org.junit.Before import org.junit.Test class DebugMenuViewModelTest : CoreViewModelTest() { + private val stubFileProviderDescriptor = mockk() private val stubDebugInsertBadBase64UseCase = mockk() private val stubMainRouter = mockk() + private val stubPreferenceManager = mockk() + private val stubBackgroundTaskManager = mockk() override fun initializeViewModel() = DebugMenuViewModel( + preferenceManager = stubPreferenceManager, + fileProviderDescriptor = stubFileProviderDescriptor, debugInsertBadBase64UseCase = stubDebugInsertBadBase64UseCase, schedulersProvider = stubSchedulersProvider, mainRouter = stubMainRouter, + backgroundTaskManager = stubBackgroundTaskManager, ) + @Before + override fun initialize() { + super.initialize() + + every { + stubPreferenceManager.observe() + } returns Flowable.just(Settings()) + } + @Test fun `given received NavigateBack intent, expected router navigateBack() method called`() { every { diff --git a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/donate/DonateViewModelTest.kt b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/donate/DonateViewModelTest.kt index 7859cc9a..218cd875 100644 --- a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/donate/DonateViewModelTest.kt +++ b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/donate/DonateViewModelTest.kt @@ -9,8 +9,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import io.reactivex.rxjava3.core.Single -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.test.runTest import org.junit.Assert import org.junit.Test @@ -32,14 +30,12 @@ class DonateViewModelTest : CoreViewModelTest() { stubFetchAndGetSupportersUseCase() } returns Single.just(mockSupporters) - runTest { - val expected = DonateState( - loading = false, - supporters = mockSupporters, - ) - val actual = viewModel.state.value - Assert.assertEquals(expected, actual) - } + val expected = DonateState( + loading = false, + supporters = mockSupporters, + ) + val actual = viewModel.state.value + Assert.assertEquals(expected, actual) } @Test @@ -48,14 +44,13 @@ class DonateViewModelTest : CoreViewModelTest() { stubFetchAndGetSupportersUseCase() } returns Single.error(stubException) - runTest { - val expected = DonateState( - loading = false, - supporters = emptyList(), - ) - val actual = viewModel.state.value - Assert.assertEquals(expected, actual) - } + val expected = DonateState( + loading = false, + supporters = emptyList(), + ) + val actual = viewModel.state.value + Assert.assertEquals(expected, actual) + } @Test @@ -74,24 +69,4 @@ class DonateViewModelTest : CoreViewModelTest() { stubMainRouter.navigateBack() } } - - @Test - fun `given received LaunchUrl intent, expected OpenUrl effect delivered to effect collector`() { - every { - stubFetchAndGetSupportersUseCase() - } returns Single.just(mockSupporters) - - val intent = mockk() - every { - intent::url.get() - } returns "https://5598.is.my.favourite.com" - - viewModel.processIntent(intent) - - runTest { - val expected = DonateEffect.OpenUrl("https://5598.is.my.favourite.com") - val actual = viewModel.effect.firstOrNull() - Assert.assertEquals(expected, actual) - } - } } diff --git a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerViewModelTest.kt b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerViewModelTest.kt new file mode 100644 index 00000000..2840ba85 --- /dev/null +++ b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/logger/LoggerViewModelTest.kt @@ -0,0 +1,56 @@ +package com.shifthackz.aisdv1.presentation.screen.logger + +import com.shifthackz.aisdv1.core.common.file.FileProviderDescriptor +import com.shifthackz.aisdv1.presentation.core.CoreViewModelTest +import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +class LoggerViewModelTest : CoreViewModelTest() { + + private val stubFileProviderDescriptor = mockk() + private val stubMainRouter = mockk() + + override fun initializeViewModel() = LoggerViewModel( + fileProviderDescriptor = stubFileProviderDescriptor, + mainRouter = stubMainRouter, + ) + + @Before + override fun initialize() { + super.initialize() + every { + stubFileProviderDescriptor.logsCacheDirPath + } returns "/tmp/local" + } + + @Test + fun `initialize, read logs, expected loaded state`() { + runTest { + val expected = LoggerState( + loading = false, + text = "" + ) + val actual = viewModel.state.value + Assert.assertEquals(expected, actual) + } + } + + @Test + fun `given received NavigateBack intent, expected router navigateBack() called`() { + every { + stubMainRouter.navigateBack() + } returns Unit + + viewModel.processIntent(LoggerIntent.NavigateBack) + + verify { + stubMainRouter.navigateBack() + } + } +} diff --git a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModelTest.kt b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModelTest.kt index 2b93515e..5aa56c54 100644 --- a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModelTest.kt +++ b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModelTest.kt @@ -16,6 +16,7 @@ import com.shifthackz.aisdv1.presentation.mocks.mockStableDiffusionModels import com.shifthackz.aisdv1.presentation.model.Modal import com.shifthackz.aisdv1.presentation.navigation.router.drawer.DrawerRouter import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter +import com.shifthackz.aisdv1.presentation.screen.debug.DebugMenuAccessor import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import com.shifthackz.aisdv1.presentation.stub.stubSchedulersProvider import io.mockk.every @@ -46,6 +47,7 @@ class SettingsViewModelTest : CoreViewModelTest() { private val stubBuildInfoProvider = mockk() private val stubMainRouter = mockk() private val stubDrawerRouter = mockk() + private val stubDebugMenuAccessor = mockk() override fun initializeViewModel() = SettingsViewModel( getStableDiffusionModelsUseCase = stubGetStableDiffusionModelsUseCase, @@ -57,6 +59,7 @@ class SettingsViewModelTest : CoreViewModelTest() { buildInfoProvider = stubBuildInfoProvider, mainRouter = stubMainRouter, drawerRouter = stubDrawerRouter, + debugMenuAccessor = stubDebugMenuAccessor, ) @Before @@ -90,15 +93,17 @@ class SettingsViewModelTest : CoreViewModelTest() { } @Test - fun `given received Action AppVersion intent, expected router navigateToDebugMenu() method called`() { + fun `given received Action AppVersion intent, expected DeveloperModeUnlocked effect delivered to effect collector`() { every { - stubMainRouter.navigateToDebugMenu() - } returns Unit + stubDebugMenuAccessor.invoke() + } returns true viewModel.processIntent(SettingsIntent.Action.AppVersion) - verify { - stubMainRouter.navigateToDebugMenu() + runTest { + viewModel.effect.test { + Assert.assertEquals(SettingsEffect.DeveloperModeUnlocked, awaitItem()) + } } }