diff --git a/app/build.gradle b/app/build.gradle index 288d49e..feb3996 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,7 +18,9 @@ android { enabled true } - buildFeatures.dataBinding = true + buildFeatures { + dataBinding true + } lintOptions { checkDependencies true @@ -43,13 +45,16 @@ android { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + + buildConfigField 'String', 'BASE_URL', '"https://api.sodalive.net"' } debug { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - applicationIdSuffix '.debug' + + buildConfigField 'String', 'BASE_URL', '"https://test-api.sodalive.net"' } } compileOptions { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4896831..d5e7a2d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> = Build.VERSION_CODES.TIRAMISU) { + packageManager.getApplicationInfo( + packageName, + PackageManager.ApplicationInfoFlags.of(0L) + ) + } else { + packageManager.getApplicationInfo(packageName, 0) + } + debuggable = 0 != appInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE + } catch (e: PackageManager.NameNotFoundException) { + /* debuggable variable will remain false */ + } + + return debuggable + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt new file mode 100644 index 0000000..7c5f920 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt @@ -0,0 +1,4 @@ +package kr.co.vividnext.sodalive.common + +object Constants { +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/RealPathUtil.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/RealPathUtil.kt new file mode 100644 index 0000000..0787e3f --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/RealPathUtil.kt @@ -0,0 +1,171 @@ +package kr.co.vividnext.sodalive.common + +import android.annotation.SuppressLint +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 +import android.text.TextUtils + +object RealPathUtil { + fun getRealPath(context: Context, fileUri: Uri): String? { + return getRealPathFromURIAPI19(context, fileUri) // SDK > 19 (Android 4.4) and up + } + + /** + * Get a file path from a Uri. This will get the the path for Storage Access + * Framework Documents, as well as the _data field for the MediaStore and + * other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @author Niks + */ + @SuppressLint("NewApi") + fun getRealPathFromURIAPI19(context: Context, uri: Uri): String? { + // DocumentProvider + if (DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + 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 if (isDownloadsDocument(uri)) { + var cursor: Cursor? = null + try { + cursor = context.contentResolver.query( + uri, + arrayOf(MediaStore.MediaColumns.DISPLAY_NAME), + null, + null, + null + ) + cursor!!.moveToNext() + val fileName = cursor.getString(0) + val path = Environment.getExternalStorageDirectory() + .toString() + "/Download/" + fileName + if (!TextUtils.isEmpty(path)) { + return path + } + } finally { + cursor?.close() + } + val id = DocumentsContract.getDocumentId(uri) + if (id.startsWith("raw:")) { + return id.replaceFirst("raw:".toRegex(), "") + } + val contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads"), + java.lang.Long.valueOf(id) + ) + + 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 + when (type) { + "image" -> contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "video" -> contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + "audio" -> contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + + val selection = "_id=?" + val selectionArgs = arrayOf(split[1]) + + return getDataColumn(context, contentUri, selection, selectionArgs) + } // MediaProvider + // DownloadsProvider + } else if ("content".equals(uri.scheme!!, ignoreCase = true)) { + + // Return the remote address + return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn( + context, + uri, + null, + null + ) + } else if ("file".equals(uri.scheme!!, ignoreCase = true)) { + return uri.path + } // File + // MediaStore (and general) + + return null + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + * @author Niks + */ + private fun getDataColumn( + context: Context, + uri: Uri?, + selection: String?, + selectionArgs: Array? + ): 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. + */ + private 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. + */ + private 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. + */ + private 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. + */ + private fun isGooglePhotosUri(uri: Uri): Boolean { + return "com.google.android.apps.photos.content" == uri.authority + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/SharedPreferenceManager.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/SharedPreferenceManager.kt new file mode 100644 index 0000000..9d32b41 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/SharedPreferenceManager.kt @@ -0,0 +1,46 @@ +package kr.co.vividnext.sodalive.common + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceManager + +object SharedPreferenceManager { + private lateinit var sharedPreferences: SharedPreferences + + fun init(context: Context) { + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + } + + fun clear() { + sharedPreferences.edit { it.clear() } + } + + private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) { + val editor = this.edit() + operation(editor) + editor.apply() + } + + private operator fun SharedPreferences.set(key: String, value: Any?) { + when (value) { + is String? -> edit { it.putString(key, value) } + is Int -> edit { it.putInt(key, value) } + is Boolean -> edit { it.putBoolean(key, value) } + is Float -> edit { it.putFloat(key, value) } + is Long -> edit { it.putLong(key, value) } + else -> throw UnsupportedOperationException("Error") + } + } + + @Suppress("UNCHECKED_CAST") + private operator fun SharedPreferences.get(key: String, defaultValue: T? = null): T { + return when (defaultValue) { + is String, null -> getString(key, defaultValue as? String) as T + is Int -> getInt(key, defaultValue as? Int ?: -1) as T + is Boolean -> getBoolean(key, defaultValue as? Boolean ?: false) as T + is Float -> getFloat(key, defaultValue as? Float ?: -1f) as T + is Long -> getLong(key, defaultValue as? Long ?: -1) as T + else -> throw UnsupportedOperationException("Error") + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt new file mode 100644 index 0000000..9d838bc --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt @@ -0,0 +1,66 @@ +package kr.co.vividnext.sodalive.di + +import android.content.Context +import com.google.gson.GsonBuilder +import kr.co.vividnext.sodalive.BuildConfig +import kr.co.vividnext.sodalive.network.TokenAuthenticator +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin +import org.koin.dsl.module +import retrofit2.Retrofit +import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory + +class AppDI(private val context: Context, isDebugMode: Boolean) { + private val baseUrl = BuildConfig.BASE_URL + + private val otherModule = module { + single { GsonBuilder().create() } + } + + private val networkModule = module { + single { + val logging = HttpLoggingInterceptor() + + if (isDebugMode) { + logging.setLevel(HttpLoggingInterceptor.Level.BODY) + } else { + logging.setLevel(HttpLoggingInterceptor.Level.NONE) + } + + OkHttpClient().newBuilder() + .addInterceptor(logging) + .authenticator(TokenAuthenticator(get())) + .build() + } + + single { + Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) + .client(get()) + .build() + } + } + + private val viewModelModule = module {} + + private val repositoryModule = module {} + + private val moduleList = listOf( + networkModule, + viewModelModule, + repositoryModule, + otherModule + ) + + init { + startKoin { + androidContext(context) + modules(moduleList) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/network/TokenAuthenticator.kt b/app/src/main/java/kr/co/vividnext/sodalive/network/TokenAuthenticator.kt new file mode 100644 index 0000000..f7a1de3 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/network/TokenAuthenticator.kt @@ -0,0 +1,20 @@ +package kr.co.vividnext.sodalive.network + +import android.content.Context +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import okhttp3.Authenticator +import okhttp3.Request +import okhttp3.Response +import okhttp3.Route + +class TokenAuthenticator( + private val context: Context +) : Authenticator { + override fun authenticate(route: Route?, response: Response): Request? { + if (response.code == 401) { + SharedPreferenceManager.clear() + } + + return null + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 7706ab9..0000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/font/font.xml b/app/src/main/res/font/font.xml new file mode 100644 index 0000000..720cf18 --- /dev/null +++ b/app/src/main/res/font/font.xml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/app/src/main/res/font/gmarket_sans_bold.ttf b/app/src/main/res/font/gmarket_sans_bold.ttf new file mode 100644 index 0000000..0d20d4a Binary files /dev/null and b/app/src/main/res/font/gmarket_sans_bold.ttf differ diff --git a/app/src/main/res/font/gmarket_sans_light.ttf b/app/src/main/res/font/gmarket_sans_light.ttf new file mode 100644 index 0000000..f315bd3 Binary files /dev/null and b/app/src/main/res/font/gmarket_sans_light.ttf differ diff --git a/app/src/main/res/font/gmarket_sans_medium.ttf b/app/src/main/res/font/gmarket_sans_medium.ttf new file mode 100644 index 0000000..2ac3b7f Binary files /dev/null and b/app/src/main/res/font/gmarket_sans_medium.ttf differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 28a6dbb..d8c3dbe 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -10,6 +10,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" + android:fontFamily="@font/gmarket_sans_bold" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index b3e26b4..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index b3e26b4..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml deleted file mode 100644 index fe090da..0000000 --- a/app/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 768b058..9eb06a0 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,4 +2,6 @@ #FF000000 #FFFFFFFF + + #9970FF diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 873cb57..2c85ba2 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,9 +1,20 @@ - + diff --git a/build.gradle b/build.gradle index 60a05fd..a77ee0e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,8 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext { + kotlin_version = '1.8.0' + } repositories { google() mavenCentral() @@ -9,6 +12,7 @@ buildscript { dependencies { classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6' classpath 'io.objectbox:objectbox-gradle-plugin:3.5.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/gradle.properties b/gradle.properties index 29c7133..5f32366 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,5 +20,6 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true org.gradle.configuration-cache=true +android.nonTransitiveRClass=true +android.defaults.buildfeatures.buildconfig=true