Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.shifthackz.aisdv1.core.common.file

const val LOCAL_DIFFUSION_CUSTOM_PATH = "/storage/emulated/0/Download/SDAI/model"

interface FileProviderDescriptor {
val providerPath: String
val imagesCacheDirPath: String
Expand Down
6 changes: 5 additions & 1 deletion core/localization/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -295,10 +295,14 @@
<string name="notification_fail_sub_title">Нажмите, чтобы открыть приложение.</string>

<string name="model_local_custom_switch">Загрузка кастом модели</string>
<string name="model_local_permission_header">Разрешения</string>
<string name="model_local_permission_title">Чтобы иметь возможность загружать пользовательскую модель, вам необходимо разрешить приложению SDAI управлять разрешениями на хранилище, поскольку, начиная с Android 11, оно необходимо для доступа к файлам хранилища без области действия.</string>
<string name="model_local_permission_button">Настроить доступ</string>
<string name="model_local_path_header">Путь к модели</string>
<string name="model_local_path_title">Путь к папке локальной модели</string>
<string name="model_local_path_button">Выбрать папку</string>

<string name="model_local_custom_title">Чтобы использовать локальную пользовательскую модель, поместите ее в локальную папку в памяти телефона: Download/SDAi/model</string>
<string name="model_local_custom_title">Чтобы использовать локальную пользовательскую модель, поместите ее в локальную папку в памяти телефона.</string>
<string name="model_local_custom_sub_title">Окончательная структура папок должна быть такой:</string>

<string name="debug_section_main">Отладка</string>
Expand Down
6 changes: 5 additions & 1 deletion core/localization/src/main/res/values-tr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,12 @@
<string name="model_local_permission_title">Özel modeli yükleyebilmek için SDAI uygulamasının depolama izinlerini yönetmesine izin vermeniz gerekir; çünkü Android 11\'den itibaren kapsamlı olmayan depolama dosyalarına erişmek gerekir.</string>
<string name="model_local_permission_button">Kurulum izni</string>

<string name="model_local_custom_title">Yerel özel modeli kullanmak için telefonunuzun depolama alanındaki yerel klasöre yerleştirin: Download/SDAi/model</string>
<string name="model_local_custom_title">Yerel özel modeli kullanmak için telefonunuzun depolama alanındaki yerel klasöre yerleştirin.</string>
<string name="model_local_permission_header">İzinler</string>
<string name="model_local_custom_sub_title">Son klasör yapısı şu şekilde olmalıdır::</string>
<string name="model_local_path_header">Model yolu</string>
<string name="model_local_path_title">Yerel model klasör yolu</string>
<string name="model_local_path_button">Klasör seç</string>

<string name="debug_section_main">Hata ayıklama</string>
<string name="debug_section_work_manager">Work Manager API</string>
Expand Down
6 changes: 5 additions & 1 deletion core/localization/src/main/res/values-uk/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,12 @@
<string name="model_local_permission_title">Щоб мати можливість завантажити спеціальну модель, вам потрібно дозволити додатку SDAI керувати дозволами на зберігання, оскільки, починаючи з Android 11, це потрібно для доступу до файлів зберігання без обмежень.</string>
<string name="model_local_permission_button">Налаштувати доступ</string>

<string name="model_local_custom_title">Щоб використовувати локальну спеціальну модель, помістіть її в локальну папку в пам’яті телефону: Download/SDAi/model</string>
<string name="model_local_custom_title">Щоб використовувати локальну спеціальну модель, помістіть її в локальну папку в пам’яті телефону.</string>
<string name="model_local_permission_header">Дозволи</string>
<string name="model_local_custom_sub_title">Остаточна структура папок має бути такою:</string>
<string name="model_local_path_header">Шлях моделі</string>
<string name="model_local_path_title">Шлях папки локальної моделі</string>
<string name="model_local_path_button">Виберіть папку</string>

<string name="debug_section_main">Відладка</string>
<string name="debug_section_work_manager">Work Manager API</string>
Expand Down
6 changes: 5 additions & 1 deletion core/localization/src/main/res/values-zh/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,12 @@
<string name="model_local_custom_switch">加载自定义模型</string>
<string name="model_local_permission_title">为了能够加载自定义模型,您需要允许SDAI应用管理存储权限,因为从Android 11开始,它需要访问非范围存储文件。</string>
<string name="model_local_permission_button">设置权限</string>
<string name="model_local_path_header">模型路径</string>
<string name="model_local_path_title">本地模型文件夹路径</string>
<string name="model_local_path_button">选择文件夹</string>

<string name="model_local_custom_title">要使用本地自定义模型,请将其放置在手机存储中的本地文件夹:Download/SDAi/model</string>
<string name="model_local_custom_title">要使用本地自定义模型,请将其放置在手机存储中的本地文件夹。</string>
<string name="model_local_permission_header">权限</string>
<string name="model_local_custom_sub_title">最终的文件夹结构应该是:</string>

<!-- 调试菜单 -->
Expand Down
6 changes: 5 additions & 1 deletion core/localization/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,14 @@
<string name="model_local_diffusion" translatable="false">Local Diffusion</string>

<string name="model_local_custom_switch">Load custom model</string>
<string name="model_local_permission_header">Permissions</string>
<string name="model_local_permission_title">To be able to load custom model, you need to allow SDAI app manage storage permissions, because starting from Android 11 it is needed to access non-scoped storage files.</string>
<string name="model_local_permission_button">Setup permission</string>
<string name="model_local_path_header">Model path</string>
<string name="model_local_path_title">Local model folder path</string>
<string name="model_local_path_button">Select folder</string>

<string name="model_local_custom_title">To use local custom model, place it to local folder in your phone storage: Download/SDAi/model</string>
<string name="model_local_custom_title">To use local custom model, place it to local folder in your phone storage.</string>
<string name="model_local_custom_sub_title">The final folder structure should be:</string>

<string name="debug_section_main">Debugging</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package com.shifthackz.aisdv1.core.extensions

import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore

fun getRealPath(context: Context, uri: Uri): String? {
if (DocumentsContract.isDocumentUri(context, uri)) {
if (isExternalStorageDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
val type = split[0]

if ("primary".equals(type, ignoreCase = true)) {
return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
} else {
return "/storage/$type/${split[1]}"
}

} else if (isDownloadsDocument(uri)) {
val id = DocumentsContract.getDocumentId(uri)
val contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), id.toLong()
)

return getDataColumn(context, contentUri, null, null)
} else if (isMediaDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
val type = split[0]

var contentUri: Uri? = null
if ("image" == type) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
} else if ("video" == type) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
} else if ("audio" == type) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}

val selection = "_id=?"
val selectionArgs = arrayOf(
split[1]
)

return getDataColumn(context, contentUri, selection, selectionArgs)
}
} else if ("content".equals(uri.scheme, ignoreCase = true)) {
// Return the remote address

if (isGooglePhotosUri(uri)) return uri.lastPathSegment

return getDataColumn(context, uri, null, null)
} else if ("file".equals(uri.scheme, ignoreCase = true)) {
return uri.path
}

return null
}

fun getDataColumn(
context: Context, uri: Uri?, selection: String?,
selectionArgs: Array<String>?
): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(
column
)

try {
cursor = context.contentResolver.query(
uri!!, projection, selection, selectionArgs,
null
)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
return cursor.getString(index)
}
} finally {
cursor?.close()
}
return null
}


/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}

/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}

/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}

/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import com.shifthackz.aisdv1.core.validation.common.CommonStringValidator
import com.shifthackz.aisdv1.core.validation.common.CommonStringValidatorImpl
import com.shifthackz.aisdv1.core.validation.dimension.DimensionValidator
import com.shifthackz.aisdv1.core.validation.dimension.DimensionValidatorImpl
import com.shifthackz.aisdv1.core.validation.path.FilePathValidator
import com.shifthackz.aisdv1.core.validation.path.FilePathValidatorImpl
import com.shifthackz.aisdv1.core.validation.url.UrlValidator
import com.shifthackz.aisdv1.core.validation.url.UrlValidatorImpl
import org.koin.core.module.dsl.factoryOf
Expand All @@ -16,4 +18,5 @@ val validatorsModule = module {
factory<UrlValidator> { UrlValidatorImpl() }

factoryOf(::CommonStringValidatorImpl) bind CommonStringValidator::class
factoryOf(::FilePathValidatorImpl) bind FilePathValidator::class
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.shifthackz.aisdv1.core.validation.path

import com.shifthackz.aisdv1.core.validation.ValidationResult

interface FilePathValidator {

operator fun invoke(input: String?): ValidationResult<Error>

sealed interface Error {
data object Empty : Error
data object Invalid : Error
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.shifthackz.aisdv1.core.validation.path

import com.shifthackz.aisdv1.core.validation.ValidationResult

class FilePathValidatorImpl : FilePathValidator {

override fun invoke(input: String?): ValidationResult<FilePathValidator.Error> = when {
input.isNullOrBlank() -> ValidationResult(
isValid = false,
validationError = FilePathValidator.Error.Empty,
)
!isValidFilePath(input) -> ValidationResult(
isValid = false,
validationError = FilePathValidator.Error.Invalid,
)
else -> ValidationResult(true)
}

private fun isValidFilePath(path: String): Boolean {
val regex = Regex("^(/[^/<>:\"|?*]+)+/?$")
return regex.matches(path)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.shifthackz.aisdv1.core.validation.path

import com.shifthackz.aisdv1.core.validation.ValidationResult
import org.junit.Assert
import org.junit.Test

class FilePathValidatorImplTest {

private val validator = FilePathValidatorImpl()

@Test
fun `iven input is null, expected not valid with Empty error`() {
val expected = ValidationResult<FilePathValidator.Error>(
isValid = false,
validationError = FilePathValidator.Error.Empty,
)
val actual = validator(null)
Assert.assertEquals(expected, actual)
}

@Test
fun `given input is empty, expected not valid with Empty error`() {
val expected = ValidationResult<FilePathValidator.Error>(
isValid = false,
validationError = FilePathValidator.Error.Empty,
)
val actual = validator("")
Assert.assertEquals(expected, actual)
}

@Test
fun `given input is blank, expected not valid with Empty error`() {
val expected = ValidationResult<FilePathValidator.Error>(
isValid = false,
validationError = FilePathValidator.Error.Empty,
)
val actual = validator(" ")
Assert.assertEquals(expected, actual)
}

@Test
fun `given input is not valid, expected not valid with Invalid error`() {
val expected = ValidationResult<FilePathValidator.Error>(
isValid = false,
validationError = FilePathValidator.Error.Invalid,
)
val actual = validator("cc")
Assert.assertEquals(expected, actual)
}

@Test
fun `given input is valid, expected valid`() {
val expected = ValidationResult<FilePathValidator.Error>(true)
val actual = validator("/tmp/local/5598")
Assert.assertEquals(expected, actual)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.file.LOCAL_DIFFUSION_CUSTOM_PATH
import com.shifthackz.aisdv1.core.common.schedulers.SchedulersToken
import com.shifthackz.aisdv1.domain.entity.ColorToken
import com.shifthackz.aisdv1.domain.entity.DarkThemeToken
Expand Down Expand Up @@ -59,6 +60,15 @@ class PreferenceManagerImpl(
.apply()
.also { onPreferencesChanged() }

override var localDiffusionCustomModelPath: String
get() = preferences.getString(
KEY_LOCAL_DIFFUSION_CUSTOM_MODEL_PATH,
LOCAL_DIFFUSION_CUSTOM_PATH,
) ?: LOCAL_DIFFUSION_CUSTOM_PATH
set(value) = preferences.edit()
.putString(KEY_LOCAL_DIFFUSION_CUSTOM_MODEL_PATH, value)
.apply()

override var localDiffusionAllowCancel: Boolean
get() = preferences.getBoolean(KEY_ALLOW_LOCAL_DIFFUSION_CANCEL, false)
set(value) = preferences.edit()
Expand Down Expand Up @@ -285,6 +295,7 @@ class PreferenceManagerImpl(
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_LOCAL_DIFFUSION_CUSTOM_MODEL_PATH = "key_local_diffusion_custom_model_path"
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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ data class Configuration(
val stabilityAiEngineId: String = "",
val authCredentials: AuthorizationCredentials = AuthorizationCredentials.None,
val localModelId: String = "",
val localModelPath: String = "",
)
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface PreferenceManager {
var swarmUiModel: String
var demoMode: Boolean
var developerMode: Boolean
var localDiffusionCustomModelPath: String
var localDiffusionAllowCancel: Boolean
var localDiffusionSchedulerThread: SchedulersToken
var monitorConnectivity: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ internal class GetConfigurationUseCaseImpl(
stabilityAiEngineId = preferenceManager.stabilityAiEngineId,
authCredentials = authorizationStore.getAuthorizationCredentials(),
localModelId = preferenceManager.localModelId,
localModelPath = preferenceManager.localDiffusionCustomModelPath,
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ internal class SetServerConfigurationUseCaseImpl(
preferenceManager.stabilityAiApiKey = configuration.stabilityAiApiKey
preferenceManager.stabilityAiEngineId = configuration.stabilityAiEngineId
preferenceManager.localModelId = configuration.localModelId
preferenceManager.localDiffusionCustomModelPath = configuration.localModelPath
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ val mockConfiguration = Configuration(
stabilityAiApiKey = "5598",
stabilityAiEngineId = "5598",
localModelId = "5598",
localModelPath = "/storage/emulated/0/5598",
)
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ class GetConfigurationUseCaseImplTest {
stubPreferenceManager::localModelId.get()
} returns mockConfiguration.localModelId

every {
stubPreferenceManager::localDiffusionCustomModelPath.get()
} returns mockConfiguration.localModelPath

useCase
.invoke()
.test()
Expand Down
Loading