{ "repository_url": "https://github.com/ronelsolomon/java-project.git", "owner": "ronelsolomon", "name": "java-project.git", "extracted_at": "2026-03-02T22:50:30.435875", "files": { "gradlew": { "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n# Gradle start up script for POSIX generated by Gradle.\n#\n# Important for running:\n#\n# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n# noncompliant, but you have some other compliant shell such as ksh or\n# bash, then to run this script, type that shell name before the whole\n# command line, like:\n#\n# ksh Gradle\n#\n# Busybox and similar reduced shells will NOT work, because this script\n# requires all of these POSIX shell features:\n# * functions;\n# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n# «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n# * compound commands having a testable exit status, especially «case»;\n# * various built-in commands including «command», «set», and «ulimit».\n#\n# Important for patching:\n#\n# (2) This script targets any POSIX shell, so it avoids extensions provided\n# by Bash, Ksh, etc; in particular arrays are avoided.\n#\n# The \"traditional\" practice of packing multiple parameters into a\n# space-separated string is a well documented source of bugs and security\n# problems, so this is (mostly) avoided, by progressively accumulating\n# options in \"$@\", and eventually passing that to Java.\n#\n# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n# see the in-line comments for details.\n#\n# There are tweaks for specific operating systems such as AIX, CygWin,\n# Darwin, MinGW, and NonStop.\n#\n# (3) This script is generated from the Groovy template\n# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n# within the Gradle project.\n#\n# You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n APP_HOME=${app_path%\"${app_path##*/}\"} # leaves a trailing /; empty if no leading path\n [ -h \"$app_path\" ]\ndo\n ls=$( ls -ld \"$app_path\" )\n link=${ls#*' -> '}\n case $link in #(\n /*) app_path=$link ;; #(\n *) app_path=$APP_HOME$link ;;\n esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n echo \"$*\"\n} >&2\n\ndie () {\n echo\n echo \"$*\"\n echo\n exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in #(\n CYGWIN* ) cygwin=true ;; #(\n Darwin* ) darwin=true ;; #(\n MSYS* | MINGW* ) msys=true ;; #(\n NONSTOP* ) nonstop=true ;;\nesac\n\nCLASSPATH=\"\\\\\\\"\\\\\\\"\"\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n # IBM's JDK on AIX uses strange locations for the executables\n JAVACMD=$JAVA_HOME/jre/sh/java\n else\n JAVACMD=$JAVA_HOME/bin/java\n fi\n if [ ! -x \"$JAVACMD\" ] ; then\n die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n fi\nelse\n JAVACMD=java\n if ! command -v java >/dev/null 2>&1\n then\n die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n case $MAX_FD in #(\n max*)\n # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n # shellcheck disable=SC2039,SC3045\n MAX_FD=$( ulimit -H -n ) ||\n warn \"Could not query maximum file descriptor limit\"\n esac\n case $MAX_FD in #(\n '' | soft) :;; #(\n *)\n # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n # shellcheck disable=SC2039,SC3045\n ulimit -n \"$MAX_FD\" ||\n warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n# * args from the command line\n# * the main class name\n# * -classpath\n# * -D...appname settings\n# * --module-path (only if needed)\n# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n # Now convert the arguments - kludge to limit ourselves to /bin/sh\n for arg do\n if\n case $arg in #(\n -*) false ;; # don't mess with options #(\n /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath\n [ -e \"$t\" ] ;; #(\n *) false ;;\n esac\n then\n arg=$( cygpath --path --ignore --mixed \"$arg\" )\n fi\n # Roll the args list around exactly as many times as the number of\n # args, so each arg winds up back in the position where it started, but\n # possibly modified.\n #\n # NB: a `for` loop captures its iteration list before it begins, so\n # changing the positional parameters here affects neither the number of\n # iterations, nor the values presented in `arg`.\n shift # remove old arg\n set -- \"$@\" \"$arg\" # push replacement arg\n done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n# and any embedded shellness will be escaped.\n# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n# treated as '${Hostname}' itself on the command line.\n\nset -- \\\n \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n -classpath \"$CLASSPATH\" \\\n -jar \"$APP_HOME/gradle/wrapper/gradle-wrapper.jar\" \\\n \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n# readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n# set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n xargs -n1 |\n sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n tr '\\n' ' '\n )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n", "size": 8710, "language": "unknown" }, "build.gradle": { "content": "// Top-level build file\nbuildscript {\n ext {\n kotlin_version = '1.9.0'\n nav_version = '2.7.7'\n hilt_version = '2.51.1'\n }\n repositories {\n google()\n mavenCentral()\n }\n dependencies {\n classpath 'com.android.tools.build:gradle:8.2.2'\n classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n classpath \"com.google.dagger:hilt-android-gradle-plugin:$hilt_version\"\n classpath \"androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version\"\n }\n}\n\nallprojects {\n repositories {\n google()\n mavenCentral()\n maven { url 'https://jitpack.io' }\n }\n}\n\ntask clean(type: Delete) {\n delete rootProject.buildDir\n}", "size": 729, "language": "unknown" }, ".gitattributes": { "content": "# Auto detect text files and perform LF normalization\n* text=auto\n", "size": 66, "language": "unknown" }, "activity_main.xml": { "content": "\n\n\n \n\n \n\n \n\n \n\n \n\n", "size": 2062, "language": "xml" }, "gradlew.bat": { "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n@rem SPDX-License-Identifier: Apache-2.0\n@rem\n\n@if \"%DEBUG%\"==\"\" @echo off\n@rem ##########################################################################\n@rem\n@rem Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\n@rem This is normally unused\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif %ERRORLEVEL% equ 0 goto execute\n\necho. 1>&2\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\necho. 1>&2\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\necho location of your Java installation. 1>&2\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto execute\n\necho. 1>&2\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\necho. 1>&2\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\necho location of your Java installation. 1>&2\n\ngoto fail\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" -jar \"%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\" %*\n\n:end\n@rem End local scope for the variables with windows NT shell\nif %ERRORLEVEL% equ 0 goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nset EXIT_CODE=%ERRORLEVEL%\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\nexit /b %EXIT_CODE%\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n", "size": 2843, "language": "unknown" }, "app/build.gradle": { "content": "plugins {\n id 'com.android.application'\n id 'kotlin-android'\n id 'kotlin-kapt'\n id 'dagger.hilt.android.plugin'\n id 'androidx.navigation.safeargs.kotlin'\n}\n\nandroid {\n namespace 'com.example.emotiondetector'\n compileSdk 34\n\n defaultConfig {\n applicationId \"com.example.emotiondetector\"\n minSdk 24\n targetSdk 34\n versionCode 1\n versionName \"1.0\"\n\n testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n }\n\n buildTypes {\n release {\n minifyEnabled true\n shrinkResources true\n proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n }\n debug {\n applicationIdSuffix \".debug\"\n debuggable true\n }\n }\n\n buildFeatures {\n viewBinding true\n mlModelBinding true\n }\n\n compileOptions {\n sourceCompatibility JavaVersion.VERSION_17\n targetCompatibility JavaVersion.VERSION_17\n }\n\n kotlinOptions {\n jvmTarget = '17'\n }\n\n aaptOptions {\n noCompress \"tflite\"\n }\n}\n\ndependencies {\n // Core Android\n implementation 'androidx.core:core-ktx:1.12.0'\n implementation 'androidx.appcompat:appcompat:1.6.1'\n implementation 'com.google.android.material:material:1.11.0'\n implementation 'androidx.constraintlayout:constraintlayout:2.1.4'\n implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'\n\n // CameraX\n def camerax_version = \"1.3.1\"\n implementation \"androidx.camera:camera-camera2:$camerax_version\"\n implementation \"androidx.camera:camera-lifecycle:$camerax_version\"\n implementation \"androidx.camera:camera-view:$camerax_version\"\n\n // ML Kit\n implementation 'com.google.mlkit:face-detection:17.1.0'\n implementation 'com.google.mlkit:image-labeling:17.0.7'\n\n // TensorFlow Lite\n implementation 'org.tensorflow:tensorflow-lite:2.14.0'\n implementation 'org.tensorflow:tensorflow-lite-support:0.4.4'\n implementation 'org.tensorflow:tensorflow-lite-metadata:0.4.4'\n implementation 'org.tensorflow:tensorflow-lite-gpu:2.14.0'\n implementation 'org.tensorflow:tensorflow-lite-task-vision:0.4.4'\n\n // MediaPipe\n implementation 'com.google.mediapipe:tasks-vision:0.10.0'\n\n // Dependency Injection\n implementation \"com.google.dagger:hilt-android:$hilt_version\"\n kapt \"com.google.dagger:hilt-android-compiler:$hilt_version\"\n\n // Navigation\n implementation \"androidx.navigation:navigation-fragment-ktx:$nav_version\"\n implementation \"androidx.navigation:navigation-ui-ktx:$nav_version\"\n\n // Coroutines\n implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'\n implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3'\n\n // Room\n def room_version = \"2.6.1\"\n implementation \"androidx.room:room-runtime:$room_version\"\n implementation \"androidx.room:room-ktx:$room_version\"\n kapt \"androidx.room:room-compiler:$room_version\"\n\n // WorkManager\n implementation \"androidx.work:work-runtime-ktx:2.9.0\"\n\n // Testing\n testImplementation 'junit:junit:4.13.2'\n androidTestImplementation 'androidx.test.ext:junit:1.1.5'\n androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'\n}", "size": 3284, "language": "unknown" }, "app/src/test/java/com/example/emotiondetector/MainViewModelTest.kt": { "content": "package com.example.emotiondetector\n\nimport android.net.Uri\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport com.example.emotiondetector.data.AppData\nimport com.example.emotiondetector.ui.MainViewModel\nimport com.example.emotiondetector.util.AppLogger\nimport com.example.emotiondetector.util.FileUtils\nimport com.example.emotiondetector.util.Prefs\nimport com.google.common.truth.Truth.assertThat\nimport dagger.hilt.android.testing.HiltAndroidRule\nimport dagger.hilt.android.testing.HiltAndroidTest\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.StandardTestDispatcher\nimport kotlinx.coroutines.test.TestCoroutineDispatcher\nimport kotlinx.coroutines.test.TestScope\nimport kotlinx.coroutines.test.advanceUntilIdle\nimport kotlinx.coroutines.test.resetMain\nimport kotlinx.coroutines.test.runTest\nimport kotlinx.coroutines.test.setMain\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.ArgumentMatchers.anyString\nimport org.mockito.Mockito.any\nimport org.mockito.Mockito.mock\nimport org.mockito.Mockito.verify\nimport org.mockito.Mockito.`when` as whenever\nimport javax.inject.Inject\n\n@OptIn(ExperimentalCoroutinesApi::class)\n@HiltAndroidTest\n@RunWith(AndroidJUnit4::class)\nclass MainViewModelTest {\n\n @get:Rule\n val hiltRule = HiltAndroidRule(this)\n\n @get:Rule\n val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n private lateinit var viewModel: MainViewModel\n private lateinit var testDispatcher: TestCoroutineDispatcher\n private lateinit var testScope: TestScope\n \n // Mocks\n private lateinit var mockAppLogger: AppLogger\n private lateinit var mockPrefs: Prefs\n private lateinit var mockFileUtils: FileUtils\n\n @Before\n fun setup() {\n testDispatcher = TestCoroutineDispatcher()\n testScope = TestScope(testDispatcher)\n Dispatchers.setMain(testDispatcher)\n \n // Initialize mocks\n mockAppLogger = mock(AppLogger::class.java)\n mockPrefs = mock(Prefs::class.java)\n mockFileUtils = mock(FileUtils::class.java)\n \n // Initialize Hilt\n hiltRule.inject()\n \n // Create ViewModel with test dependencies\n viewModel = MainViewModel(mockAppLogger, mockPrefs, mockFileUtils)\n }\n\n @After\n fun tearDown() {\n Dispatchers.resetMain()\n testDispatcher.cleanupTestCoroutines()\n }\n\n @Test\n fun `initializeApp increments app launch count`() = testScope.runTest {\n // Given\n var launchCount = 0\n whenever(mockPrefs.json(anyString(), any())).thenReturn(launchCount)\n \n // When\n viewModel.initializeApp()\n advanceUntilIdle()\n \n // Then\n verify(mockPrefs).json(\"app_launch_count\", 0)\n verify(mockAppLogger).d(\"MainViewModel\", \"App launch count: 1\")\n }\n\n @Test\n fun `toggleDarkMode updates preference and logs change`() {\n // Given\n val darkModeEnabled = true\n \n // When\n viewModel.toggleDarkMode(darkModeEnabled)\n \n // Then\n verify(mockPrefs).json(\"dark_mode\", darkModeEnabled)\n verify(mockAppLogger).d(\"MainViewModel\", \"Dark mode enabled\")\n }\n \n @Test\n fun `saveImage with valid uri saves image and updates state`() = testScope.runTest {\n // Given\n val testUri = mock(Uri::class.java)\n val expectedPath = \"/test/path/image.jpg\"\n whenever(mockFileUtils.saveImageFromUri(testUri, \"emotion_captures\"))\n .thenReturn(expectedPath)\n \n // When\n viewModel.saveImage(testUri)\n advanceUntilIdle()\n \n // Then\n verify(mockFileUtils).saveImageFromUri(testUri, \"emotion_captures\")\n verify(mockAppLogger).i(\"MainViewModel\", anyString())\n \n val state = viewModel.uiState.value as MainViewModel.MainUiState.Success\n assertThat(state.message).isEqualTo(\"Image saved successfully\")\n }\n \n @Test\n fun `saveImage with error updates state with error message`() = testScope.runTest {\n // Given\n val testUri = mock(Uri::class.java)\n val errorMessage = \"Failed to save image\"\n whenever(mockFileUtils.saveImageFromUri(testUri, \"emotion_captures\"))\n .thenThrow(RuntimeException(errorMessage))\n \n // When\n viewModel.saveImage(testUri)\n advanceUntilIdle()\n \n // Then\n val state = viewModel.uiState.value as MainViewModel.MainUiState.Error\n assertThat(state.message).contains(errorMessage)\n }\n \n @Test\n fun `loadInitialData loads cached data from prefs`() {\n // Given\n val testData = AppData()\n whenever(mockPrefs.json(\"cached_data\")).thenReturn(testData)\n \n // When\n viewModel.loadInitialData()\n \n // Then\n verify(mockPrefs).json(\"cached_data\")\n }\n}\n", "size": 5100, "language": "kotlin" }, "app/src/test/java/com/example/emotiondetector/utils/TestUtils.kt": { "content": "package com.example.emotiondetector.utils\n\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport com.example.emotiondetector.util.AppLogger\nimport com.example.emotiondetector.util.FileUtils\nimport com.example.emotiondetector.util.Prefs\nimport org.mockito.ArgumentMatchers.anyString\nimport org.mockito.Mockito\nimport org.mockito.Mockito.`when`\nimport java.io.File\nimport java.util.concurrent.Executors\n\n/**\n * Test utilities for common test functionality\n */\nobject TestUtils {\n \n /**\n * Create a test application context\n */\n fun getTestContext(): Context {\n return ApplicationProvider.getApplicationContext()\n }\n \n /**\n * Create a test file in the test app's cache directory\n */\n fun createTestFile(fileName: String, content: String = \"test content\"): File {\n val context = getTestContext()\n val file = File(context.cacheDir, fileName)\n file.parentFile?.mkdirs()\n file.writeText(content)\n return file\n }\n \n /**\n * Create a mocked Prefs instance with common configurations\n */\n fun createMockPrefs(): Prefs {\n val prefs = Mockito.mock(Prefs::class.java)\n `when`(prefs.json(anyString(), any())).thenAnswer { it.arguments[1] as Any }\n return prefs\n }\n \n /**\n * Create a mocked AppLogger instance\n */\n fun createMockLogger(): AppLogger {\n return Mockito.mock(AppLogger::class.java)\n }\n \n /**\n * Create a test FileUtils instance\n */\n fun createTestFileUtils(): FileUtils {\n val context = getTestContext()\n return FileUtils(context).apply {\n // Override any methods if needed for testing\n }\n }\n \n /**\n * Create a test executor for coroutine testing\n */\n fun createTestExecutor() = Executors.newSingleThreadExecutor()\n \n /**\n * Get a test asset file path\n */\n fun getTestAssetPath(assetName: String): String {\n return \"src/test/assets/$assetName\"\n }\n \n /**\n * Sleep for a short duration to allow coroutines to complete\n */\n suspend fun delayForCoroutines() {\n kotlinx.coroutines.delay(100)\n }\n \n /**\n * Helper to create mock with relaxed settings\n */\n inline fun relaxedMock(): T = Mockito.mock(T::class.java, \n Mockito.CALLS_REAL_METHODS ?: Mockito.RETURNS_DEFAULTS\n )\n}\n\n/**\n * Helper function to read a file from test resources\n */\nfun readTestResourceFile(fileName: String): String {\n val classLoader = TestUtils::class.java.classLoader\n return classLoader?.getResourceAsStream(fileName)?.bufferedReader()?.use { it.readText() }\n ?: throw IllegalStateException(\"Could not read test resource: $fileName\")\n}\n\n/**\n * Helper function to create a temporary file for testing\n */\nfun createTempTestFile(prefix: String = \"test\", suffix: String = \".tmp\"): File {\n return File.createTempFile(prefix, suffix, ApplicationProvider.getApplicationContext().cacheDir).apply {\n deleteOnExit()\n }\n}\n", "size": 3081, "language": "kotlin" }, "app/src/test/java/com/example/emotiondetector/workers/DownloadModelWorkerTest.kt": { "content": "package com.example.emotiondetector.workers\n\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.work.ListenableWorker.Result\nimport androidx.work.WorkerParameters\nimport androidx.work.testing.TestListenableWorkerBuilder\nimport com.example.emotiondetector.data.ModelManager\nimport com.example.emotiondetector.data.ModelPreferences\nimport com.example.emotiondetector.di.AppModule\nimport com.example.emotiondetector.di.WorkerModule\nimport com.example.emotiondetector.util.AppLogger\nimport com.example.emotiondetector.util.NetworkUtils\nimport com.google.common.truth.Truth.assertThat\nimport dagger.hilt.android.testing.HiltAndroidRule\nimport dagger.hilt.android.testing.HiltAndroidTest\nimport dagger.hilt.android.testing.UninstallModules\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.Mock\nimport org.mockito.Mockito.`when`\nimport org.mockito.Mockito.anyString\nimport org.mockito.MockitoAnnotations\nimport javax.inject.Inject\n\n@OptIn(ExperimentalCoroutinesApi::class)\n@HiltAndroidTest\n@UninstallModules(AppModule::class, WorkerModule::class)\n@RunWith(AndroidJUnit4::class)\nclass DownloadModelWorkerTest {\n\n @get:Rule\n val hiltRule = HiltAndroidRule(this)\n\n @Mock\n private lateinit var mockModelManager: ModelManager\n\n @Mock\n private lateinit var mockModelPreferences: ModelPreferences\n\n @Mock\n private lateinit var mockNetworkUtils: NetworkUtils\n\n @Mock\n private lateinit var mockAppLogger: AppLogger\n\n private lateinit var context: Context\n private lateinit var workerParams: WorkerParameters\n\n @Before\n fun setUp() {\n MockitoAnnotations.openMocks(this)\n context = ApplicationProvider.getApplicationContext()\n \n // Create test worker parameters\n workerParams = TestListenableWorkerBuilder(context).build().run {\n this.workerParameters\n }\n \n // Initialize Hilt\n hiltRule.inject()\n }\n\n @Test\n fun `doWork when download succeeds returns success`() = runTest {\n // Given\n `when`(mockNetworkUtils.isNetworkAvailable()).thenReturn(true)\n `when`(mockModelManager.downloadModel(anyString())).thenReturn(Result.success())\n \n // Create worker with test dependencies\n val worker = TestListenableWorkerBuilder(context)\n .setWorkerFactory(createWorkerFactory())\n .build()\n \n // When\n val result = worker.doWork()\n \n // Then\n assertThat(result).isEqualTo(Result.success())\n }\n \n @Test\n fun `doWork when network is unavailable returns retry`() = runTest {\n // Given\n `when`(mockNetworkUtils.isNetworkAvailable()).thenReturn(false)\n \n // Create worker with test dependencies\n val worker = TestListenableWorkerBuilder(context)\n .setWorkerFactory(createWorkerFactory())\n .build()\n \n // When\n val result = worker.doWork()\n \n // Then\n assertThat(result).isEqualTo(Result.retry())\n }\n \n @Test\n fun `doWork when download fails returns failure`() = runTest {\n // Given\n `when`(mockNetworkUtils.isNetworkAvailable()).thenReturn(true)\n `when`(mockModelManager.downloadModel(anyString()))\n .thenReturn(Result.failure())\n \n // Create worker with test dependencies\n val worker = TestListenableWorkerBuilder(context)\n .setWorkerFactory(createWorkerFactory())\n .build()\n \n // When\n val result = worker.doWork()\n \n // Then\n assertThat(result).isEqualTo(Result.failure())\n }\n \n private fun createWorkerFactory(): TestWorkerFactory {\n return TestWorkerFactory(\n mockModelManager,\n mockModelPreferences,\n mockNetworkUtils,\n mockAppLogger\n )\n }\n \n // Test worker factory for dependency injection\n class TestWorkerFactory(\n private val modelManager: ModelManager,\n private val modelPreferences: ModelPreferences,\n private val networkUtils: NetworkUtils,\n private val appLogger: AppLogger\n ) : androidx.work.WorkerFactory() {\n override fun createWorker(\n appContext: Context,\n workerClassName: String,\n workerParameters: WorkerParameters\n ): ListenableWorker? {\n return DownloadModelWorker(\n appContext,\n workerParameters,\n modelManager,\n modelPreferences,\n networkUtils,\n appLogger\n )\n }\n }\n}\n", "size": 4950, "language": "kotlin" }, "app/src/main/AndroidManifest.xml": { "content": "\n\n\n \n \n \n \n \n\n \n \n\n \n\n \n \n \n \n \n \n\n \n \n \n \n\n", "size": 2138, "language": "xml" }, "app/src/main/java/com/example/emotiondetector/EmotionDetectorApp.kt": { "content": "package com.example.emotiondetector\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.hilt.work.HiltWorkerFactory\nimport androidx.work.Configuration\nimport com.example.emotiondetector.di.AppModule\nimport com.example.emotiondetector.di.WorkerModule\nimport com.example.emotiondetector.util.AppLogger\nimport com.example.emotiondetector.util.FileUtils\nimport com.example.emotiondetector.util.Prefs\nimport dagger.hilt.android.HiltAndroidApp\nimport javax.inject.Inject\n\n/**\n * Main application class that initializes Hilt and application-wide components\n */\n@HiltAndroidApp\nclass EmotionDetectorApp : Application(), Configuration.Provider {\n\n @Inject\n lateinit var workerFactory: HiltWorkerFactory\n\n @Inject\n lateinit var appLogger: AppLogger\n\n @Inject\n lateinit var prefs: Prefs\n\n @Inject\n lateinit var fileUtils: FileUtils\n\n override fun onCreate() {\n super.onCreate()\n \n // Initialize application components\n initializeApp()\n }\n\n private fun initializeApp() {\n // Log app start\n appLogger.i(\"Application\", \"Emotion Detector app starting...\")\n \n // Initialize directories\n fileUtils.initializeAppDirs()\n \n // Log initialization complete\n appLogger.i(\"Application\", \"Application initialization complete\")\n }\n\n override fun getWorkManagerConfiguration(): Configuration {\n return Configuration.Builder()\n .setWorkerFactory(workerFactory)\n .setMinimumLoggingLevel(android.util.Log.INFO)\n .build()\n }\n\n companion object {\n /**\n * Get the application instance\n */\n fun from(context: Context): EmotionDetectorApp {\n return context.applicationContext as EmotionDetectorApp\n }\n }\n}\n\n// Add this to your AndroidManifest.xml:\n// \n", "size": 1934, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/ui/MainViewModel.kt": { "content": "package com.example.emotiondetector.ui\n\nimport android.net.Uri\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.example.emotiondetector.util.AppLogger\nimport com.example.emotiondetector.util.DateTimeUtils\nimport com.example.emotiondetector.util.FileUtils\nimport com.example.emotiondetector.util.Prefs\nimport com.example.emotiondetector.util.json\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\nimport java.util.*\nimport javax.inject.Inject\n\n@HiltViewModel\nclass MainViewModel @Inject constructor(\n private val appLogger: AppLogger,\n private val prefs: Prefs,\n private val fileUtils: FileUtils\n) : ViewModel() {\n\n private val _uiState = MutableStateFlow(MainUiState.Loading)\n val uiState: StateFlow = _uiState.asStateFlow()\n\n // Preferences example\n private var appLaunchCount by prefs.json(\"app_launch_count\", 0)\n \n // User settings with default values\n private var isDarkMode by prefs.json(\"dark_mode\", false)\n private var lastUpdateCheck by prefs.json(\"last_update_check\")\n \n init {\n initializeApp()\n }\n \n private fun initializeApp() {\n viewModelScope.launch {\n try {\n // Track app launches\n appLaunchCount++\n appLogger.d(\"MainViewModel\", \"App launch count: $appLaunchCount\")\n \n // Update last check time\n lastUpdateCheck = Date()\n \n // Check for updates if needed\n checkForUpdates()\n \n // Load initial data\n loadInitialData()\n \n _uiState.value = MainUiState.Success(\"App initialized successfully\")\n } catch (e: Exception) {\n appLogger.e(\"MainViewModel\", \"Error initializing app\", e)\n _uiState.value = MainUiState.Error(\"Initialization failed: ${e.message}\")\n }\n }\n }\n \n private fun checkForUpdates() {\n // Example: Check for updates if it's been more than 24 hours\n val lastCheck = lastUpdateCheck?.time ?: 0L\n val oneDayInMillis = 24 * 60 * 60 * 1000L\n \n if (System.currentTimeMillis() - lastCheck > oneDayInMillis) {\n appLogger.i(\"MainViewModel\", \"Checking for updates...\")\n // TODO: Implement actual update check\n }\n }\n \n private fun loadInitialData() {\n // Example: Load some initial data\n val cachedData = prefs.json(\"cached_data\") ?: AppData()\n // Process cached data...\n }\n \n fun toggleDarkMode(enabled: Boolean) {\n isDarkMode = enabled\n appLogger.d(\"MainViewModel\", \"Dark mode ${if (enabled) \"enabled\" else \"disabled\"}\")\n // TODO: Apply theme changes\n }\n \n fun saveImage(uri: Uri) {\n viewModelScope.launch {\n try {\n _uiState.value = MainUiState.Loading\n \n // Example: Save image using FileUtils\n val savedPath = fileUtils.saveImageFromUri(uri, \"emotion_captures\")\n \n // Log the action with timestamp\n val timestamp = DateTimeUtils.formatDate(Date(), \"yyyy-MM-dd HH:mm:ss\")\n appLogger.i(\"MainViewModel\", \"Image saved at: $savedPath ($timestamp)\")\n \n _uiState.value = MainUiState.Success(\"Image saved successfully\")\n } catch (e: Exception) {\n appLogger.e(\"MainViewModel\", \"Error saving image\", e)\n _uiState.value = MainUiState.Error(\"Failed to save image: ${e.message}\")\n }\n }\n }\n \n // Data classes for state management\n sealed class MainUiState {\n object Loading : MainUiState()\n data class Success(val message: String) : MainUiState()\n data class Error(val message: String) : MainUiState()\n }\n \n data class AppData(\n val lastSync: Date = Date(),\n val settings: Map = emptyMap(),\n val recentEmotions: List = emptyList()\n )\n}\n", "size": 4281, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/ui/MainActivity.kt": { "content": "package com.example.emotiondetector.ui\n\nimport android.Manifest\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalLifecycleOwner\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleEventObserver\nimport com.example.emotiondetector.ui.theme.EmotionDetectorTheme\nimport dagger.hilt.android.AndroidEntryPoint\n\n@AndroidEntryPoint\nclass MainActivity : ComponentActivity() {\n \n private val requestPermissionLauncher = registerForActivityResult(\n ActivityResultContracts.RequestPermission()\n ) { isGranted ->\n if (isGranted) {\n // Permission granted, start camera\n } else {\n // Show rationale\n }\n }\n\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n \n setContent {\n EmotionDetectorTheme {\n // A surface container using the 'background' color from the theme\n Surface(\n modifier = Modifier.fillMaxSize(),\n color = MaterialTheme.colorScheme.background\n ) {\n EmotionDetectorApp(\n onPermissionDenied = {\n // Handle permission denied\n }\n )\n }\n }\n }\n }\n}\n\n@Composable\nfun EmotionDetectorApp(\n onPermissionDenied: () -> Unit\n) {\n val lifecycleOwner = LocalLifecycleOwner.current\n \n // Handle camera permission\n DisposableEffect(key1 = lifecycleOwner) {\n val observer = LifecycleEventObserver { _, event ->\n if (event == Lifecycle.Event.ON_START) {\n // Check and request permissions\n }\n }\n \n lifecycleOwner.lifecycle.addObserver(observer)\n onDispose {\n lifecycleOwner.lifecycle.removeObserver(observer)\n }\n }\n \n // Main navigation\n MainNavigation()\n}\n\n@Composable\nfun MainNavigation() {\n // Navigation setup will be implemented here\n CameraScreen()\n}\n\n@Composable\nfun CameraScreen() {\n // Camera preview and emotion detection UI will be implemented here\n Surface(\n modifier = Modifier.fillMaxSize(),\n color = MaterialTheme.colorScheme.surface\n ) {\n // Camera preview and emotion detection overlay\n }\n}\n", "size": 2711, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/ui/viewmodel/EmotionViewModel.kt": { "content": "package com.example.emotiondetector.ui.viewmodel\n\nimport android.graphics.Bitmap\nimport android.util.Log\nimport androidx.camera.core.ImageProxy\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.example.emotiondetector.domain.EmotionDetector\nimport com.example.emotiondetector.domain.EmotionResult\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport java.util.concurrent.atomic.AtomicBoolean\nimport javax.inject.Inject\n\n@HiltViewModel\nclass EmotionViewModel @Inject constructor(\n private val emotionDetector: EmotionDetector\n) : ViewModel() {\n\n private val _uiState = MutableStateFlow(EmotionUiState())\n val uiState: StateFlow = _uiState.asStateFlow()\n \n private val isProcessing = AtomicBoolean(false)\n private var processingJob: Job? = null\n \n // Track FPS for performance monitoring\n private var frameCount = 0\n private var lastFpsUpdateTime = System.currentTimeMillis()\n \n // Model performance metrics\n private var totalInferenceTime = 0L\n private var inferenceCount = 0\n \n // Current camera facing direction\n var cameraFacingFront by mutableStateOf(true)\n private set\n \n /**\n * Process a camera frame for emotion detection\n */\n fun processImage(imageProxy: ImageProxy) {\n // Skip if already processing or UI is not ready\n if (isProcessing.get() || !_uiState.value.isReady) {\n imageProxy.close()\n return\n }\n \n isProcessing.set(true)\n \n // Cancel any existing processing job\n processingJob?.cancel()\n \n processingJob = viewModelScope.launch {\n try {\n val startTime = System.currentTimeMillis()\n \n // Detect emotions in the image\n val results = emotionDetector.detectEmotions(imageProxy)\n \n // Calculate inference time\n val inferenceTime = System.currentTimeMillis() - startTime\n \n // Update metrics\n totalInferenceTime += inferenceTime\n inferenceCount++\n \n // Update FPS counter\n updateFps()\n \n // Update UI state with results\n _uiState.update { currentState ->\n currentState.copy(\n emotionResults = results,\n lastInferenceTime = inferenceTime,\n averageInferenceTime = if (inferenceCount > 0) {\n totalInferenceTime / inferenceCount\n } else {\n 0L\n },\n isProcessing = false\n )\n }\n \n // Log performance metrics\n if (inferenceCount % 30 == 0) { // Log every 30 frames\n Log.d(\"EmotionViewModel\", \n \"Avg inference time: ${_uiState.value.averageInferenceTime}ms, \" +\n \"FPS: ${_uiState.value.currentFps}\")\n }\n \n } catch (e: Exception) {\n Log.e(\"EmotionViewModel\", \"Error processing image\", e)\n _uiState.update { it.copy(error = e.message, isProcessing = false) }\n } finally {\n isProcessing.set(false)\n }\n }\n }\n \n /**\n * Toggle between front and back camera\n */\n fun toggleCamera() {\n cameraFacingFront = !cameraFacingFront\n _uiState.update { it.copy(cameraFacingFront = cameraFacingFront) }\n }\n \n /**\n * Update FPS counter\n */\n private fun updateFps() {\n frameCount++\n val currentTime = System.currentTimeMillis()\n val elapsedTime = currentTime - lastFpsUpdateTime\n \n // Update FPS every second\n if (elapsedTime >= 1000) {\n val fps = (frameCount * 1000 / elapsedTime.toFloat()).toInt()\n _uiState.update { it.copy(currentFps = fps) }\n frameCount = 0\n lastFpsUpdateTime = currentTime\n }\n }\n \n /**\n * Reset the emotion detection state\n */\n fun reset() {\n _uiState.update { EmotionUiState() }\n frameCount = 0\n lastFpsUpdateTime = System.currentTimeMillis()\n totalInferenceTime = 0L\n inferenceCount = 0\n }\n \n /**\n * Set error state\n */\n fun setError(message: String) {\n _uiState.update { it.copy(error = message) }\n }\n \n /**\n * Clear error state\n */\n fun clearError() {\n _uiState.update { it.copy(error = null) }\n }\n \n override fun onCleared() {\n super.onCleared()\n processingJob?.cancel()\n emotionDetector.close()\n }\n}\n\n/**\n * UI state for the emotion detection screen\n */\ndata class EmotionUiState(\n val emotionResults: List = emptyList(),\n val isProcessing: Boolean = false,\n val error: String? = null,\n val currentFps: Int = 0,\n val lastInferenceTime: Long = 0,\n val averageInferenceTime: Long = 0,\n val cameraFacingFront: Boolean = true,\n val isReady: Boolean = true // Indicates if the detector is ready to process frames\n)\n", "size": 5704, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/ui/camera/CameraScreen.kt": { "content": "package com.example.emotiondetector.ui.camera\n\nimport android.Manifest\nimport android.content.pm.PackageManager\nimport android.util.Log\nimport android.util.Size\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.camera.core.CameraSelector\nimport androidx.camera.core.ImageAnalysis\nimport androidx.camera.core.Preview\nimport androidx.camera.lifecycle.ProcessCameraProvider\nimport androidx.camera.view.PreviewView\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Cameraswitch\nimport androidx.compose.material.icons.filled.Info\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalLifecycleOwner\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.viewinterop.AndroidView\nimport androidx.core.content.ContextCompat\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleEventObserver\nimport com.example.emotiondetector.R\nimport com.example.emotiondetector.ui.theme.*\nimport com.google.accompanist.permissions.ExperimentalPermissionsApi\nimport com.google.accompanist.permissions.rememberPermissionState\nimport kotlinx.coroutines.launch\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)\n@Composable\nfun CameraScreen(\n onError: (String) -> Unit = {},\n onEmotionDetected: (String, Float) -> Unit = { _, _ -> }\n) {\n val context = LocalContext.current\n val lifecycleOwner = LocalLifecycleOwner.current\n val coroutineScope = rememberCoroutineScope()\n \n // Camera state\n var hasCamPermission by remember { \n mutableStateOf(\n ContextCompat.checkSelfPermission(\n context,\n Manifest.permission.CAMERA\n ) == PackageManager.PERMISSION_GRANTED\n )\n }\n \n val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA) {\n hasCamPermission = it\n if (!it) {\n onError(\"Camera permission is required for emotion detection\")\n }\n }\n \n // Camera provider\n val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }\n var previewUseCase by remember { mutableStateOf(null) }\n var cameraSelector by remember { \n mutableStateOf(CameraSelector.DEFAULT_FRONT_CAMERA) \n }\n \n // Request permission when the screen is first launched\n LaunchedEffect(Unit) {\n if (!hasCamPermission) {\n cameraPermissionState.launchPermissionRequest()\n }\n }\n \n // Set up the camera when permission is granted\n if (hasCamPermission) {\n AndroidView(\n modifier = Modifier.fillMaxSize(),\n factory = { ctx ->\n val previewView = PreviewView(ctx).apply {\n implementationMode = PreviewView.ImplementationMode.COMPATIBLE\n scaleType = PreviewView.ScaleType.FILL_CENTER\n }\n \n // Set up camera use cases\n coroutineScope.launch {\n val cameraProvider = cameraProviderFuture.await()\n \n // Set up the preview use case\n val preview = Preview.Builder().build().also {\n it.setSurfaceProvider(previewView.surfaceProvider)\n }\n previewUseCase = preview\n \n // Set up the image analysis use case\n val imageAnalysis = ImageAnalysis.Builder()\n .setTargetResolution(Size(1280, 720))\n .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)\n .build()\n \n imageAnalysis.setAnalyzer(\n ContextCompat.getMainExecutor(context),\n { imageProxy ->\n // Process the image for emotion detection\n // This will be implemented in the next step\n imageProxy.close()\n }\n )\n \n try {\n // Unbind all use cases before binding new ones\n cameraProvider.unbindAll()\n \n // Bind use cases to the lifecycle\n cameraProvider.bindToLifecycle(\n lifecycleOwner,\n cameraSelector,\n preview,\n imageAnalysis\n )\n } catch (e: Exception) {\n onError(\"Failed to start camera: ${e.message}\")\n }\n }\n \n previewView\n }\n )\n \n // Camera controls overlay\n Box(\n modifier = Modifier\n .fillMaxSize()\n .padding(16.dp)\n ) {\n // Switch camera button\n FloatingActionButton(\n onClick = {\n cameraSelector = when (cameraSelector) {\n CameraSelector.DEFAULT_FRONT_CAMERA -> CameraSelector.DEFAULT_BACK_CAMERA\n else -> CameraSelector.DEFAULT_FRONT_CAMERA\n }\n // Restart preview with new camera selector\n previewUseCase?.targetRotation = when (cameraSelector.lensFacing) {\n CameraSelector.LENS_FACING_FRONT -> 270\n else -> 90\n }\n },\n modifier = Modifier\n .align(Alignment.BottomEnd)\n .padding(bottom = 16.dp),\n containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.9f)\n ) {\n Icon(\n imageVector = Icons.Default.Cameraswitch,\n contentDescription = \"Switch camera\"\n )\n }\n \n // Emotion result overlay\n EmotionResultOverlay(\n modifier = Modifier\n .align(Alignment.TopCenter)\n .padding(top = 32.dp)\n )\n }\n } else {\n // Permission not granted UI\n Box(\n modifier = Modifier\n .fillMaxSize()\n .background(MaterialTheme.colorScheme.surface)\n .padding(16.dp),\n contentAlignment = Alignment.Center\n ) {\n Column(\n horizontalAlignment = Alignment.CenterHorizontally,\n verticalArrangement = Arrangement.Center,\n modifier = Modifier.padding(16.dp)\n ) {\n Icon(\n imageVector = Icons.Default.Info,\n contentDescription = \"Permission required\",\n tint = MaterialTheme.colorScheme.primary,\n modifier = Modifier.size(48.dp)\n )\n Spacer(modifier = Modifier.height(16.dp))\n Text(\n text = \"Camera Permission Required\",\n style = MaterialTheme.typography.titleLarge,\n color = MaterialTheme.colorScheme.onSurface\n )\n Spacer(modifier = Modifier.height(8.dp))\n Text(\n text = \"Please grant camera permission to enable emotion detection\",\n style = MaterialTheme.typography.bodyMedium,\n color = MaterialTheme.colorScheme.onSurfaceVariant,\n textAlign = TextAlign.Center\n )\n Spacer(modifier = Modifier.height(24.dp))\n Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {\n Text(\"Grant Permission\")\n }\n }\n }\n }\n}\n\n@Composable\nprivate fun EmotionResultOverlay(\n modifier: Modifier = Modifier,\n emotion: String = \"Neutral\",\n confidence: Float = 0f\n) {\n val emotionColor = when (emotion.lowercase()) {\n \"happy\" -> HappyColor\n \"sad\" -> SadColor\n \"angry\" -> AngryColor\n \"surprise\" -> SurpriseColor\n \"fear\" -> FearColor\n \"disgust\" -> DisgustColor\n else -> NeutralColor\n }\n \n Surface(\n modifier = modifier,\n color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.9f),\n shape = MaterialTheme.shapes.medium,\n shadowElevation = 4.dp\n ) {\n Row(\n verticalAlignment = Alignment.CenterVertically,\n modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)\n ) {\n Box(\n modifier = Modifier\n .size(12.dp)\n .background(emotionColor, MaterialTheme.shapes.small)\n )\n Spacer(modifier = Modifier.width(8.dp))\n Text(\n text = \"${emotion.replaceFirstChar { it.uppercase() }}: ${(confidence * 100).toInt()}%\",\n style = MaterialTheme.typography.titleMedium,\n color = MaterialTheme.colorScheme.onSurfaceVariant\n )\n }\n }\n}\n\n@Composable\nfun PreviewCameraScreen() {\n EmotionDetectorTheme {\n Surface(\n modifier = Modifier.fillMaxSize(),\n color = MaterialTheme.colorScheme.background\n ) {\n CameraScreen(\n onError = {},\n onEmotionDetected = { _, _ -> }\n )\n }\n }\n}\n", "size": 9928, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/ui/theme/Color.kt": { "content": "package com.example.emotiondetector.ui.theme\n\nimport androidx.compose.ui.graphics.Color\n\n// Light theme colors\nval Purple80 = Color(0xFFD0BCFF)\nval PurpleGrey80 = Color(0xFFCCC2DC)\nval Pink80 = Color(0xFFEFB8C8)\n\n// Dark theme colors\nval Purple40 = Color(0xFF6650a4)\nval PurpleGrey40 = Color(0xFF625b71)\nval Pink40 = Color(0xFF7D5260)\n\n// Emotion colors\nval HappyColor = Color(0xFFFFD700) // Gold\nval SadColor = Color(0xFF1E90FF) // Dodger Blue\nval AngryColor = Color(0xFFFF4500) // Orange Red\nval SurpriseColor = Color(0xFF9932CC) // Dark Orchid\nval NeutralColor = Color(0xFF808080) // Gray\nval FearColor = Color(0xFF8B4513) // Saddle Brown\nval DisgustColor = Color(0xFF228B22) // Forest Green\n", "size": 701, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/ui/theme/Theme.kt": { "content": "package com.example.emotiondetector.ui.theme\n\nimport android.app.Activity\nimport android.os.Build\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.SideEffect\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalView\nimport androidx.core.view.WindowCompat\n\nprivate val DarkColorScheme = darkColorScheme(\n primary = Purple80,\n secondary = PurpleGrey80,\n tertiary = Pink80\n)\n\nprivate val LightColorScheme = lightColorScheme(\n primary = Purple40,\n secondary = PurpleGrey40,\n tertiary = Pink40\n\n /* Other default colors to override\n background = Color(0xFFFFFBFE),\n surface = Color(0xFFFFFBFE),\n onPrimary = Color.White,\n onSecondary = Color.White,\n onTertiary = Color.White,\n onBackground = Color(0xFF1C1B1F),\n onSurface = Color(0xFF1C1B1F),\n */\n)\n\n@Composable\nfun EmotionDetectorTheme(\n darkTheme: Boolean = isSystemInDarkTheme(),\n // Dynamic color is available on Android 12+\n dynamicColor: Boolean = true,\n content: @Composable () -> Unit\n) {\n val colorScheme = when {\n dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {\n val context = LocalContext.current\n if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)\n }\n darkTheme -> DarkColorScheme\n else -> LightColorScheme\n }\n val view = LocalView.current\n if (!view.isInEditMode) {\n SideEffect {\n val window = (view.context as Activity).window\n window.statusBarColor = colorScheme.primary.toArgb()\n WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme\n }\n }\n\n MaterialTheme(\n colorScheme = colorScheme,\n typography = Typography,\n content = content\n )\n}\n", "size": 2205, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/ui/theme/Type.kt": { "content": "package com.example.emotiondetector.ui.theme\n\nimport androidx.compose.material3.Typography\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.sp\n\n// Set of Material typography styles to start with\nval Typography = Typography(\n displayLarge = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.Normal,\n fontSize = 57.sp,\n lineHeight = 64.sp,\n letterSpacing = 0.sp\n ),\n displayMedium = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.Normal,\n fontSize = 45.sp,\n lineHeight = 52.sp,\n letterSpacing = 0.sp\n ),\n displaySmall = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.Normal,\n fontSize = 36.sp,\n lineHeight = 44.sp,\n letterSpacing = 0.sp\n ),\n headlineLarge = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.SemiBold,\n fontSize = 32.sp,\n lineHeight = 40.sp,\n letterSpacing = 0.sp\n ),\n headlineMedium = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.SemiBold,\n fontSize = 28.sp,\n lineHeight = 36.sp,\n letterSpacing = 0.sp\n ),\n headlineSmall = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.SemiBold,\n fontSize = 24.sp,\n lineHeight = 32.sp,\n letterSpacing = 0.sp\n ),\n titleLarge = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.SemiBold,\n fontSize = 22.sp,\n lineHeight = 28.sp,\n letterSpacing = 0.sp\n ),\n titleMedium = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.SemiBold,\n fontSize = 18.sp,\n lineHeight = 24.sp,\n letterSpacing = 0.1.sp\n ),\n titleSmall = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.SemiBold,\n fontSize = 14.sp,\n lineHeight = 20.sp,\n letterSpacing = 0.1.sp\n ),\n bodyLarge = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.Normal,\n fontSize = 16.sp,\n lineHeight = 24.sp,\n letterSpacing = 0.5.sp\n ),\n bodyMedium = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.Normal,\n fontSize = 14.sp,\n lineHeight = 20.sp,\n letterSpacing = 0.25.sp\n ),\n bodySmall = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.Normal,\n fontSize = 12.sp,\n lineHeight = 16.sp,\n letterSpacing = 0.4.sp\n ),\n labelLarge = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.Medium,\n fontSize = 14.sp,\n lineHeight = 20.sp,\n letterSpacing = 0.1.sp\n ),\n labelMedium = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.Medium,\n fontSize = 12.sp,\n lineHeight = 16.sp,\n letterSpacing = 0.5.sp\n ),\n labelSmall = TextStyle(\n fontFamily = FontFamily.Default,\n fontWeight = FontWeight.Medium,\n fontSize = 10.sp,\n lineHeight = 16.sp,\n letterSpacing = 0.5.sp\n )\n)\n", "size": 3377, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/di/AppModule.kt": { "content": "package com.example.emotiondetector.di\n\nimport android.content.Context\nimport com.example.emotiondetector.data.ModelManager\nimport com.example.emotiondetector.data.ModelPreferences\nimport com.example.emotiondetector.data.SecureStorage\nimport com.example.emotiondetector.data.TelemetryManager\nimport com.example.emotiondetector.domain.EmotionDetector\nimport com.example.emotiondetector.security.KeyManager\nimport com.example.emotiondetector.util.*\nimport com.google.gson.Gson\nimport com.google.gson.GsonBuilder\nimport dagger.Module\nimport dagger.Provides\nimport dagger.hilt.InstallIn\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport dagger.hilt.components.SingletonComponent\nimport net.sqlcipher.database.SQLiteDatabase\nimport okhttp3.OkHttpClient\nimport okhttp3.logging.HttpLoggingInterceptor\nimport java.util.concurrent.TimeUnit\nimport javax.inject.Singleton\n\n@Module\n@InstallIn(SingletonComponent::class)\nobject AppModule {\n \n @Provides\n @Singleton\n fun provideEmotionDetector(\n @ApplicationContext context: Context,\n modelManager: ModelManager\n ): EmotionDetector {\n return EmotionDetector(\n context = context,\n modelPath = modelManager.modelFile.absolutePath,\n threshold = 0.7f\n )\n }\n \n @Provides\n @Singleton\n fun provideModelManager(\n @ApplicationContext context: Context,\n workManager: WorkManager,\n modelPreferences: ModelPreferences\n ): ModelManager {\n return ModelManager(context, workManager, modelPreferences).apply {\n initialize()\n }\n }\n \n @Provides\n @Singleton\n fun provideModelPreferences(\n @ApplicationContext context: Context\n ): ModelPreferences {\n return ModelPreferences(context)\n }\n \n @Provides\n @Singleton\n fun provideWorkManager(\n @ApplicationContext context: Context\n ): WorkManager {\n return WorkManager.getInstance(context)\n }\n \n @Provides\n @Singleton\n fun provideKeyManager(\n @ApplicationContext context: Context\n ): KeyManager {\n return KeyManager(context).apply {\n migrateFromLegacyIfNeeded()\n }\n }\n \n @Provides\n @Singleton\n fun provideSecureStorage(\n @ApplicationContext context: Context,\n keyManager: KeyManager\n ): SecureStorage {\n // In a real app, you'd derive a key from the KeyManager\n // For simplicity, we're using a fixed key here\n val encryptionKey = SQLiteDatabase.getBytes(\"your-secure-encryption-key\".toCharArray())\n return SecureStorage(context, encryptionKey)\n }\n \n @Provides\n @Singleton\n fun provideTelemetryManager(\n @ApplicationContext context: Context,\n workManager: WorkManager,\n secureStorage: SecureStorage\n ): TelemetryManager {\n return TelemetryManager(context, workManager, secureStorage)\n }\n \n @Provides\n @Singleton\n fun provideGson(): Gson {\n return GsonBuilder()\n .setPrettyPrinting()\n .create()\n }\n \n @Provides\n @Singleton\n fun provideOkHttpClient(\n @ApplicationContext context: Context\n ): OkHttpClient {\n val loggingInterceptor = HttpLoggingInterceptor().apply {\n level = if (BuildConfig.DEBUG) {\n HttpLoggingInterceptor.Level.BODY\n } else {\n HttpLoggingInterceptor.Level.NONE\n }\n }\n \n return OkHttpClient.Builder()\n .connectTimeout(30, TimeUnit.SECONDS)\n .readTimeout(30, TimeUnit.SECONDS)\n .writeTimeout(30, TimeUnit.SECONDS)\n .addInterceptor(loggingInterceptor)\n .build()\n }\n \n @Provides\n @Singleton\n fun provideAppLogger(\n @ApplicationContext context: Context,\n fileUtils: FileUtils\n ): AppLogger {\n return AppLogger(context, fileUtils)\n }\n\n @Provides\n @Singleton\n fun providePrefs(@ApplicationContext context: Context): Prefs {\n return Prefs(context)\n }\n\n @Provides\n @Singleton\n fun provideJsonUtils(): JsonUtils {\n return JsonUtils()\n }\n \n @Provides\n @Singleton\n fun provideFileUtils(@ApplicationContext context: Context): FileUtils {\n return FileUtils(context)\n }\n \n @Provides\n @Singleton\n fun provideNetworkUtils(\n @ApplicationContext context: Context,\n fileUtils: FileUtils,\n appLogger: AppLogger\n ): NetworkUtils {\n return NetworkUtils(context, fileUtils, appLogger)\n }\n \n @Provides\n @Singleton\n fun providePermissionUtils(): PermissionUtils {\n return PermissionUtils\n }\n \n @Provides\n @Singleton\n fun provideDateTimeUtils(): DateTimeUtils {\n return DateTimeUtils\n }\n \n @Provides\n @Singleton\n fun provideImageUtils(\n @ApplicationContext context: Context,\n fileUtils: FileUtils\n ): ImageUtils {\n return ImageUtils(context, fileUtils)\n }\n}\n", "size": 5028, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/di/WorkerModule.kt": { "content": "package com.example.emotiondetector.di\n\nimport android.content.Context\nimport androidx.work.ListenableWorker\nimport androidx.work.WorkerFactory\nimport androidx.work.WorkerParameters\nimport com.example.emotiondetector.worker.DownloadModelWorker\nimport com.example.emotiondetector.worker.ModelUpdateWorker\nimport com.example.emotiondetector.worker.UploadTelemetryWorker\nimport dagger.Binds\nimport dagger.MapKey\nimport dagger.Module\nimport dagger.hilt.InstallIn\nimport dagger.hilt.components.SingletonComponent\nimport dagger.multibindings.IntoMap\nimport kotlin.reflect.KClass\nimport javax.inject.Inject\nimport javax.inject.Provider\nimport javax.inject.Singleton\n\n/**\n * Hilt WorkerFactory to handle dependency injection in workers\n */\nclass HiltWorkerFactory @Inject constructor(\n private val workerFactories: Map, @JvmSuppressWildcards Provider>\n) : WorkerFactory() {\n \n override fun createWorker(\n appContext: Context,\n workerClassName: String,\n workerParameters: WorkerParameters\n ): ListenableWorker? {\n val foundEntry = workerFactories.entries.find { \n Class.forName(workerClassName).isAssignableFrom(it.key)\n }\n \n val factoryProvider = foundEntry?.value\n ?: throw IllegalArgumentException(\"Unknown worker class name: $workerClassName\")\n \n return factoryProvider.get().create(appContext, workerParameters)\n }\n}\n\n/**\n * Interface for creating workers with dependencies\n */\ninterface ChildWorkerFactory {\n fun create(appContext: Context, params: WorkerParameters): ListenableWorker\n}\n\n/**\n * Dagger key for worker factories\n */\n@MapKey\n@Retention(AnnotationRetention.RUNTIME)\n@Target(AnnotationTarget.FUNCTION)\nannotation class WorkerKey(val value: KClass)\n\n/**\n * Module for worker dependency injection\n */\n@Module\n@InstallIn(SingletonComponent::class)\ninterface WorkerModule {\n \n @Binds\n @Singleton\n fun bindWorkerFactory(factory: HiltWorkerFactory): WorkerFactory\n \n @Binds\n @IntoMap\n @WorkerKey(DownloadModelWorker::class)\n fun bindDownloadModelWorker(factory: DownloadModelWorker.Factory): ChildWorkerFactory\n \n @Binds\n @IntoMap\n @WorkerKey(ModelUpdateWorker::class)\n fun bindModelUpdateWorker(factory: ModelUpdateWorker.Factory): ChildWorkerFactory\n \n @Binds\n @IntoMap\n @WorkerKey(UploadTelemetryWorker::class)\n fun bindUploadTelemetryWorker(factory: UploadTelemetryWorker.Factory): ChildWorkerFactory\n}\n\n@Module\n@InstallIn(SingletonComponent::class)\nobject AppModule {\n @Provides\n @Singleton\n fun provideAppLogger(context: Context, fileUtils: FileUtils): AppLogger {\n return AppLogger(context, fileUtils)\n }\n\n @Provides\n @Singleton\n fun providePrefs(context: Context): Prefs {\n return Prefs(context)\n }\n\n @Provides\n @Singleton\n fun provideJsonUtils(): JsonUtils {\n return JsonUtils()\n }\n}\n\n/**\n * Factory for creating DownloadModelWorker instances with dependencies\n */\nclass DownloadModelWorkerFactory @Inject constructor(\n private val worker: Provider\n) : ChildWorkerFactory {\n \n override fun create(appContext: Context, params: WorkerParameters): ListenableWorker {\n return worker.get()\n }\n \n @AssistedFactory\n interface Factory : ChildWorkerFactory {\n override fun create(appContext: Context, params: WorkerParameters): DownloadModelWorker\n }\n}\n\n/**\n * Factory for creating ModelUpdateWorker instances with dependencies\n */\nclass ModelUpdateWorkerFactory @Inject constructor(\n private val worker: Provider\n) : ChildWorkerFactory {\n \n override fun create(appContext: Context, params: WorkerParameters): ListenableWorker {\n return worker.get()\n }\n \n @AssistedFactory\n interface Factory : ChildWorkerFactory {\n override fun create(appContext: Context, params: WorkerParameters): ModelUpdateWorker\n }\n}\n\n/**\n * Factory for creating UploadTelemetryWorker instances with dependencies\n */\nclass UploadTelemetryWorkerFactory @Inject constructor(\n private val worker: Provider\n) : ChildWorkerFactory {\n \n override fun create(appContext: Context, params: WorkerParameters): ListenableWorker {\n return worker.get()\n }\n \n @AssistedFactory\n interface Factory : ChildWorkerFactory {\n override fun create(appContext: Context, params: WorkerParameters): UploadTelemetryWorker\n }\n}\n", "size": 4540, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/util/JsonUtils.kt": { "content": "package com.example.emotiondetector.util\n\nimport com.google.gson.*\nimport com.google.gson.reflect.TypeToken\nimport java.lang.reflect.Type\nimport java.util.*\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Utility class for JSON serialization and deserialization using Gson\n */\n@Singleton\nclass JsonUtils @Inject constructor() {\n /**\n * Custom Gson instance with custom type adapters and settings\n */\n val gson: Gson = GsonBuilder()\n .setPrettyPrinting()\n .setDateFormat(\"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\")\n .registerTypeAdapter(Date::class.java, DateDeserializer())\n .registerTypeAdapter(Date::class.java, DateSerializer())\n .create()\n \n /**\n * Convert an object to JSON string\n */\n fun toJson(obj: Any?): String = gson.toJson(obj)\n \n /**\n * Convert a JSON string to an object of the specified type\n */\n inline fun fromJson(json: String?): T? {\n return try {\n if (json.isNullOrBlank()) null else gson.fromJson(json, object : TypeToken() {}.type)\n } catch (e: Exception) {\n null\n }\n }\n \n /**\n * Convert a JSON string to a list of objects of the specified type\n */\n inline fun listFromJson(json: String?): List {\n return try {\n val type = object : TypeToken>() {}.type\n if (json.isNullOrBlank()) emptyList() else gson.fromJson(json, type) ?: emptyList()\n } catch (e: Exception) {\n emptyList()\n }\n }\n \n /**\n * Convert a JSON string to a map with string keys and values of the specified type\n */\n inline fun mapFromJson(json: String?): Map {\n return try {\n val type = object : TypeToken>() {}.type\n if (json.isNullOrBlank()) emptyMap() else gson.fromJson(json, type) ?: emptyMap()\n } catch (e: Exception) {\n emptyMap()\n }\n }\n \n /**\n * Create a deep copy of an object by serializing and deserializing it\n */\n inline fun deepCopy(obj: T): T? {\n return try {\n fromJson(toJson(obj))\n } catch (e: Exception) {\n null\n }\n }\n \n /**\n * Convert an object to a map\n */\n fun toMap(obj: Any?): Map {\n return try {\n val json = toJson(obj)\n gson.fromJson(json, object : TypeToken>() {}.type)\n } catch (e: Exception) {\n emptyMap()\n }\n }\n \n /**\n * Convert a map to an object of the specified type\n */\n inline fun fromMap(map: Map<*, *>?): T? {\n return try {\n val json = toJson(map)\n fromJson(json)\n } catch (e: Exception) {\n null\n }\n }\n \n /**\n * Pretty print a JSON string\n */\n fun prettyPrint(json: String?): String {\n return try {\n val jsonElement = JsonParser.parseString(json)\n gson.toJson(jsonElement)\n } catch (e: Exception) {\n json ?: \"\"\n }\n }\n \n /**\n * Check if a string is valid JSON\n */\n fun isValidJson(json: String?): Boolean {\n if (json.isNullOrBlank()) return false\n return try {\n JsonParser.parseString(json)\n true\n } catch (e: Exception) {\n false\n }\n }\n \n /**\n * Merge two JSON strings\n */\n fun mergeJson(json1: String, json2: String): String {\n return try {\n val jsonElement1 = JsonParser.parseString(json1).asJsonObject\n val jsonElement2 = JsonParser.parseString(json2).asJsonObject\n \n for ((key, value) in jsonElement2.entrySet()) {\n jsonElement1.add(key, value)\n }\n \n gson.toJson(jsonElement1)\n } catch (e: Exception) {\n json1\n }\n }\n \n /**\n * Custom date deserializer for Gson\n */\n private class DateDeserializer : JsonDeserializer {\n override fun deserialize(\n json: JsonElement,\n typeOfT: Type?,\n context: JsonDeserializationContext?\n ): Date {\n return try {\n val dateStr = json.asString\n // Try ISO 8601 format\n val date = try {\n java.time.OffsetDateTime.parse(dateStr).toInstant().toEpochMilli()\n } catch (e: Exception) {\n // Try other formats if needed\n SimpleDateFormat(\"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\", Locale.US)\n .apply { timeZone = TimeZone.getTimeZone(\"UTC\") }\n .parse(dateStr)?.time ?: 0L\n }\n Date(date)\n } catch (e: Exception) {\n Date(0)\n }\n }\n }\n \n /**\n * Custom date serializer for Gson\n */\n private class DateSerializer : JsonSerializer {\n override fun serialize(\n src: Date?,\n typeOfSrc: Type?,\n context: JsonSerializationContext?\n ): JsonElement {\n return JsonPrimitive(\n SimpleDateFormat(\"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\", Locale.US)\n .apply { timeZone = TimeZone.getTimeZone(\"UTC\") }\n .format(src ?: Date(0))\n )\n }\n }\n \n companion object {\n /**\n * Get a Gson instance with default settings\n */\n fun getDefaultGson(): Gson = GsonBuilder()\n .setDateFormat(\"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\")\n .create()\n \n /**\n * Extension function to convert any object to JSON string\n */\n fun Any?.toJsonString(): String = JsonUtils().toJson(this)\n \n /**\n * Extension function to parse JSON string to object\n */\n inline fun String?.fromJsonString(): T? = JsonUtils().fromJson(this)\n \n /**\n * Extension function to convert map to object\n */\n inline fun Map<*, *>?.toObject(): T? = JsonUtils().fromMap(this ?: emptyMap())\n \n /**\n * Extension function to convert object to map\n */\n fun Any?.toMap(): Map = JsonUtils().toMap(this)\n }\n}\n", "size": 6395, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/util/Prefs.kt": { "content": "package com.example.emotiondetector.util\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport androidx.core.content.edit\nimport com.example.emotiondetector.di.AppScope\nimport kotlin.properties.ReadWriteProperty\nimport kotlin.reflect.KProperty\n\n/**\n * Type-safe shared preferences wrapper\n */\n@AppScope\nclass Prefs @Inject constructor(\n private val context: Context,\n private val prefsName: String = \"${context.packageName}_prefs\"\n) {\n private val prefs: SharedPreferences by lazy {\n context.getSharedPreferences(prefsName, Context.MODE_PRIVATE)\n }\n\n // Primitive types\n var int: Int by PrefDelegate(0)\n var long: Long by PrefDelegate(0L)\n var float: Float by PrefDelegate(0f)\n var boolean: Boolean by PrefDelegate(false)\n var string: String by PrefDelegate(\"\")\n \n // Nullable types\n var nullableInt: Int? by NullablePrefDelegate()\n var nullableLong: Long? by NullablePrefDelegate()\n var nullableFloat: Float? by NullablePrefDelegate()\n var nullableBoolean: Boolean? by NullablePrefDelegate()\n var nullableString: String? by NullablePrefDelegate()\n \n // Object types (serialized to JSON)\n inline fun json(defaultValue: T? = null) =\n JsonPrefDelegate(defaultValue, defaultValue != null)\n \n // Set of strings\n var stringSet: Set by StringSetPrefDelegate()\n \n // Enum support\n inline fun > enum(defaultValue: T) =\n EnumPrefDelegate(defaultValue)\n \n // Clear all preferences\n fun clear() = prefs.edit().clear().apply()\n \n // Remove a specific key\n fun remove(key: String) = prefs.edit().remove(key).apply()\n \n // Check if a key exists\n fun contains(key: String) = prefs.contains(key)\n \n // Get all keys\n val all: Map get() = prefs.all\n \n // Register/Unregister preference change listener\n fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {\n prefs.registerOnSharedPreferenceChangeListener(listener)\n }\n \n fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {\n prefs.unregisterOnSharedPreferenceChangeListener(listener)\n }\n \n // Base delegate for non-nullable types\n inner class PrefDelegate(\n private val defaultValue: T\n ) : ReadWriteProperty {\n @Suppress(\"UNCHECKED_CAST\")\n override fun getValue(thisRef: Any, property: KProperty<*>): T {\n return when (defaultValue) {\n is Int -> prefs.getInt(property.name, defaultValue) as T\n is Long -> prefs.getLong(property.name, defaultValue) as T\n is Float -> prefs.getFloat(property.name, defaultValue) as T\n is Boolean -> prefs.getBoolean(property.name, defaultValue) as T\n is String -> prefs.getString(property.name, defaultValue) as T\n else -> throw IllegalArgumentException(\"Unsupported type\")\n }\n }\n \n override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {\n prefs.edit {\n when (value) {\n is Int -> putInt(property.name, value)\n is Long -> putLong(property.name, value)\n is Float -> putFloat(property.name, value)\n is Boolean -> putBoolean(property.name, value)\n is String -> putString(property.name, value)\n else -> throw IllegalArgumentException(\"Unsupported type\")\n }\n }\n }\n }\n \n // Delegate for nullable types\n inner class NullablePrefDelegate : ReadWriteProperty {\n @Suppress(\"UNCHECKED_CAST\")\n override fun getValue(thisRef: Any, property: KProperty<*>): T? {\n return when (property.returnType.classifier) {\n Int::class -> if (prefs.contains(property.name)) prefs.getInt(property.name, 0) as T else null\n Long::class -> if (prefs.contains(property.name)) prefs.getLong(property.name, 0L) as T else null\n Float::class -> if (prefs.contains(property.name)) prefs.getFloat(property.name, 0f) as T else null\n Boolean::class -> if (prefs.contains(property.name)) prefs.getBoolean(property.name, false) as T else null\n String::class -> prefs.getString(property.name, null) as T?\n else -> throw IllegalArgumentException(\"Unsupported type\")\n }\n }\n \n override fun setValue(thisRef: Any, property: KProperty<*>, value: T?) {\n prefs.edit {\n when (value) {\n is Int -> putInt(property.name, value)\n is Long -> putLong(property.name, value)\n is Float -> putFloat(property.name, value)\n is Boolean -> putBoolean(property.name, value)\n is String -> putString(property.name, value)\n null -> remove(property.name)\n else -> throw IllegalArgumentException(\"Unsupported type\")\n }\n }\n }\n }\n \n // Delegate for JSON serialization\n inner class JsonPrefDelegate(\n private val defaultValue: T? = null,\n private val hasDefault: Boolean = false\n ) : ReadWriteProperty {\n private val gson by lazy { JsonUtils.gson }\n \n override fun getValue(thisRef: Any, property: KProperty<*>): T? {\n val json = prefs.getString(property.name, null) ?: return defaultValue\n return try {\n gson.fromJson(json, object : com.google.gson.reflect.TypeToken() {}.type)\n } catch (e: Exception) {\n if (hasDefault) defaultValue else null\n }\n }\n \n override fun setValue(thisRef: Any, property: KProperty<*>, value: T?) {\n prefs.edit {\n if (value == null) {\n remove(property.name)\n } else {\n putString(property.name, gson.toJson(value))\n }\n }\n }\n }\n \n // Delegate for string sets\n inner class StringSetPrefDelegate : ReadWriteProperty> {\n override fun getValue(thisRef: Any, property: KProperty<*>): Set {\n return prefs.getStringSet(property.name, emptySet()) ?: emptySet()\n }\n \n override fun setValue(thisRef: Any, property: KProperty<*>, value: Set) {\n prefs.edit {\n putStringSet(property.name, value.toSet()) // Create a new set to avoid caching issues\n }\n }\n }\n \n // Delegate for enums\n inner class EnumPrefDelegate>(\n private val defaultValue: T\n ) : ReadWriteProperty {\n override fun getValue(thisRef: Any, property: KProperty<*>): T {\n val value = prefs.getString(property.name, null)\n return value?.let {\n try {\n enumValueOf(it)\n } catch (e: Exception) {\n defaultValue\n }\n } ?: defaultValue\n }\n \n override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {\n prefs.edit {\n putString(property.name, value.name)\n }\n }\n }\n \n companion object {\n // Extension function for easier access\n fun Context.prefs(name: String = \"${this.packageName}_prefs\") = Prefs(this, name)\n }\n}\n\n// Extension functions for easier preference access\ninline fun Prefs.json(\n key: String,\n defaultValue: T? = null\n): ReadWriteProperty = JsonPrefDelegate(defaultValue, defaultValue != null).apply { \n // We need to store the key somewhere, but since we can't modify the property name,\n // we'll use a custom delegate that takes the key as a parameter\n object : ReadWriteProperty {\n private var value: T? = null\n private var initialized = false\n \n override fun getValue(thisRef: Any, property: KProperty<*>): T? {\n if (!initialized) {\n value = this@json.getValue(thisRef, property)\n initialized = true\n }\n return value\n }\n \n override fun setValue(thisRef: Any, property: KProperty<*>, value: T?) {\n this.value = value\n this@json.setValue(thisRef, property, value)\n }\n }\n}\n\n// Extension for enum preferences\ninline fun > Prefs.enum(\n key: String,\n defaultValue: T\n): ReadWriteProperty = object : ReadWriteProperty {\n private var value: T = defaultValue\n private var initialized = false\n \n override fun getValue(thisRef: Any, property: KProperty<*>): T {\n if (!initialized) {\n val name = prefs.getString(key, null)\n value = name?.let { \n try { \n enumValueOf(it) \n } catch (e: Exception) { \n defaultValue \n } \n } ?: defaultValue\n initialized = true\n }\n return value\n }\n \n override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {\n this.value = value\n prefs.edit { putString(key, value.name) }\n }\n}\n", "size": 9476, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/util/ImageUtils.kt": { "content": "package com.example.emotiondetector.util\n\nimport android.graphics.*\nimport android.media.Image\nimport android.renderscript.*\nimport androidx.camera.core.ImageProxy\nimport androidx.core.graphics.scale\nimport com.example.emotiondetector.di.AppScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport java.io.ByteArrayOutputStream\nimport java.nio.ByteBuffer\nimport javax.inject.Inject\nimport kotlin.math.max\nimport kotlin.math.min\n\n/**\n * Utility class for image processing operations\n */\n@AppScope\nclass ImageUtils @Inject constructor() {\n\n companion object {\n private const val TAG = \"ImageUtils\"\n \n // Standard emotion labels in the model's output order\n val EMOTION_LABELS = listOf(\"angry\", \"disgust\", \"fear\", \"happy\", \"neutral\", \"sad\", \"surprise\")\n }\n \n /**\n * Convert an ImageProxy to a Bitmap\n */\n fun imageProxyToBitmap(image: ImageProxy, rotationDegrees: Int = 0): Bitmap {\n val yBuffer = image.planes[0].buffer\n val uBuffer = image.planes[1].buffer\n val vBuffer = image.planes[2].buffer\n \n val ySize = yBuffer.remaining()\n val uSize = uBuffer.remaining()\n val vSize = vBuffer.remaining()\n \n val nv21 = ByteArray(ySize + uSize + vSize)\n \n // U and V are swapped\n yBuffer.get(nv21, 0, ySize)\n vBuffer.get(nv21, ySize, vSize)\n uBuffer.get(nv21, ySize + vSize, uSize)\n \n val yuvImage = YuvImage(nv21, ImageFormat.NV21, image.width, image.height, null)\n val out = ByteArrayOutputStream()\n yuvImage.compressToJpeg(Rect(0, 0, image.width, image.height), 100, out)\n val imageBytes = out.toByteArray()\n \n val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)\n \n // Apply rotation if needed\n return if (rotationDegrees != 0) {\n rotateBitmap(bitmap, rotationDegrees.toFloat())\n } else {\n bitmap\n }\n }\n \n /**\n * Rotate a bitmap by the specified degrees\n */\n fun rotateBitmap(source: Bitmap, degrees: Float): Bitmap {\n val matrix = Matrix().apply {\n postRotate(degrees)\n postScale(-1f, -1f) // Flip to match camera preview\n }\n return Bitmap.createBitmap(\n source, 0, 0, source.width, source.height, matrix, true\n )\n }\n \n /**\n * Crop a bitmap to a square from the center\n */\n fun cropToSquare(bitmap: Bitmap): Bitmap {\n val size = min(bitmap.width, bitmap.height)\n val x = (bitmap.width - size) / 2\n val y = (bitmap.height - size) / 2\n return Bitmap.createBitmap(bitmap, x, y, size, size)\n }\n \n /**\n * Resize a bitmap to the specified dimensions\n */\n fun resizeBitmap(bitmap: Bitmap, width: Int, height: Int): Bitmap {\n return bitmap.scale(width, height, true)\n }\n \n /**\n * Convert a bitmap to grayscale\n */\n fun toGrayscale(bitmap: Bitmap): Bitmap {\n val result = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)\n val canvas = Canvas(result)\n val paint = Paint()\n \n val colorMatrix = ColorMatrix()\n colorMatrix.setSaturation(0f)\n \n val filter = ColorMatrixColorFilter(colorMatrix)\n paint.colorFilter = filter\n \n canvas.drawBitmap(bitmap, 0f, 0f, paint)\n return result\n }\n \n /**\n * Normalize pixel values to [-1, 1] range\n */\n fun normalizeBitmap(bitmap: Bitmap, mean: Float = 0f, std: Float = 255f): FloatArray {\n val pixels = IntArray(bitmap.width * bitmap.height)\n bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)\n \n return FloatArray(pixels.size) { i ->\n val pixel = pixels[i]\n val gray = (Color.red(pixel) * 0.299f + \n Color.green(pixel) * 0.587f + \n Color.blue(pixel) * 0.114f)\n (gray - mean) / std\n }\n }\n \n /**\n * Draw emotion probabilities on a bitmap\n */\n fun drawEmotionProbabilities(\n bitmap: Bitmap,\n probabilities: FloatArray,\n labels: List = EMOTION_LABELS\n ): Bitmap {\n val result = bitmap.copy(Bitmap.Config.ARGB_8888, true)\n val canvas = Canvas(result)\n \n val paint = Paint().apply {\n textSize = 32f\n color = Color.WHITE\n style = Paint.Style.FILL\n isAntiAlias = true\n textAlign = Paint.Align.LEFT\n }\n \n // Draw background for text\n val textBg = Paint().apply {\n color = Color.argb(128, 0, 0, 0)\n style = Paint.Style.FILL\n }\n \n // Calculate text bounds for background\n val textBounds = Rect()\n val textHeight = paint.fontMetrics.let { it.descent - it.ascent } + 4\n \n // Draw each emotion and its probability\n labels.forEachIndexed { index, label ->\n val prob = probabilities.getOrNull(index) ?: 0f\n val text = \"$label: ${String.format(\"%.2f\", prob)}\"\n \n paint.getTextBounds(text, 0, text.length, textBounds)\n \n // Draw background\n canvas.drawRect(\n 0f,\n index * textHeight,\n (textBounds.width() + 20).toFloat(),\n (index + 1) * textHeight,\n textBg\n )\n \n // Draw text\n canvas.drawText(\n text,\n 10f,\n (index + 1) * textHeight - paint.fontMetrics.descent,\n paint\n )\n }\n \n return result\n }\n \n /**\n * Calculate the optimal preview size based on the target aspect ratio and available sizes\n */\n fun getOptimalPreviewSize(\n sizes: List,\n targetRatio: Float,\n maxWidth: Int = Int.MAX_VALUE,\n maxHeight: Int = Int.MAX_VALUE\n ): android.util.Size? {\n // Try to find a size with the target aspect ratio and within max dimensions\n val bigEnough = sizes.filter {\n val ratio = it.width.toFloat() / it.height\n Math.abs(ratio - targetRatio) <= 0.1f &&\n it.width <= maxWidth &&\n it.height <= maxHeight\n }\n \n // If we have any matching sizes, return the largest one\n if (bigEnough.isNotEmpty()) {\n return bigEnough.maxByOrNull { it.width * it.height }!!\n }\n \n // Otherwise, return the largest size that fits within max dimensions\n return sizes.filter {\n it.width <= maxWidth && it.height <= maxHeight\n }.maxByOrNull { it.width * it.height }\n }\n}\n", "size": 6850, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/util/PermissionUtils.kt": { "content": "package com.example.emotiondetector.util\n\nimport android.Manifest\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.net.Uri\nimport android.provider.Settings\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.SideEffect\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.core.content.ContextCompat\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentActivity\nimport com.google.accompanist.permissions.ExperimentalPermissionsApi\nimport com.google.accompanist.permissions.MultiplePermissionsState\nimport com.google.accompanist.permissions.PermissionState\nimport com.google.accompanist.permissions.rememberMultiplePermissionsState\nimport com.google.accompanist.permissions.rememberPermissionState\n\n/**\n * Utility class for handling runtime permissions\n */\nobject PermissionUtils {\n // Common permission groups\n object Permissions {\n val CAMERA = arrayOf(Manifest.permission.CAMERA)\n val STORAGE = arrayOf(\n Manifest.permission.READ_EXTERNAL_STORAGE,\n Manifest.permission.WRITE_EXTERNAL_STORAGE\n )\n val LOCATION = arrayOf(\n Manifest.permission.ACCESS_FINE_LOCATION,\n Manifest.permission.ACCESS_COARSE_LOCATION\n )\n }\n\n \n /**\n * Check if all permissions in the given array are granted\n */\n fun hasPermissions(context: Context, permissions: Array): Boolean {\n return permissions.all {\n ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED\n }\n }\n \n /**\n * Check if the permission rationale should be shown\n */\n fun shouldShowRequestPermissionRationale(activity: Activity, permission: String): Boolean {\n return activity.shouldShowRequestPermissionRationale(permission)\n }\n \n /**\n * Open app settings to allow the user to grant permissions manually\n */\n fun openAppSettings(context: Context) {\n val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {\n data = Uri.fromParts(\"package\", context.packageName, null)\n flags = Intent.FLAG_ACTIVITY_NEW_TASK\n }\n context.startActivity(intent)\n }\n \n /**\n * Composable function to request a single permission\n */\n @Composable\n fun RequestPermission(\n permission: String,\n onPermissionDenied: () -> Unit = {},\n onPermissionGranted: () -> Unit = {},\n content: @Composable (PermissionState) -> Unit\n ) {\n val permissionState = rememberPermissionState(permission) { isGranted ->\n if (isGranted) {\n onPermissionGranted()\n } else {\n onPermissionDenied()\n }\n }\n \n // Request permission when the composable is first composed\n SideEffect {\n permissionState.launchPermissionRequest()\n }\n \n content(permissionState)\n }\n \n /**\n * Composable function to request multiple permissions\n */\n @OptIn(ExperimentalPermissionsApi::class)\n @Composable\n fun RequestMultiplePermissions(\n permissions: List,\n onPermissionsDenied: (List) -> Unit = {},\n onPermissionsGranted: () -> Unit = {},\n content: @Composable (MultiplePermissionsState) -> Unit\n ) {\n val permissionsState = rememberMultiplePermissionsState(permissions)\n \n // Request permissions when the composable is first composed\n SideEffect {\n if (!permissionsState.allPermissionsGranted) {\n permissionsState.launchMultiplePermissionRequest()\n }\n }\n \n // Handle permission results\n SideEffect {\n if (permissionsState.allPermissionsGranted) {\n onPermissionsGranted()\n } else if (permissionsState.shouldShowRationale) {\n val deniedPermissions = permissionsState.permissions\n .filter { !it.status.isGranted }\n .map { it.permission }\n onPermissionsDenied(deniedPermissions)\n }\n }\n \n content(permissionsState)\n }\n \n /**\n * Composable function to handle camera permission\n */\n @Composable\n fun CameraPermission(\n onPermissionGranted: @Composable () -> Unit,\n onPermissionDenied: @Composable () -> Unit,\n onPermissionRationale: @Composable (onRequestPermission: () -> Unit) -> Unit = { it() }\n ) {\n val context = LocalContext.current\n val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)\n \n when {\n cameraPermissionState.hasPermission -> {\n onPermissionGranted()\n }\n cameraPermissionState.shouldShowRationale -> {\n onPermissionRationale {\n cameraPermissionState.launchPermissionRequest()\n }\n }\n else -> {\n onPermissionDenied()\n }\n }\n }\n \n /**\n * Extension function to check if all permissions are granted\n */\n fun MultiplePermissionsState.hasAllPermissionsGranted(): Boolean {\n return permissions.all { it.status.isGranted }\n }\n \n /**\n * Extension function to check if any permission should show rationale\n */\n fun MultiplePermissionsState.shouldShowRationale(): Boolean {\n return permissions.any { it.status.shouldShowRationale }\n }\n \n /**\n * Extension function to request permissions from an Activity\n */\n fun FragmentActivity.requestPermissions(\n permissions: Array,\n requestCode: Int,\n onPermissionsResult: (Map) -> Unit\n ) {\n val launcher = registerForActivityResult(\n ActivityResultContracts.RequestMultiplePermissions()\n ) { permissionsResult ->\n onPermissionsResult(permissionsResult)\n }\n launcher.launch(permissions)\n }\n \n /**\n * Extension function to request permissions from a Fragment\n */\n fun Fragment.requestPermissions(\n permissions: Array,\n requestCode: Int,\n onPermissionsResult: (Map) -> Unit\n ) {\n val launcher = registerForActivityResult(\n ActivityResultContracts.RequestMultiplePermissions()\n ) { permissionsResult ->\n onPermissionsResult(permissionsResult)\n }\n launcher.launch(permissions)\n }\n}\n", "size": 6773, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/util/DateTimeUtils.kt": { "content": "package com.example.emotiondetector.util\n\nimport java.text.SimpleDateFormat\nimport java.util.*\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Utility class for date and time operations\n */\n@Singleton\nclass DateTimeUtils @Inject constructor() {\n companion object {\n // Common date/time patterns\n const val PATTERN_ISO_8601 = \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\" // 2023-05-15T14:30:45.123Z\n const val PATTERN_DATE = \"yyyy-MM-dd\" // 2023-05-15\n const val PATTERN_TIME = \"HH:mm:ss\" // 14:30:45\n const val PATTERN_DATE_TIME = \"yyyy-MM-dd HH:mm:ss\" // 2023-05-15 14:30:45\n const val PATTERN_FILE_TIMESTAMP = \"yyyyMMdd_HHmmss\" // 20230515_143045\n const val PATTERN_READABLE_DATE = \"MMM d, yyyy\" // May 15, 2023\n const val PATTERN_READABLE_TIME = \"h:mm a\" // 2:30 PM\n const val PATTERN_READABLE_DATE_TIME = \"MMM d, yyyy h:mm a\" // May 15, 2023 2:30 PM\n \n // Time constants in milliseconds\n const val SECOND_MILLIS = 1000L\n const val MINUTE_MILLIS = 60 * SECOND_MILLIS\n const val HOUR_MILLIS = 60 * MINUTE_MILLIS\n const val DAY_MILLIS = 24 * HOUR_MILLIS\n const val WEEK_MILLIS = 7 * DAY_MILLIS\n }\n \n private val defaultLocale = Locale.getDefault()\n \n /**\n * Get current timestamp in milliseconds\n */\n fun currentTimeMillis(): Long = System.currentTimeMillis()\n \n /**\n * Format a timestamp to a string using the specified pattern\n */\n fun formatTimestamp(\n timestamp: Long,\n pattern: String = PATTERN_ISO_8601,\n locale: Locale = defaultLocale\n ): String {\n return SimpleDateFormat(pattern, locale).format(Date(timestamp))\n }\n \n /**\n * Parse a date string to a timestamp using the specified pattern\n */\n fun parseTimestamp(\n dateString: String,\n pattern: String = PATTERN_ISO_8601,\n locale: Locale = defaultLocale\n ): Long? {\n return try {\n SimpleDateFormat(pattern, locale).parse(dateString)?.time\n } catch (e: Exception) {\n null\n }\n }\n \n /**\n * Get a human-readable relative time string (e.g., \"2 hours ago\")\n */\n fun getRelativeTimeSpanString(\n timestamp: Long,\n now: Long = currentTimeMillis(),\n minResolution: Long = 0,\n flags: Int = 0\n ): String {\n val diff = now - timestamp\n \n return when {\n diff < MINUTE_MILLIS -> \"Just now\"\n diff < 2 * MINUTE_MILLIS -> \"A minute ago\"\n diff < 50 * MINUTE_MILLIS -> \"${diff / MINUTE_MILLIS} minutes ago\"\n diff < 90 * MINUTE_MILLIS -> \"An hour ago\"\n diff < 24 * HOUR_MILLIS -> \"${diff / HOUR_MILLIS} hours ago\"\n diff < 48 * HOUR_MILLIS -> \"Yesterday\"\n diff < 7 * DAY_MILLIS -> \"${diff / DAY_MILLIS} days ago\"\n diff < 2 * WEEK_MILLIS -> \"Last week\"\n diff < 4 * WEEK_MILLIS -> \"${diff / WEEK_MILLIS} weeks ago\"\n diff < 30 * DAY_MILLIS -> \"${diff / (7 * DAY_MILLIS)} weeks ago\"\n diff < 12 * 4 * WEEK_MILLIS -> \"${diff / (30 * DAY_MILLIS)} months ago\"\n else -> formatTimestamp(timestamp, PATTERN_READABLE_DATE)\n }\n }\n \n /**\n * Get the start of the day (00:00:00) for the given timestamp\n */\n fun getStartOfDay(timestamp: Long): Long {\n val calendar = Calendar.getInstance().apply {\n timeInMillis = timestamp\n set(Calendar.HOUR_OF_DAY, 0)\n set(Calendar.MINUTE, 0)\n set(Calendar.SECOND, 0)\n set(Calendar.MILLISECOND, 0)\n }\n return calendar.timeInMillis\n }\n \n /**\n * Get the end of the day (23:59:59.999) for the given timestamp\n */\n fun getEndOfDay(timestamp: Long): Long {\n val calendar = Calendar.getInstance().apply {\n timeInMillis = timestamp\n set(Calendar.HOUR_OF_DAY, 23)\n set(Calendar.MINUTE, 59)\n set(Calendar.SECOND, 59)\n set(Calendar.MILLISECOND, 999)\n }\n return calendar.timeInMillis\n }\n \n /**\n * Add days to a timestamp\n */\n fun addDays(timestamp: Long, days: Int): Long {\n val calendar = Calendar.getInstance().apply {\n timeInMillis = timestamp\n add(Calendar.DAY_OF_YEAR, days)\n }\n return calendar.timeInMillis\n }\n \n /**\n * Calculate the difference in days between two timestamps\n */\n fun getDaysDifference(from: Long, to: Long = currentTimeMillis()): Int {\n val diff = getStartOfDay(to) - getStartOfDay(from)\n return (diff / DAY_MILLIS).toInt()\n }\n \n /**\n * Format a duration in milliseconds to a human-readable string (e.g., \"2h 30m\")\n */\n fun formatDuration(durationMs: Long): String {\n val seconds = (durationMs / 1000) % 60\n val minutes = (durationMs / (1000 * 60)) % 60\n val hours = (durationMs / (1000 * 60 * 60)) % 24\n val days = durationMs / (1000 * 60 * 60 * 24)\n \n return when {\n days > 0 -> \"${days}d ${hours}h ${minutes}m\"\n hours > 0 -> \"${hours}h ${minutes}m ${seconds}s\"\n minutes > 0 -> \"${minutes}m ${seconds}s\"\n else -> \"${seconds}s\"\n }\n }\n \n /**\n * Check if two timestamps are on the same day\n */\n fun isSameDay(timestamp1: Long, timestamp2: Long): Boolean {\n val cal1 = Calendar.getInstance().apply { timeInMillis = timestamp1 }\n val cal2 = Calendar.getInstance().apply { timeInMillis = timestamp2 }\n \n return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&\n cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)\n }\n \n /**\n * Get the current time in ISO 8601 format\n */\n fun getCurrentIso8601(): String {\n return formatTimestamp(currentTimeMillis(), PATTERN_ISO_8601)\n }\n \n /**\n * Get the current time in a human-readable format\n */\n fun getCurrentReadableTime(): String {\n return formatTimestamp(currentTimeMillis(), PATTERN_READABLE_TIME)\n }\n \n /**\n * Get the current date in a human-readable format\n */\n fun getCurrentReadableDate(): String {\n return formatTimestamp(currentTimeMillis(), PATTERN_READABLE_DATE)\n }\n \n /**\n * Get the current date and time in a human-readable format\n */\n fun getCurrentReadableDateTime(): String {\n return formatTimestamp(currentTimeMillis(), PATTERN_READABLE_DATE_TIME)\n }\n \n /**\n * Get a timestamp for a specific date and time\n */\n fun getTimestamp(\n year: Int,\n month: Int, // 0-11\n day: Int,\n hour: Int = 0,\n minute: Int = 0,\n second: Int = 0\n ): Long {\n val calendar = Calendar.getInstance().apply {\n set(Calendar.YEAR, year)\n set(Calendar.MONTH, month)\n set(Calendar.DAY_OF_MONTH, day)\n set(Calendar.HOUR_OF_DAY, hour)\n set(Calendar.MINUTE, minute)\n set(Calendar.SECOND, second)\n set(Calendar.MILLISECOND, 0)\n }\n return calendar.timeInMillis\n }\n \n /**\n * Get the age in years from a birth date timestamp\n */\n fun getAge(birthDate: Long): Int {\n val birthCalendar = Calendar.getInstance().apply { timeInMillis = birthDate }\n val now = Calendar.getInstance()\n \n var age = now.get(Calendar.YEAR) - birthCalendar.get(Calendar.YEAR)\n \n // Adjust age if birthday hasn't occurred yet this year\n if (now.get(Calendar.DAY_OF_YEAR) < birthCalendar.get(Calendar.DAY_OF_YEAR)) {\n age--\n }\n \n return age\n }\n}\n", "size": 7750, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/util/AppLogger.kt": { "content": "package com.example.emotiondetector.util\n\nimport android.content.Context\nimport android.util.Log\nimport com.example.emotiondetector.di.AppScope\nimport java.io.*\nimport java.text.SimpleDateFormat\nimport java.util.*\nimport java.util.concurrent.Executors\nimport java.util.concurrent.TimeUnit\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Logging utility class for the application\n */\n@Singleton\nclass AppLogger @Inject constructor(\n private val context: Context,\n private val fileUtils: FileUtils\n) {\n companion object {\n private const val TAG = \"EmotionDetector\"\n private const val MAX_LOG_FILE_SIZE = 2 * 1024 * 1024 // 2MB\n private const val MAX_LOG_FILES = 5\n private const val LOG_FILE_PREFIX = \"app_log_\"\n private const val LOG_FILE_EXT = \".txt\"\n \n // Log levels\n const val VERBOSE = 0\n const val DEBUG = 1\n const val INFO = 2\n const val WARN = 3\n const val ERROR = 4\n const val ASSERT = 5\n }\n \n private val logDir: File by lazy {\n fileUtils.createAppDir(FileUtils.DIR_LOGS)\n }\n \n private val dateFormat = SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss.SSS\", Locale.getDefault())\n private val logExecutor = Executors.newSingleThreadExecutor()\n \n private var logLevel = if (BuildConfig.DEBUG) VERBOSE else INFO\n private var logToFile = !BuildConfig.DEBUG // Log to file in release by default\n \n /**\n * Set the minimum log level\n */\n fun setLogLevel(level: Int) {\n logLevel = level.coerceIn(VERBOSE, ASSERT)\n }\n \n /**\n * Enable or disable file logging\n */\n fun setFileLogging(enabled: Boolean) {\n logToFile = enabled\n }\n \n /**\n * Log a verbose message\n */\n fun v(tag: String = TAG, message: String, throwable: Throwable? = null) {\n log(VERBOSE, tag, message, throwable)\n }\n \n /**\n * Log a debug message\n */\n fun d(tag: String = TAG, message: String, throwable: Throwable? = null) {\n log(DEBUG, tag, message, throwable)\n }\n \n /**\n * Log an info message\n */\n fun i(tag: String = TAG, message: String, throwable: Throwable? = null) {\n log(INFO, tag, message, throwable)\n }\n \n /**\n * Log a warning message\n */\n fun w(tag: String = TAG, message: String, throwable: Throwable? = null) {\n log(WARN, tag, message, throwable)\n }\n \n /**\n * Log an error message\n */\n fun e(tag: String = TAG, message: String, throwable: Throwable? = null) {\n log(ERROR, tag, message, throwable)\n }\n \n /**\n * Log a WTF (What a Terrible Failure) message\n */\n fun wtf(tag: String = TAG, message: String, throwable: Throwable? = null) {\n log(ASSERT, tag, message, throwable)\n }\n \n /**\n * Get all log files\n */\n fun getLogFiles(): List {\n return logDir.listFiles { _, name ->\n name.startsWith(LOG_FILE_PREFIX) && name.endsWith(LOG_FILE_EXT)\n }?.sortedBy { it.name } ?: emptyList()\n }\n \n /**\n * Clear all log files\n */\n fun clearLogs() {\n logExecutor.execute {\n getLogFiles().forEach { it.delete() }\n }\n }\n \n /**\n * Get the current log file\n */\n fun getCurrentLogFile(): File? {\n val files = getLogFiles()\n return files.lastOrNull()\n }\n \n private fun log(level: Int, tag: String, message: String, throwable: Throwable?) {\n if (level < logLevel) return\n \n // Log to Android logcat\n when (level) {\n VERBOSE -> Log.v(tag, message, throwable)\n DEBUG -> Log.d(tag, message, throwable)\n INFO -> Log.i(tag, message, throwable)\n WARN -> Log.w(tag, message, throwable)\n ERROR -> Log.e(tag, message, throwable)\n ASSERT -> Log.wtf(tag, message, throwable)\n }\n \n // Log to file if enabled\n if (logToFile) {\n logToFile(level, tag, message, throwable)\n }\n }\n \n private fun logToFile(level: Int, tag: String, message: String, throwable: Throwable?) {\n logExecutor.execute {\n try {\n val logFile = getOrCreateLogFile()\n val timestamp = dateFormat.format(Date())\n val levelStr = when (level) {\n VERBOSE -> \"V\"\n DEBUG -> \"D\"\n INFO -> \"I\"\n WARN -> \"W\"\n ERROR -> \"E\"\n ASSERT -> \"A\"\n else -> \"?\"\n }\n \n val logMessage = StringBuilder()\n .append(\"$timestamp $levelStr/$tag: $message\")\n .apply {\n if (throwable != null) {\n append(\"\\n\")\n append(Log.getStackTraceString(throwable))\n }\n }\n .append(\"\\n\")\n \n // Write to file\n FileOutputStream(logFile, true).bufferedWriter().use { writer ->\n writer.append(logMessage.toString())\n }\n \n // Rotate logs if needed\n rotateLogsIfNeeded()\n } catch (e: Exception) {\n // If logging fails, there's not much we can do\n Log.e(TAG, \"Failed to write to log file\", e)\n }\n }\n }\n \n private fun getOrCreateLogFile(): File {\n val currentFile = getCurrentLogFile()\n \n // If no file exists or current file is too large, create a new one\n return if (currentFile == null || currentFile.length() >= MAX_LOG_FILE_SIZE) {\n val timestamp = SimpleDateFormat(\"yyyyMMdd_HHmmss\", Locale.getDefault())\n .format(Date())\n File(logDir, \"${LOG_FILE_PREFIX}${timestamp}${LOG_FILE_EXT}\")\n } else {\n currentFile\n }\n }\n \n private fun rotateLogsIfNeeded() {\n val logFiles = getLogFiles()\n \n // If we have too many log files, delete the oldest ones\n if (logFiles.size > MAX_LOG_FILES) {\n val filesToDelete = logFiles.take(logFiles.size - MAX_LOG_FILES)\n filesToDelete.forEach { it.delete() }\n }\n }\n \n /**\n * Get the logs as a string\n */\n fun getLogsAsString(): String {\n return try {\n val logFiles = getLogFiles()\n val stringBuilder = StringBuilder()\n \n logFiles.forEach { file ->\n if (file.exists()) {\n file.bufferedReader().use { reader ->\n stringBuilder.append(reader.readText())\n }\n }\n }\n \n stringBuilder.toString()\n } catch (e: Exception) {\n \"Error reading logs: ${e.message}\"\n }\n }\n \n /**\n * Gracefully shutdown the logger\n */\n fun shutdown() {\n logExecutor.shutdown()\n try {\n if (!logExecutor.awaitTermination(1, TimeUnit.SECONDS)) {\n logExecutor.shutdownNow()\n }\n } catch (e: InterruptedException) {\n logExecutor.shutdownNow()\n Thread.currentThread().interrupt()\n }\n }\n}\n", "size": 7368, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/util/AppConfig.kt": { "content": "package com.example.emotiondetector.util\n\nimport android.content.Context\nimport androidx.annotation.StringRes\nimport com.example.emotiondetector.R\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Centralized configuration for the application\n */\n@Singleton\nclass AppConfig @Inject constructor(\n private val context: Context\n) {\n // Model configuration\n object Model {\n const val DEFAULT_MODEL_FILENAME = \"emotion_model.tflite\"\n const val MODEL_INPUT_SIZE = 48 // Width/height of the input image expected by the model\n const val MODEL_OUTPUT_SIZE = 7 // Number of emotion classes\n const val MODEL_MEAN = 0f // Mean normalization value\n const val MODEL_STD = 255f // Standard deviation for normalization\n const val DEFAULT_THRESHOLD = 0.7f // Confidence threshold for predictions\n \n // Model update configuration\n const val MODEL_UPDATE_CHECK_INTERVAL_HOURS = 24L\n const val MODEL_DOWNLOAD_CONNECT_TIMEOUT_SECONDS = 60L\n const val MODEL_DOWNLOAD_READ_TIMEOUT_SECONDS = 60L\n }\n \n // Camera configuration\n object Camera {\n const val TARGET_ASPECT_RATIO = 4f / 3f // 4:3 aspect ratio for camera preview\n const val TARGET_RESOLUTION_WIDTH = 1280\n const val TARGET_RESOLUTION_HEIGHT = 720\n const val TARGET_FRAME_RATE = 30 // Target frames per second\n const val ANALYSIS_IMAGE_FORMAT = android.graphics.ImageFormat.YUV_420_888\n }\n \n // Telemetry configuration\n object Telemetry {\n const val UPLOAD_INTERVAL_HOURS = 24L\n const val MAX_BATCH_SIZE = 100\n const val MAX_STORED_EVENTS = 1000\n const val MAX_RETRY_ATTEMPTS = 3\n const val INITIAL_RETRY_DELAY_MS = 10_000L // 10 seconds\n }\n \n // API configuration\n object Api {\n const val BASE_URL = \"https://api.your-backend.com/v1/\"\n const val TIMEOUT_SECONDS = 30L\n const val MAX_RETRIES = 3\n const val RETRY_DELAY_MS = 1000L\n }\n \n // Storage configuration\n object Storage {\n const val DATABASE_NAME = \"emotion_detector.db\"\n const val MODEL_DIR = \"models\"\n const val CACHE_DIR = \"cache\"\n const val MAX_CACHE_SIZE_BYTES = 50L * 1024 * 1024 // 50MB\n }\n \n // Get string resources with formatting\n fun getString(@StringRes resId: Int, vararg formatArgs: Any): String {\n return context.getString(resId, *formatArgs)\n }\n \n // Get emotion labels\n fun getEmotionLabels(): List = listOf(\n context.getString(R.string.emotion_angry),\n context.getString(R.string.emotion_disgust),\n context.getString(R.string.emotion_fear),\n context.getString(R.string.emotion_happy),\n context.getString(R.string.emotion_neutral),\n context.getString(R.string.emotion_sad),\n context.getString(R.string.emotion_surprise)\n )\n \n // Get emotion colors (as color resource IDs)\n fun getEmotionColors(): List = listOf(\n R.color.emotion_angry,\n R.color.emotion_disgust,\n R.color.emotion_fear,\n R.color.emotion_happy,\n R.color.emotion_neutral,\n R.color.emotion_sad,\n R.color.emotion_surprise\n )\n \n // Get default model download URL (could be overridden by remote config)\n fun getDefaultModelDownloadUrl(): String {\n return \"${Api.BASE_URL}models/latest\"\n }\n \n // Check if analytics are enabled (could be controlled by user settings)\n fun isAnalyticsEnabled(): Boolean {\n // In a real app, this could check user preferences\n return true\n }\n \n // Check if crash reporting is enabled\n fun isCrashReportingEnabled(): Boolean {\n // In a real app, this could check user preferences\n return true\n }\n}\n", "size": 3797, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/util/NetworkUtils.kt": { "content": "package com.example.emotiondetector.util\n\nimport android.content.Context\nimport android.net.ConnectivityManager\nimport android.net.NetworkCapabilities\nimport android.os.Build\nimport com.example.emotiondetector.di.AppScope\nimport okhttp3.*\nimport okhttp3.MediaType.Companion.toMediaTypeOrNull\nimport okhttp3.RequestBody.Companion.asRequestBody\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport okio.IOException\nimport org.json.JSONObject\nimport java.util.concurrent.TimeUnit\nimport javax.inject.Inject\n\n/**\n * Utility class for network-related operations\n */\n@AppScope\nclass NetworkUtils @Inject constructor(\n private val context: Context,\n private val okHttpClient: OkHttpClient\n) {\n companion object {\n private const val TAG = \"NetworkUtils\"\n private const val DEFAULT_TIMEOUT_SECONDS = 30L\n private val JSON_MEDIA_TYPE = \"application/json; charset=utf-8\".toMediaTypeOrNull()\n }\n \n /**\n * Check if the device has an active internet connection\n */\n fun isNetworkAvailable(): Boolean {\n val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager\n \n return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n val network = connectivityManager.activeNetwork ?: return false\n val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false\n \n capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||\n capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||\n capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)\n } else {\n @Suppress(\"DEPRECATION\")\n val networkInfo = connectivityManager.activeNetworkInfo\n networkInfo?.isConnected == true\n }\n }\n \n /**\n * Make a GET request\n */\n suspend fun get(\n url: String,\n headers: Map = emptyMap(),\n timeoutSeconds: Long = DEFAULT_TIMEOUT_SECONDS\n ): Result = makeRequest(\n url = url,\n method = \"GET\",\n headers = headers,\n requestBody = null,\n timeoutSeconds = timeoutSeconds\n )\n \n /**\n * Make a POST request with JSON body\n */\n suspend fun postJson(\n url: String,\n jsonBody: String,\n headers: Map = emptyMap(),\n timeoutSeconds: Long = DEFAULT_TIMEOUT_SECONDS\n ): Result = makeRequest(\n url = url,\n method = \"POST\",\n headers = headers + (\"Content-Type\" to \"application/json\"),\n requestBody = jsonBody.toRequestBody(JSON_MEDIA_TYPE),\n timeoutSeconds = timeoutSeconds\n )\n \n /**\n * Make a POST request with form data\n */\n suspend fun postForm(\n url: String,\n formData: Map,\n headers: Map = emptyMap(),\n timeoutSeconds: Long = DEFAULT_TIMEOUT_SECONDS\n ): Result {\n val formBody = FormBody.Builder().apply {\n formData.forEach { (key, value) ->\n add(key, value)\n }\n }.build()\n \n return makeRequest(\n url = url,\n method = \"POST\",\n headers = headers,\n requestBody = formBody,\n timeoutSeconds = timeoutSeconds\n )\n }\n \n /**\n * Upload a file with additional form data\n */\n suspend fun uploadFile(\n url: String,\n file: java.io.File,\n fileFieldName: String = \"file\",\n fileMimeType: String = \"application/octet-stream\",\n formData: Map = emptyMap(),\n headers: Map = emptyMap(),\n timeoutSeconds: Long = 120L // Longer timeout for file uploads\n ): Result {\n val requestBody = MultipartBody.Builder()\n .setType(MultipartBody.FORM)\n .apply {\n // Add file\n addFormDataPart(\n fileFieldName,\n file.name,\n file.asRequestBody(fileMimeType.toMediaTypeOrNull())\n )\n \n // Add additional form data\n formData.forEach { (key, value) ->\n addFormDataPart(key, value)\n }\n }\n .build()\n \n return makeRequest(\n url = url,\n method = \"POST\",\n headers = headers,\n requestBody = requestBody,\n timeoutSeconds = timeoutSeconds\n )\n }\n \n /**\n * Make a generic HTTP request\n */\n private suspend fun makeRequest(\n url: String,\n method: String,\n headers: Map,\n requestBody: RequestBody?,\n timeoutSeconds: Long\n ): Result = try {\n if (!isNetworkAvailable()) {\n return Result.failure(IOException(\"No network connection available\"))\n }\n \n val request = Request.Builder()\n .url(url)\n .apply {\n when (method.uppercase()) {\n \"GET\" -> get()\n \"POST\" -> post(requestBody ?: FormBody.Builder().build())\n \"PUT\" -> put(requestBody ?: FormBody.Builder().build())\n \"DELETE\" -> delete(requestBody)\n else -> throw IllegalArgumentException(\"Unsupported HTTP method: $method\")\n }\n \n // Add headers\n headers.forEach { (key, value) ->\n header(key, value)\n }\n }\n .build()\n \n // Create a new client with the specified timeout\n val client = okHttpClient.newBuilder()\n .connectTimeout(timeoutSeconds, TimeUnit.SECONDS)\n .readTimeout(timeoutSeconds, TimeUnit.SECONDS)\n .writeTimeout(timeoutSeconds, TimeUnit.SECONDS)\n .build()\n \n // Execute the request\n client.newCall(request).execute().use { response ->\n if (!response.isSuccessful) {\n val errorBody = response.body?.string() ?: \"No error body\"\n return Result.failure(IOException(\"Unexpected response code: ${response.code}, $errorBody\"))\n }\n return Result.success(response)\n }\n } catch (e: Exception) {\n Result.failure(e)\n }\n \n /**\n * Parse a JSON response body to a JSONObject\n */\n fun parseJsonResponse(response: Response): Result = try {\n val responseBody = response.body?.string()\n ?: return Result.failure(IOException(\"Empty response body\"))\n \n Result.success(JSONObject(responseBody))\n } catch (e: Exception) {\n Result.failure(IOException(\"Failed to parse JSON response: ${e.message}\", e))\n }\n \n /**\n * Parse a JSON response body to a String\n */\n fun parseStringResponse(response: Response): Result = try {\n val responseBody = response.body?.string()\n ?: return Result.failure(IOException(\"Empty response body\"))\n \n Result.success(responseBody)\n } catch (e: Exception) {\n Result.failure(IOException(\"Failed to read response: ${e.message}\", e))\n }\n \n /**\n * Check if the response indicates success (status code 2xx)\n */\n fun isResponseSuccessful(response: Response): Boolean {\n return response.code in 200..299\n }\n \n /**\n * Add authentication headers to a request\n */\n fun addAuthHeaders(\n headers: MutableMap,\n token: String? = null,\n apiKey: String? = null\n ) {\n token?.let { headers[\"Authorization\"] = \"Bearer $it\" }\n apiKey?.let { headers[\"X-API-Key\"] = it }\n }\n}\n", "size": 7845, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/util/FileUtils.kt": { "content": "package com.example.emotiondetector.util\n\nimport android.content.Context\nimport android.net.Uri\nimport android.os.Environment\nimport android.webkit.MimeTypeMap\nimport com.example.emotiondetector.di.AppScope\nimport java.io.*\nimport java.text.SimpleDateFormat\nimport java.util.*\nimport javax.inject.Inject\n\n/**\n * Utility class for file operations\n */\n@AppScope\nclass FileUtils @Inject constructor(\n private val context: Context\n) {\n companion object {\n private const val TAG = \"FileUtils\"\n private const val BUFFER_SIZE = 8192 // 8KB buffer size\n \n // Directory names\n const val DIR_MODELS = \"models\"\n const val DIR_CACHE = \"cache\"\n const val DIR_LOGS = \"logs\"\n \n // File extensions\n const val EXT_TFLITE = \".tflite\"\n const val EXT_TXT = \".txt\"\n const val EXT_JSON = \".json\"\n \n // MIME types\n const val MIME_TFLITE = \"application/octet-stream\"\n const val MIME_JSON = \"application/json\"\n const val MIME_TEXT = \"text/plain\"\n }\n \n /**\n * Get the app's private files directory\n */\n fun getAppFilesDir(): File {\n return context.filesDir\n }\n \n /**\n * Get the app's cache directory\n */\n fun getAppCacheDir(): File {\n return context.cacheDir\n }\n \n /**\n * Get the app's external files directory\n */\n fun getExternalFilesDir(dirName: String? = null): File? {\n return context.getExternalFilesDir(dirName)\n }\n \n /**\n * Get a file in the app's private files directory\n */\n fun getFileInAppDir(fileName: String): File {\n return File(context.filesDir, fileName)\n }\n \n /**\n * Get a file in the app's cache directory\n */\n fun getFileInCacheDir(fileName: String): File {\n return File(context.cacheDir, fileName)\n }\n \n /**\n * Create a directory in the app's private files directory\n */\n fun createAppDir(dirName: String): File {\n val dir = File(context.filesDir, dirName)\n if (!dir.exists()) {\n dir.mkdirs()\n }\n return dir\n }\n \n /**\n * Create a directory in the app's cache directory\n */\n fun createCacheDir(dirName: String): File {\n val dir = File(context.cacheDir, dirName)\n if (!dir.exists()) {\n dir.mkdirs()\n }\n return dir\n }\n \n /**\n * Create a temporary file with the given prefix and extension\n */\n @Throws(IOException::class)\n fun createTempFile(prefix: String, extension: String): File {\n return File.createTempFile(\n prefix,\n if (extension.startsWith(\".\")) extension else \".$extension\",\n context.cacheDir\n )\n }\n \n /**\n * Create a timestamped file name with the given prefix and extension\n */\n fun createTimestampedFileName(prefix: String, extension: String): String {\n val timeStamp = SimpleDateFormat(\"yyyyMMdd_HHmmss\", Locale.getDefault())\n .format(Date())\n return \"${prefix}_${timeStamp}${if (extension.startsWith(\".\")) extension else \".$extension\"}\"\n }\n \n /**\n * Copy a file from source to destination\n */\n @Throws(IOException::class)\n fun copyFile(source: File, destination: File) {\n source.inputStream().use { input ->\n destination.outputStream().use { output ->\n input.copyTo(output, BUFFER_SIZE)\n }\n }\n }\n \n /**\n * Copy a file from an input stream to a destination file\n */\n @Throws(IOException::class)\n fun copyFile(inputStream: InputStream, destination: File) {\n inputStream.use { input ->\n destination.outputStream().use { output ->\n input.copyTo(output, BUFFER_SIZE)\n }\n }\n }\n \n /**\n * Read a text file as a string\n */\n @Throws(IOException::class)\n fun readTextFile(file: File): String {\n return file.readText()\n }\n \n /**\n * Write text to a file\n */\n @Throws(IOException::class)\n fun writeTextFile(file: File, text: String) {\n file.writeText(text)\n }\n \n /**\n * Delete a file or directory recursively\n */\n fun deleteRecursively(file: File): Boolean {\n if (file.isDirectory) {\n file.listFiles()?.forEach {\n deleteRecursively(it)\n }\n }\n return file.delete()\n }\n \n /**\n * Get the MIME type of a file\n */\n fun getMimeType(file: File): String {\n return getMimeType(file.absolutePath)\n }\n \n /**\n * Get the MIME type of a file by its path\n */\n fun getMimeType(path: String): String {\n val extension = path.substringAfterLast('.').lowercase(Locale.getDefault())\n return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: \"application/octet-stream\"\n }\n \n /**\n * Get the extension of a file\n */\n fun getFileExtension(file: File): String {\n return getFileExtension(file.name)\n }\n \n /**\n * Get the extension of a file by its name\n */\n fun getFileExtension(fileName: String): String {\n return fileName.substringAfterLast('.', \"\")\n }\n \n /**\n * Get the file name without extension\n */\n fun getFileNameWithoutExtension(file: File): String {\n return getFileNameWithoutExtension(file.name)\n }\n \n /**\n * Get the file name without extension from a file name\n */\n fun getFileNameWithoutExtension(fileName: String): String {\n return fileName.substringBeforeLast('.')\n }\n \n /**\n * Check if external storage is available for read and write\n */\n fun isExternalStorageWritable(): Boolean {\n return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED\n }\n \n /**\n * Check if external storage is available to at least read\n */\n fun isExternalStorageReadable(): Boolean {\n return Environment.getExternalStorageState() in \n setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY)\n }\n \n /**\n * Get the file extension from a URI\n */\n fun getFileExtension(uri: Uri): String? {\n return context.contentResolver.getType(uri)?.let { mimeType ->\n MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)\n }\n }\n \n /**\n * Get a file's size in a human-readable format\n */\n fun getReadableFileSize(size: Long): String {\n val units = arrayOf(\"B\", \"KB\", \"MB\", \"GB\", \"TB\")\n var sizeInUnits = size.toDouble()\n var unitIndex = 0\n \n while (sizeInUnits >= 1024 && unitIndex < units.size - 1) {\n sizeInUnits /= 1024\n unitIndex++\n }\n \n return \"%.2f %s\".format(sizeInUnits, units[unitIndex])\n }\n}\n", "size": 6851, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/security/KeyManager.kt": { "content": "package com.example.emotiondetector.security\n\nimport android.content.Context\nimport android.security.keystore.KeyGenParameterSpec\nimport android.security.keystore.KeyProperties\nimport android.util.Base64\nimport android.util.Log\nimport java.security.KeyStore\nimport java.security.SecureRandom\nimport javax.crypto.Cipher\nimport javax.crypto.KeyGenerator\nimport javax.crypto.SecretKey\nimport javax.crypto.spec.GCMParameterSpec\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Manages encryption keys for secure storage\n * Uses Android's KeyStore to securely store cryptographic keys\n */\n@Singleton\nclass KeyManager @Inject constructor(\n private val context: Context\n) {\n companion object {\n private const val TAG = \"KeyManager\"\n private const val KEYSTORE_PROVIDER = \"AndroidKeyStore\"\n private const val KEY_ALIAS = \"emotion_detector_key\"\n private const val KEY_SIZE = 256\n private const val IV_SIZE = 12 // 96 bits for GCM\n \n // For backward compatibility with existing encrypted data\n private const val SHARED_PREFS_NAME = \"emotion_detector_keys\"\n private const val ENCRYPTED_KEY_ALIAS = \"encrypted_key\"\n private const val ENCRYPTED_IV_ALIAS = \"encrypted_iv\"\n }\n \n private val keyStore: KeyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply {\n load(null)\n }\n \n private val sharedPrefs = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)\n \n init {\n ensureKeyExists()\n }\n \n /**\n * Get the encryption key, creating it if it doesn't exist\n */\n private fun ensureKeyExists() {\n if (!keyStore.containsAlias(KEY_ALIAS)) {\n createKey()\n }\n }\n \n /**\n * Create a new encryption key in the Android KeyStore\n */\n private fun createKey() {\n try {\n val keyGenerator = KeyGenerator.getInstance(\n KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER)\n \n val builder = KeyGenParameterSpec.Builder(KEY_ALIAS,\n KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)\n .setBlockModes(KeyProperties.BLOCK_MODE_GCM)\n .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)\n .setKeySize(KEY_SIZE)\n .setRandomizedEncryptionRequired(true)\n .setUserAuthenticationRequired(false)\n \n keyGenerator.init(builder.build())\n keyGenerator.generateKey()\n \n Log.d(TAG, \"New encryption key created\")\n } catch (e: Exception) {\n Log.e(TAG, \"Error creating encryption key\", e)\n throw RuntimeException(\"Failed to create encryption key\", e)\n }\n }\n \n /**\n * Get the encryption key from the KeyStore\n */\n private fun getKey(): SecretKey {\n ensureKeyExists()\n val key = keyStore.getKey(KEY_ALIAS, null) as? SecretKey\n return key ?: throw IllegalStateException(\"Failed to get encryption key\")\n }\n \n /**\n * Generate a secure random IV\n */\n private fun generateIv(): ByteArray {\n val iv = ByteArray(IV_SIZE)\n SecureRandom().nextBytes(iv)\n return iv\n }\n \n /**\n * Encrypt the provided data\n */\n fun encrypt(data: ByteArray): ByteArray {\n return try {\n val cipher = Cipher.getInstance(\"AES/GCM/NoPadding\")\n val iv = generateIv()\n val spec = GCMParameterSpec(128, iv)\n \n cipher.init(Cipher.ENCRYPT_MODE, getKey(), spec)\n val encrypted = cipher.doFinal(data)\n \n // Combine IV and encrypted data\n ByteArray(iv.size + encrypted.size).apply {\n System.arraycopy(iv, 0, this, 0, iv.size)\n System.arraycopy(encrypted, 0, this, iv.size, encrypted.size)\n }\n } catch (e: Exception) {\n Log.e(TAG, \"Encryption failed\", e)\n throw RuntimeException(\"Encryption failed\", e)\n }\n }\n \n /**\n * Decrypt the provided data\n */\n fun decrypt(encryptedData: ByteArray): ByteArray {\n if (encryptedData.size < IV_SIZE) {\n throw IllegalArgumentException(\"Encrypted data is too short\")\n }\n \n return try {\n val iv = encryptedData.copyOfRange(0, IV_SIZE)\n val encrypted = encryptedData.copyOfRange(IV_SIZE, encryptedData.size)\n \n val cipher = Cipher.getInstance(\"AES/GCM/NoPadding\")\n val spec = GCMParameterSpec(128, iv)\n \n cipher.init(Cipher.DECRYPT_MODE, getKey(), spec)\n cipher.doFinal(encrypted)\n } catch (e: Exception) {\n Log.e(TAG, \"Decryption failed\", e)\n throw RuntimeException(\"Decryption failed\", e)\n }\n }\n \n /**\n * Encrypt a string and return it as a Base64-encoded string\n */\n fun encryptString(data: String): String {\n val encrypted = encrypt(data.toByteArray(Charsets.UTF_8))\n return Base64.encodeToString(encrypted, Base64.NO_WRAP)\n }\n \n /**\n * Decrypt a Base64-encoded encrypted string\n */\n fun decryptString(encryptedData: String): String {\n val decoded = Base64.decode(encryptedData, Base64.NO_WRAP)\n return String(decrypt(decoded), Charsets.UTF_8)\n }\n \n /**\n * For backward compatibility: Migrate from old encryption scheme if needed\n */\n fun migrateFromLegacyIfNeeded() {\n if (sharedPrefs.contains(ENCRYPTED_KEY_ALIAS)) {\n try {\n // In a real app, you would decrypt the old key and re-encrypt the data\n // with the new key, then remove the old key\n Log.d(TAG, \"Migrating from legacy encryption\")\n \n // After migration, remove the old keys\n sharedPrefs.edit()\n .remove(ENCRYPTED_KEY_ALIAS)\n .remove(ENCRYPTED_IV_ALIAS)\n .apply()\n \n Log.d(TAG, \"Migration completed\")\n } catch (e: Exception) {\n Log.e(TAG, \"Migration failed\", e)\n // If migration fails, we'll continue with the new key\n // and the old data will be re-encrypted when accessed\n }\n }\n }\n}\n", "size": 6386, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/worker/ModelUpdateWorker.kt": { "content": "package com.example.emotiondetector.worker\n\nimport android.content.Context\nimport android.util.Log\nimport androidx.hilt.work.HiltWorker\nimport androidx.work.CoroutineWorker\nimport androidx.work.WorkerParameters\nimport com.example.emotiondetector.data.ModelManager\nimport dagger.assisted.Assisted\nimport dagger.assisted.AssistedInject\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\n/**\n * Worker that checks for model updates at regular intervals\n */\n@HiltWorker\nclass ModelUpdateWorker @AssistedInject constructor(\n @Assisted private val context: Context,\n @Assisted private val params: WorkerParameters,\n private val modelManager: ModelManager\n) : CoroutineWorker(context, params) {\n\n @AssistedInject.Factory\n interface Factory : ChildWorkerFactory {\n override fun create(appContext: Context, params: WorkerParameters): ModelUpdateWorker\n }\n\n companion object {\n private const val TAG = \"ModelUpdateWorker\"\n }\n \n override suspend fun doWork(): Result = withContext(Dispatchers.IO) {\n return@withContext try {\n Log.d(TAG, \"Checking for model updates...\")\n \n // In a real app, this would check with your backend for updates\n // For now, we'll just log that we checked\n val hasUpdate = false\n val newVersion = 0\n val downloadUrl = \"\"\n \n /* Example implementation:\n val response = apiClient.checkForModelUpdate(modelManager.modelVersion)\n val hasUpdate = response.hasUpdate\n val newVersion = response.version\n val downloadUrl = response.downloadUrl\n */\n \n if (hasUpdate) {\n Log.d(TAG, \"New model version $newVersion available\")\n \n // Schedule the download (this could be done with WorkManager's chaining)\n // For now, we'll just log it\n Log.d(TAG, \"Would download model from $downloadUrl\")\n \n // In a real app, you might want to check conditions before downloading:\n // - Is the device on WiFi?\n // - Is the device charging?\n // - Is there enough storage space?\n \n // Example of how you might schedule the download:\n /*\n val downloadRequest = OneTimeWorkRequestBuilder()\n .setInputData(workDataOf(\n DownloadModelWorker.KEY_MODEL_URL to downloadUrl,\n DownloadModelWorker.KEY_MODEL_VERSION to newVersion\n ))\n .setConstraints(\n Constraints.Builder()\n .setRequiredNetworkType(NetworkType.UNMETERED) // Only on WiFi\n .setRequiresCharging(true) // Only when charging\n .build()\n )\n .build()\n \n workManager.enqueue(downloadRequest)\n */\n } else {\n Log.d(TAG, \"No model updates available\")\n }\n \n Result.success()\n } catch (e: Exception) {\n Log.e(TAG, \"Error checking for model updates\", e)\n Result.retry() // Retry with backoff\n }\n }\n}\n", "size": 3399, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/worker/UploadTelemetryWorker.kt": { "content": "package com.example.emotiondetector.worker\n\nimport android.content.Context\nimport android.util.Log\nimport androidx.hilt.work.HiltWorker\nimport androidx.work.CoroutineWorker\nimport androidx.work.WorkerParameters\nimport androidx.work.workDataOf\nimport com.example.emotiondetector.data.TelemetryManager\nimport dagger.assisted.Assisted\nimport dagger.assisted.AssistedInject\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport okhttp3.MediaType.Companion.toMediaType\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.util.concurrent.TimeUnit\n\n/**\n * Worker that uploads telemetry data to the backend\n */\n@HiltWorker\nclass UploadTelemetryWorker @AssistedInject constructor(\n @Assisted private val context: Context,\n @Assisted private val params: WorkerParameters,\n private val telemetryManager: com.example.emotiondetector.data.TelemetryManager,\n private val okHttpClient: OkHttpClient\n) : CoroutineWorker(context, params) {\n\n @AssistedInject.Factory\n interface Factory : ChildWorkerFactory {\n override fun create(appContext: Context, params: WorkerParameters): UploadTelemetryWorker\n }\n\n companion object {\n private const val TAG = \"UploadTelemetryWorker\"\n private const val MAX_BATCH_SIZE = 100\n private const val UPLOAD_TIMEOUT_SECONDS = 30L\n private val JSON_MEDIA_TYPE = \"application/json; charset=utf-8\".toMediaType()\n \n // In a real app, these would come from a configuration or build config\n private const val BASE_URL = \"https://api.your-backend.com/telemetry\"\n private const val API_KEY = \"your_api_key_here\"\n }\n \n override suspend fun doWork(): Result = withContext(Dispatchers.IO) {\n Log.d(TAG, \"Starting telemetry upload\")\n \n try {\n // Get a batch of events to upload\n val events = telemetryManager.getEvents()\n \n if (events.isEmpty()) {\n Log.d(TAG, \"No telemetry events to upload\")\n return@withContext Result.success()\n }\n \n Log.d(TAG, \"Uploading ${events.size} telemetry events\")\n \n // Convert events to JSON\n val eventsJson = JSONArray().apply {\n events.take(MAX_BATCH_SIZE).forEach { event ->\n put(JSONObject().apply {\n put(\"id\", event.id)\n put(\"timestamp\", event.timestamp)\n put(\"event_type\", event.eventType)\n put(\"params\", JSONObject(event.params))\n })\n }\n }\n \n val requestBody = JSONObject()\n .put(\"events\", eventsJson)\n .put(\"device_id\", getDeviceId())\n .toString()\n .toRequestBody(JSON_MEDIA_TYPE)\n \n // Create the request\n val request = Request.Builder()\n .url(\"$BASE_URL/ingest\")\n .header(\"Authorization\", \"Bearer $API_KEY\")\n .header(\"Content-Type\", \"application/json\")\n .post(requestBody)\n .build()\n \n // Execute the request\n val response = okHttpClient.newBuilder()\n .connectTimeout(UPLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)\n .readTimeout(UPLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)\n .writeTimeout(UPLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)\n .build()\n .newCall(request)\n .execute()\n \n if (!response.isSuccessful) {\n throw Exception(\"Upload failed: ${response.code} - ${response.message}\")\n }\n \n // Parse the response\n val responseBody = response.body?.string()\n Log.d(TAG, \"Telemetry upload successful: $responseBody\")\n \n // Delete the uploaded events\n val uploadedIds = events.take(MAX_BATCH_SIZE).map { it.id }\n telemetryManager.deleteEvents(uploadedIds)\n \n // If there are more events to upload, chain another worker\n if (events.size > MAX_BATCH_SIZE) {\n Log.d(TAG, \"More events to upload, chaining next batch\")\n Result.retry()\n } else {\n Log.d(TAG, \"All events uploaded successfully\")\n Result.success()\n }\n \n } catch (e: Exception) {\n Log.e(TAG, \"Error uploading telemetry data\", e)\n \n // Retry with exponential backoff\n val backoffDelay = runAttemptCount * 10_000L // 10s * attempt count\n Result.retry(workDataOf(\"backoff_delay\" to backoffDelay))\n }\n }\n \n /**\n * Get a unique device ID for telemetry purposes\n * In a real app, you might want to use a more persistent ID\n */\n private fun getDeviceId(): String {\n return android.provider.Settings.Secure.getString(\n context.contentResolver,\n android.provider.Settings.Secure.ANDROID_ID\n ) ?: \"unknown_device\"\n }\n}\n", "size": 5268, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/worker/DownloadModelWorker.kt": { "content": "package com.example.emotiondetector.worker\n\nimport android.content.Context\nimport android.util.Log\nimport androidx.hilt.work.HiltWorker\nimport androidx.work.CoroutineWorker\nimport androidx.work.Data\nimport androidx.work.WorkerParameters\nimport androidx.work.workDataOf\nimport com.example.emotiondetector.data.ModelManager\nimport dagger.assisted.Assisted\nimport dagger.assisted.AssistedInject\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport okio.buffer\nimport okio.sink\nimport java.io.File\nimport java.io.IOException\n\n/**\n * Worker that handles downloading and updating ML models\n */\n@HiltWorker\nclass DownloadModelWorker @AssistedInject constructor(\n @Assisted private val context: Context,\n @Assisted private val params: WorkerParameters,\n private val modelManager: ModelManager,\n private val okHttpClient: OkHttpClient\n) : CoroutineWorker(context, params) {\n\n @AssistedInject.Factory\n interface Factory : ChildWorkerFactory {\n override fun create(appContext: Context, params: WorkerParameters): DownloadModelWorker\n }\n\n companion object {\n const val KEY_MODEL_URL = \"model_url\"\n const val KEY_MODEL_VERSION = \"model_version\"\n const val KEY_PROGRESS = \"progress\"\n \n private const val TAG = \"DownloadModelWorker\"\n private const val BUFFER_SIZE = 8 * 1024 // 8KB buffer\n private const val PROGRESS_UPDATE_INTERVAL = 100L // Update progress every 100ms\n }\n \n private var isCancelled = false\n \n override suspend fun doWork(): Result = withContext(Dispatchers.IO) {\n val modelUrl = inputData.getString(KEY_MODEL_URL) ?: return@withContext Result.failure()\n val modelVersion = inputData.getInt(KEY_MODEL_VERSION, -1)\n \n if (modelVersion == -1) {\n Log.e(TAG, \"Invalid model version\")\n return@withContext Result.failure()\n }\n \n val tempFile = File.createTempFile(\"temp_model\", \".tflite\", context.cacheDir)\n \n try {\n val request = Request.Builder()\n .url(modelUrl)\n .build()\n \n val response = okHttpClient.newCall(request).execute()\n \n if (!response.isSuccessful) {\n throw IOException(\"Unexpected response code: ${response.code}\")\n }\n \n val contentLength = response.body?.contentLength() ?: -1L\n var downloadedBytes = 0L\n var lastUpdateTime = System.currentTimeMillis()\n \n response.body?.use { body ->\n body.source().use { source ->\n tempFile.sink().buffer().use { sink ->\n while (true) {\n if (isStopped || isCancelled) {\n Log.d(TAG, \"Download cancelled\")\n return@withContext Result.failure()\n }\n \n val read = source.read(sink.buffer, BUFFER_SIZE.toLong())\n if (read == -1L) break\n \n downloadedBytes += read\n sink.emit()\n \n // Update progress at reasonable intervals\n val currentTime = System.currentTimeMillis()\n if (currentTime - lastUpdateTime > PROGRESS_UPDATE_INTERVAL) {\n val progress = if (contentLength > 0) {\n (downloadedBytes * 100 / contentLength).toFloat() / 100f\n } else 0f\n \n setProgress(workDataOf(KEY_PROGRESS to progress))\n lastUpdateTime = currentTime\n }\n }\n }\n }\n }\n \n // Verify the downloaded file\n if (tempFile.length() == 0L) {\n throw IOException(\"Downloaded file is empty\")\n }\n \n // Replace the old model with the new one\n val modelFile = modelManager.modelFile\n if (tempFile.renameTo(modelFile)) {\n modelManager.updateModelVersion(modelVersion)\n Log.d(TAG, \"Model updated to version $modelVersion\")\n Result.success()\n } else {\n throw IOException(\"Failed to save the downloaded model\")\n }\n \n } catch (e: Exception) {\n Log.e(TAG, \"Error downloading model\", e)\n Result.failure()\n } finally {\n // Clean up temp file if it still exists\n if (tempFile.exists()) {\n tempFile.delete()\n }\n }\n }\n \n override fun onStopped() {\n super.onStopped()\n isCancelled = true\n }\n}\n", "size": 5063, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/data/TelemetryManager.kt": { "content": "package com.example.emotiondetector.data\n\nimport android.content.Context\nimport android.util.Log\nimport androidx.work.*\nimport com.example.emotiondetector.worker.UploadTelemetryWorker\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\nimport net.sqlcipher.database.SQLiteDatabase\nimport net.sqlcipher.database.SupportFactory\nimport java.util.*\nimport java.util.concurrent.TimeUnit\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Manages collection and secure storage of telemetry data\n */\n@Singleton\nclass TelemetryManager @Inject constructor(\n @ApplicationContext private val context: Context,\n private val workManager: WorkManager,\n private val secureStorage: SecureStorage\n) {\n companion object {\n private const val TAG = \"TelemetryManager\"\n private const val UPLOAD_WORK_NAME = \"telemetry_upload\"\n private const val UPLOAD_INTERVAL_HOURS = 24L // Upload every 24 hours\n \n // Event types\n const val EVENT_MODEL_LOAD = \"model_load\"\n const val EVENT_INFERENCE = \"inference\"\n const val EVENT_ERROR = \"error\"\n \n // Parameter keys\n const val PARAM_MODEL_VERSION = \"model_version\"\n const val PARAM_INFERENCE_TIME = \"inference_time_ms\"\n const val PARAM_EMOTION = \"emotion\"\n const val PARAM_CONFIDENCE = \"confidence\"\n const val PARAM_ERROR_MSG = \"error_message\"\n }\n \n /**\n * Record a telemetry event\n */\n suspend fun recordEvent(\n eventType: String,\n params: Map = emptyMap()\n ) {\n try {\n val event = TelemetryEvent(\n id = UUID.randomUUID().toString(),\n timestamp = System.currentTimeMillis(),\n eventType = eventType,\n params = params\n )\n \n secureStorage.insertEvent(event)\n Log.d(TAG, \"Recorded event: $eventType\")\n \n // Schedule periodic upload if not already scheduled\n schedulePeriodicUpload()\n \n } catch (e: Exception) {\n Log.e(TAG, \"Error recording telemetry event\", e)\n }\n }\n \n /**\n * Get all events of a specific type\n */\n suspend fun getEvents(eventType: String? = null): List {\n return secureStorage.getEvents(eventType)\n }\n \n /**\n * Delete events older than the specified timestamp\n */\n suspend fun deleteOldEvents(olderThan: Long) {\n secureStorage.deleteEventsOlderThan(olderThan)\n }\n \n /**\n * Schedule periodic upload of telemetry data\n */\n private fun schedulePeriodicUpload() {\n val constraints = Constraints.Builder()\n .setRequiredNetworkType(NetworkType.CONNECTED)\n .setRequiresBatteryNotLow(true)\n .build()\n \n val uploadRequest = PeriodicWorkRequestBuilder(\n repeatInterval = UPLOAD_INTERVAL_HOURS,\n repeatIntervalTimeUnit = TimeUnit.HOURS\n )\n .setConstraints(constraints)\n .build()\n \n workManager.enqueueUniquePeriodicWork(\n UPLOAD_WORK_NAME,\n ExistingPeriodicWorkPolicy.UPDATE,\n uploadRequest\n )\n }\n \n /**\n * Manually trigger telemetry upload\n */\n fun triggerUpload() {\n val uploadRequest = OneTimeWorkRequestBuilder()\n .setConstraints(\n Constraints.Builder()\n .setRequiredNetworkType(NetworkType.CONNECTED)\n .build()\n )\n .build()\n \n workManager.enqueue(uploadRequest)\n }\n}\n\n/**\n * Data class representing a telemetry event\n */\ndata class TelemetryEvent(\n val id: String,\n val timestamp: Long,\n val eventType: String,\n val params: Map = emptyMap()\n)\n\n/**\n * Interface for secure storage of telemetry events\n */\ninterface TelemetryStorage {\n suspend fun insertEvent(event: TelemetryEvent)\n suspend fun getEvents(eventType: String? = null): List\n suspend fun deleteEvents(ids: List)\n suspend fun deleteEventsOlderThan(timestamp: Long)\n}\n\n/**\n * Implementation of TelemetryStorage using SQLCipher for encryption\n */\n@Singleton\nclass SecureStorage @Inject constructor(\n @ApplicationContext context: Context,\n encryptionKey: ByteArray\n) : TelemetryStorage {\n \n private val database: SQLiteDatabase\n \n init {\n val factory = SupportFactory(encryptionKey)\n val dbFile = context.getDatabasePath(\"telemetry.db\")\n dbFile.parentFile?.mkdirs()\n \n database = SQLiteDatabase.openOrCreateDatabase(\n dbFile,\n factory,\n null,\n null\n )\n \n createTablesIfNeeded()\n }\n \n private fun createTablesIfNeeded() {\n database.execSQL(\"\"\"\n CREATE TABLE IF NOT EXISTS events (\n id TEXT PRIMARY KEY,\n timestamp INTEGER NOT NULL,\n event_type TEXT NOT NULL,\n params TEXT NOT NULL\n )\n \"\"\".trimIndent())\n \n // Create index on timestamp for faster queries\n database.execSQL(\"\"\"\n CREATE INDEX IF NOT EXISTS idx_events_timestamp \n ON events(timestamp)\n \"\"\".trimIndent())\n \n // Create index on event_type for faster filtering\n database.execSQL(\"\"\"\n CREATE INDEX IF NOT EXISTS idx_events_type \n ON events(event_type)\n \"\"\".trimIndent())\n }\n \n override suspend fun insertEvent(event: TelemetryEvent) {\n // In a real app, you'd use Room with SQLCipher\n // This is a simplified implementation\n val values = android.content.ContentValues().apply {\n put(\"id\", event.id)\n put(\"timestamp\", event.timestamp)\n put(\"event_type\", event.eventType)\n // In a real app, you'd want to properly serialize the params map\n put(\"params\", event.params.toString())\n }\n \n database.insertWithOnConflict(\n \"events\",\n null,\n values,\n SQLiteDatabase.CONFLICT_REPLACE\n )\n }\n \n override suspend fun getEvents(eventType: String?): List {\n val events = mutableListOf()\n \n val selection = eventType?.let { \"event_type = ?\" } ?: \"1\"\n val selectionArgs = eventType?.let { arrayOf(it) } ?: emptyArray()\n \n database.query(\n \"events\",\n arrayOf(\"id\", \"timestamp\", \"event_type\", \"params\"),\n selection,\n selectionArgs,\n null, null,\n \"timestamp DESC\",\n \"1000\" // Limit to 1000 most recent events\n )?.use { cursor ->\n while (cursor.moveToNext()) {\n try {\n val id = cursor.getString(0)\n val timestamp = cursor.getLong(1)\n val type = cursor.getString(2)\n val paramsStr = cursor.getString(3)\n \n // In a real app, you'd properly deserialize the params\n val params = emptyMap()\n \n events.add(TelemetryEvent(id, timestamp, type, params))\n } catch (e: Exception) {\n Log.e(\"TelemetryStorage\", \"Error reading event\", e)\n }\n }\n }\n \n return events\n }\n \n override suspend fun deleteEvents(ids: List) {\n if (ids.isEmpty()) return\n \n val placeholders = ids.joinToString(\",\") { \"?\" }\n val whereClause = \"id IN ($placeholders)\"\n \n database.delete(\n \"events\",\n whereClause,\n ids.toTypedArray()\n )\n }\n \n override suspend fun deleteEventsOlderThan(timestamp: Long) {\n database.delete(\n \"events\",\n \"timestamp < ?\",\n arrayOf(timestamp.toString())\n )\n }\n}\n", "size": 8198, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/data/ModelManager.kt": { "content": "package com.example.emotiondetector.data\n\nimport android.content.Context\nimport android.util.Log\nimport androidx.work.*\nimport com.example.emotiondetector.worker.DownloadModelWorker\nimport com.example.emotiondetector.worker.ModelUpdateWorker\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\nimport java.io.File\nimport java.util.concurrent.TimeUnit\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Manages the lifecycle of ML models including downloading, updating, and versioning\n */\n@Singleton\nclass ModelManager @Inject constructor(\n @ApplicationContext private val context: Context,\n private val workManager: WorkManager,\n private val modelPreferences: ModelPreferences\n) {\n companion object {\n private const val TAG = \"ModelManager\"\n private const val MODEL_DIR = \"models\"\n private const val MODEL_FILENAME = \"emotion_model.tflite\"\n private const val MODEL_VERSION_KEY = \"model_version\"\n private const val MODEL_CHECK_INTERVAL_HOURS = 24L // Check for updates daily\n }\n \n private val modelDir: File by lazy {\n val dir = File(context.filesDir, MODEL_DIR)\n if (!dir.exists()) {\n dir.mkdirs()\n }\n dir\n }\n \n val modelFile: File\n get() = File(modelDir, MODEL_FILENAME)\n \n val modelVersion: Int\n get() = modelPreferences.getInt(MODEL_VERSION_KEY, 1)\n \n /**\n * Initialize the model manager and schedule periodic update checks\n */\n fun initialize() {\n // Ensure we have the default model if none exists\n if (!modelFile.exists()) {\n copyDefaultModel()\n }\n \n // Schedule periodic update checks\n scheduleModelUpdateCheck()\n }\n \n /**\n * Check if a new model version is available\n */\n suspend fun checkForUpdates(force: Boolean = false): Boolean {\n // In a real app, this would check with your backend\n // For now, we'll just return false\n return false\n }\n \n /**\n * Download and update to a new model version\n */\n fun downloadModel(version: Int, url: String) {\n val inputData = workDataOf(\n DownloadModelWorker.KEY_MODEL_URL to url,\n DownloadModelWorker.KEY_MODEL_VERSION to version\n )\n \n val request = OneTimeWorkRequestBuilder()\n .setInputData(inputData)\n .setConstraints(\n Constraints.Builder()\n .setRequiredNetworkType(NetworkType.CONNECTED)\n .setRequiresStorageNotLow(true)\n .build()\n )\n .build()\n \n workManager.enqueueUniqueWork(\n \"download_model_$version\",\n ExistingWorkPolicy.REPLACE,\n request\n )\n }\n \n /**\n * Get the download progress as a Flow\n */\n fun getDownloadProgress(version: Int): Flow {\n return workManager.getWorkInfosForUniqueWorkFlow(\"download_model_$version\")\n .map { workInfos ->\n val info = workInfos.firstOrNull()\n when (info?.state) {\n WorkInfo.State.SUCCEEDED -> 1f\n WorkInfo.State.FAILED -> -1f\n WorkInfo.State.RUNNING -> {\n info.progress.getFloat(DownloadModelWorker.KEY_PROGRESS, 0f)\n }\n else -> 0f\n }\n }\n }\n \n /**\n * Update the current model version\n */\n fun updateModelVersion(version: Int) {\n modelPreferences.putInt(MODEL_VERSION_KEY, version)\n }\n \n /**\n * Schedule periodic checks for model updates\n */\n private fun scheduleModelUpdateCheck() {\n val constraints = Constraints.Builder()\n .setRequiredNetworkType(NetworkType.CONNECTED)\n .setRequiresBatteryNotLow(true)\n .build()\n \n val updateRequest = PeriodicWorkRequestBuilder(\n repeatInterval = MODEL_CHECK_INTERVAL_HOURS,\n repeatIntervalTimeUnit = TimeUnit.HOURS\n )\n .setConstraints(constraints)\n .build()\n \n workManager.enqueueUniquePeriodicWork(\n \"model_update_check\",\n ExistingPeriodicWorkPolicy.UPDATE,\n updateRequest\n )\n }\n \n /**\n * Copy the default model from assets to the app's files directory\n */\n private fun copyDefaultModel() {\n try {\n context.assets.open(MODEL_FILENAME).use { input ->\n modelFile.outputStream().use { output ->\n input.copyTo(output)\n }\n }\n Log.d(TAG, \"Default model copied successfully\")\n } catch (e: Exception) {\n Log.e(TAG, \"Error copying default model\", e)\n }\n }\n}\n\n/**\n * Simple preferences wrapper for model-related settings\n */\nclass ModelPreferences @Inject constructor(\n @ApplicationContext context: Context\n) {\n private val prefs = context.getSharedPreferences(\"model_prefs\", Context.MODE_PRIVATE)\n \n fun getInt(key: String, default: Int): Int = prefs.getInt(key, default)\n \n fun putInt(key: String, value: Int) {\n prefs.edit().putInt(key, value).apply()\n }\n \n fun getString(key: String, default: String): String = \n prefs.getString(key, default) ?: default\n \n fun putString(key: String, value: String) {\n prefs.edit().putString(key, value).apply()\n }\n}\n", "size": 5600, "language": "kotlin" }, "app/src/main/java/com/example/emotiondetector/domain/EmotionDetector.kt": { "content": "package com.example.emotiondetector.domain\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.Matrix\nimport android.graphics.RectF\nimport androidx.camera.core.ImageProxy\nimport com.google.mediapipe.tasks.core.BaseOptions\nimport com.google.mediapipe.tasks.vision.core.RunningMode\nimport com.google.mediapipe.tasks.vision.facedetector.FaceDetector\nimport com.google.mediapipe.tasks.vision.facedetector.FaceDetectorResult\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport org.tensorflow.lite.support.image.TensorImage\nimport org.tensorflow.lite.task.core.BaseOptions as TFLiteBaseOptions\nimport org.tensorflow.lite.task.vision.classifier.Classifications\nimport org.tensorflow.lite.task.vision.classifier.ImageClassifier\nimport java.nio.ByteBuffer\nimport kotlin.math.max\nimport kotlin.math.min\n\n/**\n * Handles emotion detection using MediaPipe for face detection and TFLite for emotion classification\n */\nclass EmotionDetector(\n private val context: Context,\n private val modelPath: String = \"emotion_model.tflite\",\n private val maxResults: Int = 5,\n private val threshold: Float = 0.3f\n) {\n private var faceDetector: FaceDetector? = null\n private var emotionClassifier: ImageClassifier? = null\n private var isInitialized = false\n \n private val modelInputSize = 48 // Input size expected by the emotion model\n \n init {\n initialize()\n }\n \n private fun initialize() {\n try {\n // Initialize MediaPipe Face Detector\n val faceDetectorOptions = com.google.mediapipe.tasks.vision.facedetector.FaceDetectorOptions.builder()\n .setBaseOptions(\n BaseOptions.builder()\n .setModelAssetPath(\"face_detection_short_range.tflite\") // Bundled with MediaPipe\n .build()\n )\n .setRunningMode(RunningMode.IMAGE)\n .setMinDetectionConfidence(0.5f)\n .build()\n \n faceDetector = FaceDetector.createFromOptions(context, faceDetectorOptions)\n \n // Initialize TFLite Emotion Classifier\n val baseOptions = TFLiteBaseOptions.builder()\n .setNumThreads(4)\n .build()\n \n val options = ImageClassifier.ImageClassifierOptions.builder()\n .setBaseOptions(baseOptions)\n .setMaxResults(maxResults)\n .setScoreThreshold(threshold)\n .build()\n \n emotionClassifier = ImageClassifier.createFromFileAndOptions(\n context,\n modelPath,\n options\n )\n \n isInitialized = true\n } catch (e: Exception) {\n throw RuntimeException(\"Error initializing EmotionDetector\", e)\n }\n }\n \n suspend fun detectEmotions(imageProxy: ImageProxy): List = withContext(Dispatchers.IO) {\n if (!isInitialized) {\n return@withContext emptyList()\n }\n \n try {\n // Convert ImageProxy to Bitmap\n val bitmap = imageProxy.toBitmap()\n \n // Detect faces\n val faceDetector = faceDetector ?: return@withContext emptyList()\n val mpImage = com.google.mediapipe.tasks.vision.core.Image(\n com.google.mediapipe.tasks.vision.core.Image.IMAGE_FORMAT_RGBA,\n bitmap\n )\n \n val detectionResult = faceDetector.detect(mpImage)\n \n if (detectionResult.detections().isEmpty()) {\n return@withContext emptyList()\n }\n \n // Process each detected face\n val results = mutableListOf()\n \n for (detection in detectionResult.detections()) {\n val boundingBox = detection.boundingBox()\n \n // Extract face region\n val faceBitmap = cropBitmap(bitmap, boundingBox)\n \n // Classify emotion\n val emotion = classifyEmotion(faceBitmap)\n \n if (emotion != null) {\n results.add(\n EmotionResult(\n boundingBox = boundingBox,\n emotion = emotion.first,\n confidence = emotion.second\n )\n )\n }\n }\n \n return@withContext results\n } catch (e: Exception) {\n e.printStackTrace()\n emptyList()\n } finally {\n imageProxy.close()\n }\n }\n \n private suspend fun classifyEmotion(bitmap: Bitmap): Pair? = withContext(Dispatchers.Default) {\n try {\n val classifier = emotionClassifier ?: return@withContext null\n \n // Convert to grayscale if needed by the model\n val processedBitmap = convertToGrayscale(bitmap)\n \n // Convert to TensorImage\n val tensorImage = TensorImage.fromBitmap(processedBitmap)\n \n // Run inference\n val results = classifier.classify(tensorImage)\n \n // Get top result\n if (results.isNotEmpty() && results[0].categories.isNotEmpty()) {\n val topCategory = results[0].categories.maxByOrNull { it.score }\n topCategory?.let { it.label to it.score }\n } else {\n null\n }\n } catch (e: Exception) {\n e.printStackTrace()\n null\n }\n }\n \n private fun cropBitmap(bitmap: Bitmap, boundingBox: RectF): Bitmap {\n val x = max(0f, boundingBox.left)\n val y = max(0f, boundingBox.top)\n val width = min(bitmap.width - x, boundingBox.width())\n val height = min(bitmap.height - y, boundingBox.height())\n \n val cropped = Bitmap.createBitmap(\n bitmap,\n x.toInt(),\n y.toInt(),\n width.toInt(),\n height.toInt()\n )\n \n // Resize to model input size\n return Bitmap.createScaledBitmap(\n cropped,\n modelInputSize,\n modelInputSize,\n true\n )\n }\n \n private fun convertToGrayscale(bitmap: Bitmap): Bitmap {\n val width = bitmap.width\n val height = bitmap.height\n \n val pixels = IntArray(width * height)\n bitmap.getPixels(pixels, 0, width, 0, 0, width, height)\n \n // Convert to grayscale using the luminosity method\n for (i in pixels.indices) {\n val pixel = pixels[i]\n val r = (pixel shr 16) and 0xFF\n val g = (pixel shr 8) and 0xFF\n val b = pixel and 0xFF\n \n // Luminosity method (0.21 R + 0.72 G + 0.07 B)\n val gray = (0.21 * r + 0.72 * g + 0.07 * b).toInt()\n \n pixels[i] = 0xFF000000.toInt() or (gray shl 16) or (gray shl 8) or gray\n }\n \n return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888)\n }\n \n fun close() {\n faceDetector?.close()\n emotionClassifier = null\n isInitialized = false\n }\n}\n\ndata class EmotionResult(\n val boundingBox: RectF,\n val emotion: String,\n val confidence: Float\n)\n\n// Extension function to convert ImageProxy to Bitmap\nprivate fun ImageProxy.toBitmap(): Bitmap {\n val yBuffer = planes[0].buffer\n val uBuffer = planes[1].buffer\n val vBuffer = planes[2].buffer\n \n val ySize = yBuffer.remaining()\n val uSize = uBuffer.remaining()\n val vSize = vBuffer.remaining()\n \n val nv21 = ByteArray(ySize + uSize + vSize)\n \n // Y buffer is not always the first in the array, need to handle padding\n yBuffer.get(nv21, 0, ySize)\n vBuffer.get(nv21, ySize, vSize)\n uBuffer.get(nv21, ySize + vSize, uSize)\n \n // Convert NV21 to ARGB_8888\n val yuvImage = android.graphics.YuvImage(\n nv21, \n android.graphics.ImageFormat.NV21, \n width, \n height, \n null\n )\n \n val outputStream = java.io.ByteArrayOutputStream()\n yuvImage.compressToJpeg(\n android.graphics.Rect(0, 0, width, height), \n 100, \n outputStream\n )\n \n val jpegArray = outputStream.toByteArray()\n return android.graphics.BitmapFactory.decodeByteArray(jpegArray, 0, jpegArray.size)\n}\n", "size": 8605, "language": "kotlin" }, "gradle/wrapper/gradle-wrapper.jar": { "content": "PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0010\u0000\t\u0000META-INF/LICENSEUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000Z[s6\u0016~ϯhfg\u0019FI}RcU73>B$(aC\u0012,@Z=\u0017(Nu=֢sw\u000eJ|gr\u0007]ΩW//e6v\u0010n(}g\u0017퇡͛0ݛro^\u001bX݈wwn\u0011as[;|\\[7z\u0003>!\u0001ōu\u0007P_ymfD3iDd'\u00068l*QU6VN\u0015ªޚj,qEổv\u0011\u000bD[JlbJ\u0016\nȷf\u000fA{\u001c[\nz\u0019{Xiջ ̡SVJP\u000fG!ao\u000f\\Z1 `ӝK\u000e\u0002j'\u001bqKϔ\u0018;< i,IJ\u0002\u0000z1\u0006^\njxk0`MS\biUА\u0005\u0006]\u0005JӶ⠇=\n⽱G?@$F\u0007\u001fͼ\u0019\u001dʼn+}KA\u0002gKB\fF\u0012y)'\u0015Np_7{X!\u000e{E\u0007Ӿd9h&rA\u0013r\u001e%պ\u0006kʖ(\f\n\u001f\u0004\u001b\u0003pU.H\u0004[Ձ\u0011J\nHL.Ì3q\u0005k7;ν\u000e&\u001aQ\u0015y|x\u0001\t\u000e\u0015\u0001[\u001c\u0005<\u0019'\u0001,6[\t)\b՞FZoUך,\u0019hMh*8Xwe3) \tEg\u0006VGghCpJ\u0005\u000fG\u0018~\b_hFeq7¹;3pP~ִr/;:$\bDEM\u0019\u00024c-`bz@/䘐6Ƅ2?\u000e\"\u0001\u0000'\u0007\u000bNP\u000en*-pc2\u0019(\u001c!iL8R@w\u00181\u0001tX\u0000H\u001en\tRh\u0001XJ\u001fJ2B@70\u0003\u001c-\u0005/k2\u001c\u0006-d\u0017q\u0005\u0007POagX\b\u000ea\u000bE+\t1d\u001be#XQ\t4F\u0000q\u0006^\u0012 (\u000eQ*V\u0007F?D\u000fc\u0015nE\\8u\u00005@\n̴Q+14>O\u0002\u000b\u001b\u001b>\b<0rA%lf\u001aJ\nXw]}~\u0001I\u0017|z\u0018w$W\nZc~^Z\u0014\u000b\u001dUV5Gȃ3\u0019n\u000bтqV]\u0007k\u0000\"[˒Dh3:\u0010}\u001c)\u0017\n\u0013.Ҩ\u0007\nbL$H2l\u001bZ\u0005N\"K\u0001QMm7n\u0001;\u0015h#3Z\u0011Lj\u0013\u0015De\u001e}5yu^f^\u0016\b˰H5\u0000\u0018\u0017腭l(\u000e\u0016uD>[_`\u0016FWPhd!R\u0014+\u0003%\u0000\u0011u\u001b -+Y\n\u001bTr\b;*,!%H\u0006\u001f+\u001fȵr\u0017\u0019L 6\n8n9:cKxi'BTS0!\u001e(hF\u0007J\u0019&v\u0014(rz\u0011C(Ȱ\u0017#\u0011j\u0002{K|v':\u001e;d\u0017)On@dS\u0007e\n\t(\"$\u0007}R\u0012:\bඥ\u0001{sF›\u001f\u0003ѷs\u000b*]<~`Vb3rqz,Ge\u0005URd\u0006\u0012\b!38\u0005@\u000ez5eB\u00015A#L<5\u001e\u001ce3\u001c_V'\nє\bga`\u0005Xq|t\tq\u000bk\u0010}#!\u0013ЙK'X}[N#\u0016\u0013Y>B9'la\u00075sG+X\u0001\u0013\fZ!P$PqCt-z>k= l/\u001f\u0015\u0010ѦA\u0011P\n/\u0017\u0010؁\u001d\u0013q\u0013e\n\tf\u0004죰\u0006MӁʈ]^\u001af\u0003+ܺ\u00117;^Ք\u0005\t\u001dҡ审\n6\u0015\u0011\u000f\u0018IdtA8\u0010wڂL-\u000ePbYc/\u00000S\u0006c\u0015N\t|~\nV8\f(͈?hz6jE\u0000,\u0016O\u0004\u0015_\u00028\tvS&\u001cñ?Zb 4\u0012\u0003e\nͨϔh\u001c%/*\u000e\u0010+\u0005V\u0010|Ѻ \nĊ໹X|24[yLvB:p\t\u001e%H\u001ba\u0011@\b\u0019\nĊ\u0007\n3\u0012\u000b%\ts\u0017unD\u001eC>!R\fe-Bd6\u001fˆ*bGd&%-5P\u001d8\"?\u0013S8ި*Uc\u001bh$b\u0002p\u0017yid0\u00003\\L&VA<džy⢉RWAL\u0000N\u0006_+P?G24\t˽h•\u0011L}A\"MMV$T\"yu6K\nVMpd8K&*q4\u0019N\u00138{jvM\u0000\u0005x蠊:rzJ/I.H|x\"aV6zvt>x:aͧKki\u0016\u0005\f`ZG^2\u0003.7T_2L\u001dwXFH57B9pR|\u0011io\u0007`\u0012?\fL=2x\u0002h\u0010vi\u0002\u0006P\u0018\bCXxte\b9\u0007ٍ\u0010\u001a_1}\t\u0018ŹYd48R\u0011g#c_\u000eA\u001b4\u000eT\u001c=‚'XɥPMӨ\nX\u0019xG\t\u0007M\u0007Nj!|5P\u0002R\u001buKZ|\u0002\tv9$nR-V\u0001\"5/i\nV$wI+\u0015\u0006-j6/'νƹ\u0016@l\u0011L,7=܋Oz_n:{/\u0016??\u001b;op:I4JIS\u0006ќT\u0006:BKȞC,\u0018~y\u0000^/W/߮\u000bݯ\u001f\u0014B\n}`e|\\a\u000f\u001f\u0016kansՖo\u000b\u001bY\u0000{Tӭ\u0003pW8\n\u00175H5D\u0017B\u001079Dx\u0000\u0011;S&3{V\u0017,?9\u0014\u0017}r\u001b<_b\u0015@`\u0019a'\bv6j\t7Y\u0010@C>2ԮJu]b2ʍ/\u0015\u0013\u00057zK<\"[-\u0007\u00069)\u001f8\t.k4m'\u0002ZtW\u0002җ\u0003\\n=}\u0002bW\tH`x\u0017r^h@h8|gUmtɚcĘ;3|bpx\n\u0018\u000e؝1A73\u0014e\u0012\tFT\u0019-W#c\n\u0015\u000b\u0004[\u0000\f\u001er\u00108\u0018HO\u0007q^F\u001cQ%i\u0001\u0019\u0010s\u00060\u0017\u0012k\u0002Z! /H:KO{t=,|-ro\fOAi9l+Z\u0011\u0000ԑ+\u0015\u001f1G#ŝj;jI\u001aY0OAAW-p\u001e\u0017_造\u001c\u0013V2\u001a\tNotMv\u001b\u00129\u0016!@`%nQ\u0012IQ\u0016\u0006~&=\u0019196uMjhWx\u00050\\ږ(hŔΣO\u0001+fx{d#\u001d\u0016H6dEcF\u001b.\u001c\u001b\u0006PK\u0007\b\u001e\n\u0000\u0000'\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0014\u0000\t\u0000META-INF/MANIFEST.MFUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000-\n0\u0010E\u0004f\u0017dc=ֱ\u00044Lu{9S/\no,5ف:O%ĹQ[FĖ3\u000b\u0002SxHTճfqٸL\u00187\u001b\u0006^[x\n\b\n\u0017PK\u0007\bi\u000e{\u0000\u0000\u0000\u0000\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u00001\u0000\t\u0000org/gradle/cli/CommandLineArgumentException.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000MOJ\u00031\u0010Z/\u00179viŲT\u0011詧\u0016i\u001add\bb\u001fķ$x\u0001|(q\u0016\u0014g>?\u0000`\b\u0007\f^Y\u0017Bѥ|l(t\"HG\" +\u0011\"B\u000e\n|\u0014&`*\"t5c1\u001a\n>_\u0010\u0011V\"\u001a\u0010DC)b7\u0003\">(\nn\u0001cОgx\n2f^ʋ`,'¥StUi\u0015\u0012&\u0019\u001c?p*\u0016\n\u0006+tqdg\u0017Y\u001dh^\u001bfP\u001f0]U0JRv\b\u001d\u000e%@\u0006?\u001a\u001d}\u0003PK\u0007\b\u0002\u0004\"\u0001\u0000\u0000p\u0001\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000&\u0000\t\u0000org/gradle/cli/CommandLineOption.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000eR[O\u0013Q\u0010\u000e\u0014+P\b^qe-\bV0\u0010/$U%\u0010/큽4[1?\u000fF$hb|w;م\u0012^Μ͙_\u00000%{{+o:7vkj\u00155sܒ㙂⾰\u0005\u000f\u0004=6xP2\u001a\tZN-nq;\u0010Ei\u001c,ɈP6f\tW[-ۦ@2µ+/]\u000fH╙J\u0014\u00010Tkx(m0nܴnR_\u001cfVQ\n\u0012\f|6w-}-PA?ňa\u001a\u0003Z,VA?PK\u0007\bldMn\u0002\u0000\u0000\u0003\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u00003\u0000\t\u0000org/gradle/cli/CommandLineParser$AfterOptions.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000SN\u0013A\u0014=C҂\b- Z\u001a0\n\u0004ILH\u001a0\u0018ceawݢȃ\f\u0004%\u0007w\u0012\u00104&sgs;w~\u000b\u0002\n\f^V>\u00185n\u000bi\u001bKU7J\u0015\n\u001bH\u000flAJx<4]aM?4\u000bEh8\u001b\u001bǨ-.[\u000b\b*zh#<-t\\)rC\u0007B+s\u000f*-\u000eC`\ffTxz\fSv\u0014=Q<\u001a>v\"*\u0014jz\u001e\tш\u001a\u0019J=)m\u0019Hh\u0018dHYg\u0010\u0006z!@\u000bn\u000b0ĕna&\u001bbϭБ֡a8!$\u0018@a5ArT>\u0007x\u000fx)oFq͖;\fÁ\u0007\u0005ׅyQ咴\u0003+\fqOk/w\u0012Y8覭gzK=/]I\u001d\u0013F\u0018@Y\u001a_x^5bȈ+iBF!_;u3rRr\u001awp7ۘb\u0018\u0002`0$m_hڞ\"j41>\u001ePC+c\"֛~MW\tSSi\u0019F\u001eY?uhj\u0012\tM\u0015\u0013'\u0014a+o+\u001dPl\fK|p㛥\u0004\tٟ.\u001ccp_Z94\u000eM\u000eɚ)l\u0013\u00181挣wI\u0015\u000fPK\u0007\bk\u0007[\u0002\u0000\u0000\u0004\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000<\u0000\t\u0000org/gradle/cli/CommandLineParser$BeforeFirstSubCommand.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000UkO`\u0014~^@\n(\u0017~\u0019\u0015\u0019l+\u001397[\"qJ2L4ﺗv13L?@\u0013\u0015#\u001fe<]g\u0004A\u0017׬{{/\u0000FqwO\"ח]R'T}QMcU\nckS\u0012$w)'h=M\b}ɫY:MOjYxU3\u0002\u001bLfD\u001f=KX7[f$*\\\u001bˆ-ke.\u000b#_$\u000e\ngXV1ya\neLM#=X\u0016K3d6w=OE%Z/\\j\u0010\nw\u001b}!կ\u0018^i\u0006u&L\n/2\\\u001fKpy\u0005mho.(\nd!a\u000fCc\u0013C\u000fy3,\f<3I\u001bH/{-\u00006\f/\u001f=Р>M-\u001eJ\u000f\"\u0019Z\u001a'\n:\u000eU'\u001eCN>ڇTP.\u0013ͪ.|\fn۶)2ҵۊCy 3F7'73ɹ\u0019pۡHֺ-Б\u001a0`i~np-;\u0013تfXcV6Z &}[LG~B?\u0016`\u0002גjWTGO>;>uD[C?ը\u0017ۜ.ɽ'qI{$ư~?\u0018K$\u00016S\u0007\u000eTMai,O\u0004VL`Md\u0002k\u0013X\u0018za!\u000e+A=-G&q8J8\u0002Àw\u0004=V\u00128\u0002N`Ig9\u0010o\u00020j\b-˨3\u001e7w5f3a\\\u0013!\b}Jg~F\u0003\u0004[3?\u0013O)r\u00139_?E\u0013+ˊC4G;Q$\u001e \u0006Vj<ǽ \u000fc R{@i{\u0004X_\u0004Q}e\u0003\n4մA{y\u000bPK\u0007\b\u0006\u0000\u0000b\u000e\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000<\u0000\t\u0000org/gradle/cli/CommandLineParser$MissingOptionArgState.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000SmOP\u0014~.\u0003\n[N7v/Թ(d\u0011ow]K-1F~\u000fL\u000fG\u0019OKg,Ah{=}眞s_\u0000`NkqsOȶ\u001d۵\u001d\u001eڞ4\\-\u000bG@]\u001e\u00180\u001bh\u001d\u0004u-]Î4Zzլ!\u0018;=!Gˍ*\u001dl)oK}\u0007\u0014rh~\n!|S<\u001dP|b툊ؕuul7I9\u0003/?$7Q\u000f}k+P0P:{`\u0012$ä\u00170n\u001e=\bxHali\u0019'\u0011o3O*PP0b\nӘ,Co[~ӛoxW\u001c.V\u0018v-à\u000fQ.N4(\u0011!4P4\u001cÌ'\u0019!?\"ɊU;>KE\u000fO\u0012눐\u001f\u000fUq\u0019Ki\\\u0015\u0015q!*Ɛ'hTU\u0017&N\n\u0019(\njFX\u0019Rzu5\f{綄\u001c*5_0\u0017\u0002&hF\"\u0016\u0018Lu\u001bN\u0015\"\u00193G|\u0002Z\"H\u0004Rg\u0006X\u0018h$bm*F\u0002^$\u0014ٙ\u0017d\u0007Z,\npS\u0004K4#Xq\u0000}\bɣ\u0007\u0012i'\u0010Rzu\u0019ǐ\nPK\u0007\bĀ;M\u0002\u0000\u0000\u0004\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000=\u0000\t\u0000org/gradle/cli/CommandLineParser$OptionAwareParserState.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000TkOP\u0018~\u000e\u001bq\u00197\u0001Qnle\\\u0004E$hHp,@$uRh;r!\u000f7A\nD\u0013?v\u0003喬MN{yr\u001f\u0000aZp\nꌪIU/\u0007=hv \b\u0012\u0015d宦\n}-ٮ:-W$\u0003Cf\u001aQ}l2}S,\u0002]V8\b!M PHb\u0011\u001e\u001e\u001fNk\u0005q~\u0007c\b\u0017KR\u0017MK0L\u00152$/X\"[fjh)RKWȁ\u0003?\u0007./u{BA!YG\u001dC \u0012(\u000bdf\u0019]\u001d\u0018}'Ϟ3\u001dӛgx\u0013N\u001e>.0\u001aBE\u0013C0\u0003\u0011 \u001aA=ByAJ0\u001e?);Fje6A-qm8Q\u0007w(6h\u001e\b\u001e3\u001eRN\\OjσW~\u0005}\fMȓ<#-\u001cϥ*Ki2R\u0015f#P4~\f0CP!\u000b[Y\tݣD\u0010C<\u0018ٝJ\u0014$)jncy5ͼ]e66ֲ\fғ\u0010GT\tPHaF+\u0019(\u0018c7hqlůeY\u0006I\u0005&\u0018$Vٙ\u001dZ90WAaL?q\u0011̄1\n:\"}\f;)[Bn%4u\npj\u0010\u0010hsJH\u0000\u001bi}I^\u0004\t!V\f́\u000b&\n\u0015E\u0007:/OHJ\u001c+\t~\u0000\u001f\u0018 zCyoyf\u0003\u000e0s}{\u0015\\\\N@\f@Y!\u0015w2a'HA\"CnC1Dqi\u0006\u0003,\u0007E\u001af\b6E+ST\b;&S\u0011)#4\"e\u001b&8\n~\u0000c\u0011x(\u00152&R\u0013P2:ȳGNX4N\ncwa\u001d\f\u0016/G4:/P:\u0018ԾI-.J}6\u00073\u0001uh0\u001a0\\q화Vb\u001b5C3J\fc=ww~\u0001;vl莸}v\u0011\u001fQeѲ\u0007x\u0006\u000bb4?y\u0006?5`~j?P\u0011N\u0017<7W\t&\u0019,OH\u0013\u0011\u0002\u0006S^tw]/F\u0001_5h\u000fG\u0019%~f;g9s\u0000a0|<;o[<\u0015of[\u0006{[p%ࡠ\u000e\u000f\n#ӰfPT6\\\u001e\u0018Nب(W5\u0004\u001dn(V\u0018\u0018UfHnD0분z[RN\u0006i.(\u001f6@\u0018դѾJE$h\u0001Jy\u0005\u000b~\u0001\u0006\u0014zo$E@r|ǽ\u000fXfP~>Q\b\t&.C\u0014}+1('\u000bPK\u0007\b\\w\u0011\u000e\u0002\u0000\u0000C\u0003\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u00002\u0000\t\u0000org/gradle/cli/CommandLineParser$ParserState.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000QN\u001b1\u0010\u001d'!K\u0003))\u0005z+\u000eIe\u0005\u0015(\u0002\u0001DJ\u0011 8pz'\u001b׎H\b\u000f/8!\u0007*!P\"Œf3y\u0004\nuG\\\\m_t/Lڗ 51RܢB!=\u0005*\u001b*æO\u00034uIr;P\u0002Y\u0007NFR'\u0014\u001dͨ\u0016[k?ZAC,0\u0006\u0019X?B\u0006Mc0^+TG>:E:\u000bP|\u0002PK\u0007\b\n\u0001\u0000\u0000\u0002\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000?\u0000\t\u0000org/gradle/cli/CommandLineParser$UnknownOptionParserState.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000SN\u0013Q\u0010=\u0014\no\u0015\u0014WԶt\u0011+\u0018\u0013$1\u001a\u001b0\u0018ݽ,\u000bݢȃ\fФ`\u000f\u001f2-E\u001b$i3wfΙ3s'y2|>8xQW'\u0002[_ԭm[_s=\u001ea`-(.'x$(#\u0011^T#}q{(5yp\u0015GuaaΚK|߮{\u001e\u0005\u001dnQ\u0004\u001b\b!辐\u0011xtT6l\u0001cȬuiǮ'\u00181\u001dmO+\u0010s.#!_\u0005{A.X)DZ\"$|G\u0019nT\\R#A\u001bt!0Xx뱚R}?,~c\u0013\u001bC\u000637\u0018\n\nA\u001aY\n\u0019\"!\n\u001a.(oDC\u000fz7\tSE\u0010\u0016À(e*j\u001fy\u0017\t\u0003*y\"\u0016\u001a&1W\u001es74t\u001aFu0rܺ\u001e,%PqxI4n\u0006n1\fQFN#M\u0004תŠ[\u001a\n \"]\n#\u0001%bW|ɫ\u001c\"M9\u000bꞚnl\n\u0018\u0003-iIÅ7\u00189p\u0010!ƿ\u0001M%\\nUed_Z+h岭\\pk_[)\\oKwNOE#\n=Vl`Y\u00145p\u0011\n+\u0000ٔqhrb\u001bj\u0012J\u0010\u0012\u0000PK\u0007\b_rJ%t\u0002\u0000\u0000\u0004\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000&\u0000\t\u0000org/gradle/cli/CommandLineParser.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000U][\u0013G\u0014~\u00047ĴHTl,6\u0012\u0002!\u0005\u0014\u0011hX) AmDK!Y\nJ^^۶ZuozџQ{fW^Μy99\u001f\u001a@\u000fj\f\u001e=\u0010ڼ@\\w5R5L\u0019غ #L]AeZō\u000fr\u0015j)UՔ!}uk=\tퟭ&\u00192OuTX%\u00121\u0012Y\u0017YdOb!0\u0004\u0010.5G\u0013W\fS0SJ\u001c\"F:kW*G8w\\(\b2\u000bȘVpj}l\u000f\u0003ûФC\u0006.TTbt#:7R\u001cI|\u0014\tE\u0010B\f詓p&1{0O-\u001b;\"\u0016\u0019\u000em\u000e\u0011\u0014$ii*4N4\u0005ԁ\u001dϽix-B\"_\u0017)!2*ܣ0nf)5aib\u0007Jꛈ\u0018\f*Hc\b.ʂ^'\fMSqJQpa*-TjlۦrW֚EC\\TWTY-s\nQ:\\*t'նTn\n2pv(>;5C5\"O%Ww!ݿ9\bHbr2n\nxx~&6Y+@O~kkMW-8|.e\u0018\u0012#'\u0013o0:n1$\"܋aLHׄe\b\u0015t\u001bw\u001a\t6}e\u0005_\u0018TGԲ-͡\f\f08H\u0015R\u0018[ȉ^\u0004eC\t\u0006C\"\u0003D#)q\u0015Rj`;ӷʺ+\u001bUyґYݐ2ʪj\u0019LJF7B? bRk1)diJ_wyFviv̟\u0003dq\u0019W8X\u001cYơO8\u0003\u0013{c\u000fD\u001bp,\u0005h|\u0005\u0017HOЁU_\u0014P\u0003};^\u000f\\8\u001dJv\u001c\u0019\bXp\t\u001f?õX0ڳgH4[B)\u001a\to|l1+H \u0016,D+\u0018^o[]\u001bbqc\u0005/c% F;~\u001cS\u001cI҈wQ`%87{1a[\u001a?\u000fe\u000f\u0002\u0002PK\u0007\b#\u0004\u0000\u0000c\b\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000&\u0000\t\u0000org/gradle/cli/ParsedCommandLine.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000Uv\u0013U\u0018vvXhP@$DJ\u001c\u001a\u001az\u0000Z\u0004H\u001aN:d&L`d\u0000\u0000Vm,+/\\9@b\\?;\u0000a2\u001c\u000bkvdgVh\u000e7rqmk{nXT\u0017R\u00109rssS,ad3>_6Mr;j|^U0,\u001d*w;.\"rrf2\u0019~\u0000\u0018ˎo\u0018&g\bN!QpT\t4\u0012wT],|\u0012\fw}5aV!']\u0012+2p\"U\u0003=LTݝZZ`\u0018,9喷Yw2\u00130\u0017}\n򇞣.;r1\u00128꣔ȾE2k\fƻd\u001b\u0018dH\b2\fu)ᤌa\u0004\u0015\u0004ߏ^@Q2\tgd\u0015(\u0019\u0003\u0002\u0002nEsuiOBHE\u0011\u0006\u0006\u0005mqGlGt<A(.Jc\f\u0012&\u0018$R\u0006mQm\n\n 8ߪO6s\u0010G\u0013xG:oN\u0019%&vVƜ\\_s=6`\u0013\u0005I\\q\u0005mH\"T*\b\t:=]Ǽ\u0018/˶Ǘ-mX\fӭ\"Yι1[MlHb[)\u001bΉe\u00115r&\u000f\u0006pSp\u0013\u000b\u0005pJt)0;t\u0016n\u0014iRH:\u001b\fl,;!nv\u001b\u0014EF|`\u00052)L·FX|}P\u0002\u001aNQ%FviSx c\t۔\u0007b}#Z|$5Rh\u0017t\u0017P&r\f\u0016j6a\u001b&FBcXȲԀ>\u0019@^KfG\u0006AGqjLKǐ^\u001a\u0012\u0017HmU\u001a+]\u000f5\u0004ݫ8N=zzi&\u001af+8Qp6U\u000f\u0018⬰ϑ}žPEaɾtT\u0005&\u0019\u0012\u0019\f/qlw\u000f@x\u001dpG$@,\u000f0r\u001f;ӣ?\u000b!5:R>Zӑ*ֲ\u0015/-tQn\\-\u0004r$~-#0Q7}O~\u001cg2CB2\u000b4\u0004\u001dsl\u001f\u0012-\u0003\\%\u0013/G3L0\u001bO>D։\b\nF\u0007뿢7\n4h&XP|\u0005Rڐ=\nPK\u0007\blA<\u0004\u0000\u0000\u0007\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000,\u0000\t\u0000org/gradle/cli/ParsedCommandLineOption.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000mPJ@\u0010֪C\u000eb\fURE\u0010qk7IM\n\"A|\u000b\u000f\"(\u0000>8-x2\u0017\u0003\u000b7'+Հo*K\\gid\u00111oɐtb_@I\n\\8Փў?D\u000e\u0003=\u001e\u001f7\u0011{m}0\tחA!NNcfGd\u001db\f\"\u001ae\bmVXEڐVf022\u0014*ki\u001dE\u0017Y4js^g8Yì\u001c4\u000e;\u0007Ry4\u00059T/rmskc[\n:@i{箊\n\u0016+P\u0015Xa\u0015T(ca\u0001s\t^h0\u0005j\u0013mډI\u001a5F(q\u0007w߱w\nL%3(\u0002PK\u0007\bShRS\u0001\u0000\u0000\u0001\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u00003\u0000\t\u0000org/gradle/internal/file/PathTraversalChecker.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000uS[sF\u001466b̥\u0017nZH\"bIJ0IB=\u00140Ёײ@\u0017wwa@00m\u00180jFww\u0000,0ںUyH\u001dg\tΜ\u0013dI?d\u001dA\u0014JU-\u001a$YX9\u001f\u0012ޯE9G{ii>X8E.;c\u0002Ti+0JQ\u001a\u0012.\\׽E^u\tPie\u0003\u0019+Q,\u0018j\fPN,(BHimnas\u0019uaܴ3iiT@.0w3ppf^xv}\t\\e[HѸ[\u0015PyPnmbפs}۱(Oa\u0017\f\u000f\u0004Lz\u0019\u000f厰o>\u001b\u0010]bqӵ\u001a\u0010bڵ\fZ\u0016k(\u0010׵f\ndʵ^`\u0019Nb-lܿ^05Kn˺x\u0005֞꿅;IzhhTH_\tPFX`%\u0006PK\u0007\b50\u0001\u0003\u0000\u0000\u0004\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000A\u0000\t\u0000org/gradle/internal/file/locking/ExclusiveFileAccessManager.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000ePN@\u0010}[\u0012LBRH|{\bc(\u0002P{*JL%ukG T>?3'\u0004\u0007\u001cc\u00070y͛ys\u0000`\u0017\u001b\u0002\nD)Q\u001fq\u001d\u0000\t\u0000org/gradle/util/internal/WrapperDistributionUrlConverter.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000Q]O\u0013A\u0014=#Ų*\b\u000f֗ݮ`$\n5`LL0\u001a\u001a4}No\u0003پ\u0018!\nJ\"&(,\u00055h$=s=|+\n0|>͂1T;ia\u0004\u0018Z𾢨RER[2YF̭\u001c|ϨTΎ0G\taaYLH\u0017\fO;\u0002F{{緼c˹۫\u0013Hv_\u0019?k\u000fURKV\u000f\u001fWU1َ{\u0005]M1|l4\u000fK\u0003)qc},O47\u0018\u001e?@6ôM]\fn\u000bA}Žtix(ˁY\u0019\u001e\n\u0019`}\u0018X\u0013Ժk1n\u001e\u0018\u000f\u001aGg}\tS?\u0001PK\u0007\bUQ\u0001\u0000\u0000\u0002\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000/\u0000\t\u0000org/gradle/wrapper/BootstrapMainStarter$1.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000mQn\u0013A\u0010!5Ɛ\u0017Ip#W\u000e\"\u0012\u0001$NAHX‸89 !|\u0003\u0017. q\u0003(D\u0001\u0001\u0012iuuUu\u001f_\u00018]\u000f/\u0007\u0011\u000bv4Փꪬh*՘\u0005l\u0002pJ!S\u0017a^tB6p7:3KI_\u001f?\u0014\u001fO\n\u0010eW\u0018+\u0004]\u000fKAAoyo@)4k~f,+t*_䅧f?\u00184ɸa$\u001f'XW؞тrK_fcM\u0015j|\\z&ACaq&>VXkw^č&\u0012d@Zs\u001d\u0015֟1ۜu^+\u001c^,͝\u0001\u0000\u0000\u001e\u0002\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000A\u0000\t\u0000org/gradle/wrapper/Download$DefaultDownloadProgressListener.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000SQoV\u0014ԭqKJ\u001b(Tգ,\tMC\u000b\nV\u0012֩AE&\u001bqN\u0010Þxa\u000f㞑Rm\u00134\\\u0003\u0000͖|=;|\u00009w\u000f\u001eWmnm\t6McΛV\u001e/\u0002[P\\\nOPf%+0r{7N%WhW*y(\u0010vyiw\\_\b\u000eE\u0007BEمj\u0016\u0003\b\u0018\fbik'\u0018jtʎ'ےB\u0006۾\u0017pU\u0017^%\u0003G0la$|!5\f1d6=;\"\n\f^8B24BH6k\f#6\u0011\u0015rm[\t:җb\u0006q\u000fFP~TEwO\u0019NSPa!/l\u0018\u0018GF\t\u0003#\u0018\u001dE\u001a\u0006t\u001cP^ր1\u001daگؚ\u0011pKs\u00106C:Z\u0018B0\u0019\n\u001eu5\u001c'\u001ezn \u000fu^OkHɫ\u00146^,:TD^h\u0017S3GA(2\u001c}g\u001ai:*SgP3)·Dm?\u0004۞ \u0005upH+A-)Yco\u0010\u001f=]Kսpaz\t72NR\u0014傎\n$\b\n\u001c1t0qr}m_:{h\n\u0006]/^[ț*\u001f\u0016Fʤ0$&oB\thE@L\u001c(\u0002L\u0016[_?á;b;8D\tU+\u0018\u0017x\u001b}=)ÿ3ܣ`\n3[ߦn\\Fnw[\u0013T\t7Mr1&*],f*(eӻ8w\b_Pi\u0015r`L\u000f\b{Kh17KO\u0019B\u000b)\u0011\u0012{\u0001\u00126,\u0015*zfr9w|\u001fi\u0006PK\u0007\b4>t*\u0003\u0000\u0000\u0004\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u00004\u0000\t\u0000org/gradle/wrapper/Download$ProxyAuthenticator.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000TRP\u0014][K\bW\u0011\u001b\u001aPB[\u0001r\u0011\"^\u0001a\u001cC\u001bHz#\u001f\u0007Qf\u001ctƏr\u0019C{J~\n@?f\u0019-h\u0012vJ\u001bь\nW3lδg:v$\u0004ť\u0004w\u0005%3܍\u0018\u0019allp\u0015Z.\u001d\\{\u000f\n\u0019eQH\u001f-6m!i)-K\\\u0014G\u0007HJlko`\fʲ1-\u0010ud:VlF\u0019jL\u0019Z\\hR\u000fP*\b^E\u0010F#CGZxuw\u001c*m2t项\n\"&$^Kh\u0002zqaɆK*.t<\u0015\u00013T/.-H2>/I\u0007.\u00123]YPX\u0010\u001a:\u0019L\u00007\u0019Is\fbh=--{SA\u0017\u0015h^ʛVJQNwbg[~RO$a-z~ќo\u0015W \fA9\u0006+P_b\u0000\u0006\u0016\u0017\u001ac10\u0012ޓA\f\u001eU\u00050p,)A;j\u000f\u0018:c\u0000\u001ed='rRJP\u0012*&1`\u0004\t?Ƴ8#\u0005\u0013Qь\u0016Py>h\u0003\u0013a![\u0005+3ZѺJ8yh\u0000j;`PJzp\u0001m\u001fq=6\u0002n\u0014t\b=\u0000\u0002\"-1\u0015\u0005\n|Hr\u0007報2OԴ\u001eb\"I\u0014ӽ{\u001f>#-숽\u0002\u0000PK\u0007\b\u001b\u0002\u0000\u0000\u0011\u0005\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000!\u0000\t\u0000org/gradle/wrapper/Download.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000W\t|\u001cU\u0019d7;n!ٶ!v4B^!\u0007!.-fݙef6\u0007z ^G-UQ*\b4Dji\u0015\u000fDDE>P~of7$kOo}c?0u8p\u0003\u0015\u0003Jtk`E}ETOԄb\u0016L1Nt'brZ\u001cR`tGtҬh\u001aT\u0012&HŃI%\u0015TMFmFc0H\u0010\u001cRk\tZ\\87T-N\u0011n荡`T,1xzڈ\u001dj3ЍxCPb\t0j(\u00147\u001aZBWb\u001e\u0018J+#JCB\n\u000eQ˃\u0012\u001es;\n\b\u00179eqfjZ\\\u0013\fW\u0016bi\u0007ts؅HsBM\u000b;l\u001bӖhTR\u0002[1ܫ&\u0018X\u001bEQ]#Xy\u0002$[h#R!Gx<\u0007~r\u0015Z[\u0018k}X^,E9òB=Ae9\u0019XVH>\\\u0015^\u0004p)ÒYK\u001e\"^bYmY:\tx\u0001*\u0018=dPu1˶z\u001f.ŨB5Kr2$עNgX:ʼn<\b?j>\u0011{\u001bl%s\\Jrc\u0017\u001elޯ$܇\u00179\u0002\u001aȔ\bfc|S\n݄f\u0011+\u0018U/\u0005V/,+A¢Epj\u001f|X\"Z\u0018`\u001bêqi\u0017۱\u0007/\u0016\u000bm>\\\u000bW;åۃj65m\nQnQ)э.\n\f>C5ڧRii+2q\u000fz\u0018\u0016QLԧ\u0014^\u00110H$Qwnb֭\u001bN^cXٛ]Q!\u001f^47(Y\u00148;\f%\u0010sԱR\u0013|(m\u001bP>pɹ498ɷEyJxكT\n;\u0014j1b\\&IH0.lu*\u0003\tէy1\fk~\f㚥):ݐeB)\u0011>\u001b\u0012YnNiB\\4\u000b\u0014N2\u0019b`zo+8\u0010Z(&47S\u0007ӥ܋x\u0005a}\u0015]X&5D\u001eKY[mƭ^܂א@%\u0016kQL5:\u0016\u0018j}>Ѫk\u001a6829d({H\u0001ou):vR9{wbt\t$.\u001c-i5\u0011\u0013f/\u0014D`b\f\u00022Yf)XoۄS\u0017n]x;gS`Dmڠ;\u0002{\u0017\u000e&~cf\u00112\u001e/\u0018nݽkgRyWool_\u001d\u0000Y;XV4YL\u001e%Gg|.&#䝶C\u0018uJC\u001dH=!۞\u0004pH\u0007\u0018\u0002z!\u0005](?{8P\u0019\tuCɶ[}\u000fD\u0012>.r^cT#\u0004&(\u001e|\nkc\u001dt\u00181\u0004fxyK\u0014\u0007p܋fVH\u0014ItM\u001b$ c\u000b2z d,P_2F>TZdv-J$%Iwr:6\u0005'-_\u0007\u0013\u0014\u0006G\u0002)\f>+u窜Q{m7;\u001eH\u001cCў\u0016\u000f\u001er\u0016\u0017)\"6\u000fӵ@9ve<ŗ\u0015\u001dF\t_u.qK̇Ň'u`ņX2\u0011\u001aPXhb)x:38hK\u00118E\u0014c\\w߬\u0016w\u0005Q2䦕\u001e+>;P\u0017\u000f\u0018nuhQ-\u0005\n˔GUkhUMY-LRt%GqzJwR(7\u000b&R?CDD4M\u001f.\u001a8Ic\b<\u0011>\\$\"[Y9h͐$P\u0002\u00132;gQ1C\u0012 f\n\u0012DO5\u0012LGPeUf_)ᯔQT9\u0019U \u000bd\u001c7]$?hn\u0016o\u0014zu\u0003\u0015<<'\u000beb21ِJ\u001b|GVK\tc\u0017aPL<@v\u001aK#퓸(Sn\n5\fwa3}\u0004\u0019N!\u0012:\f6uMa3\u0019H(\u0018;k\u0019\\S h6ǖ5\\(;ckqNc{n\u0012j㚕ё\u00136\"t,-#^5pDHȠHg\u00067lM\fM\f\u0006JjW\u0006\\\u0001wd\u0012i\fGI\u001e,A\u001e)\u001fmXN#\u0001W`\ne\\f*z\u000b(Ho֠;F\u0001)TD\u001e)iM`DP&\u0013x׋\"Tjex\u001d^\be\u0007G%\u0004/f9;\"]6\u001c:\u0000Ӹ3\u00127dp)\u0002C\u0019\u001c:\n.ww\u00055A_G#Y[\u001f\u000b\u000e'&1'MF\"M.T\u0006?]\u0010&?\"˲,[l9g\u001f\u000b:g\t'h8A[\u0003S\tn!\u001b\u001e6\u0007p\"n\b>IP\n6t_N\u0005CULUD-spbؐ\naxd]dpso\u0017/sX~:\u0005\u001c%ߟ\u000b@8K%\u000b\u0012lK\u001d\f\u0004\\aO\u001djå%uRO}\\\u0004b-B\u0001PK\u0007\bAf\t\u0000\u0000*\u0012\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000-\u0000\t\u0000org/gradle/wrapper/GradleUserHomeLookup.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000R]O\u0013A\u0014=C+~XQPTdU(\t\n\u0018I\u0004C\n$>5n\n1C\u0017\u0004&\u0000h/3ssw_\u0001cIiFJk@oVO_խЏ\\O&n\u0018\u0018~h\u0013<1e_Ɔ'k\u0010~o\u0017Ӫ\u001e9/#\u001dnlY9V~{1\u0011Ɛ\u0002\n\u001b8\u000eI\\ZQf4j\u0010\u0002V*v]\u0004C嘎Gk%7~S#\f\u0007iCV`X\u000e1\u000fd%9L\nwۍ^s{Q[fgE]h%#gO\u0004-s\u00037y*\u001c\t\u001d,u=T\u000e%ɭql\tQ,`\nW\u0004)[ٛj\t9\\\u0013(:T!\b,U.:YHp\u001d7\n,\u0017\u001e\n#\u0005)n-vHOk5\u001aJ$53aC)#c='u-csPˆbzP6քdV\u0019\u001a\bPBkjZ\u001d[s\u0019`3d\u0012\u000bA-\u001azz L\"=z&ᒆu~\n1c21}s\"3-72\u0003\u0003\u0019-\u001bd!=ӸzcuiLNiƤ\u001ehw\u001e3er39SgZ\u0018\u00171\u0005\u0013\u001bjb7ږԲYT&߀n\u0018&væQ\u0013idX.0\u001blTAs\u001dVLspP\u001beKt#&wA|*-*EAɴ\u0002d#F2&\u0014S$]\u0011\u0010ZD~ΣSwde\u001a:gu\u0002m)]\u0012\nuT=\u0014Ĩ*Nն\n`~\u0018W`l8V1s1`dY7R=T\u0006$a4\n;5s\u0010F\u0001?R!2M\u0007kk\u0012VӥL3ej\u0002Ka{^\u0000\"VWZ\n\u0017Rؑr&Y\u0012xi-\u0015uy\"07N\u0013o+B*5S֝ZFO^\u0010\u0004iM[K^j^\ni\nm\u00034\fe\f\u0004г\nu>DPeʺ2ᤜ\u0011wN+V\u001b\u0011K\u001enmqt\u0015\u0017;{\u0002S\u0002=LS)-\u001d e\u0000!gd¨A>~X_K&#CiHzǐpxx\nC\u001e\\\u0001`7x(\u0007ZA#\u0005CkƐ\u001d%kf\\٧A#4 \u0019<\f6L\u0016Zf \nv\u000f\u000f\u0016D LRC\bl\n\n!(%tS\fV\u0015\t+YSO\u0015:4\"3\u0013H\"w\u0014Bf\u0006\f\u0014o,ƱuzK&\n#\u0006\u0005ղ\u0017\u0015%-\u0006re\u0010nVtL]ZN\u0010Q\u0002MRتe\u0007!L!VͶ\u000e۬Q\"]\u0010{+M8ɿ m\n\u0002X\nK-\nNH&d\u000e\"w;T\u001f,\u000fcT-jQd\u0006\u0005t+鵂ayodp\u0015eiK,Nkco\u000e1C\u0005~Q}\u001fŤhٮ3ZHnɤ$ěidTS\u0015YXz>HV\u0007CE*%2ݏl[?jZ\u001dTЇ飨\u0013ik\u0014K\u001f\u0007\u0004'_S_Lp 5\nxϘ;\n\u001e^6\u0019O*=DfB!N2^~\bXG2\u00008W?\u0018=\nBi>PgL\u0014׬\u001e\u0017)l/[p`glYzR@\u000eHH\u001cKkj\u0013_\u001c\u0005:-T\u0014Ӛ\u001e_/\"\u0015\u0015~ђ\u0017\u0007ρR.LϨ%\n\u0005\u0016ܰJgD:\u001f\u000f/:uzN\u001b—;gS\u0004I5-4ZsYnC\u000bn\u0002aq+x}\u0001k9(pDV_K?K\u000f\u000b\u0018\u0003\u0002⧥ّɈJ?\u0013\"fM4~.\u001a;Λ.Jk.K\u0014\u001f\u0004TzQ&nQCCeş\f$-N_\u000fӰ7\u0007ȼH# -\u0016?Gf^\u000e5\u000e%u\u0013_m\n$1[V!_\u000b+\u00032\u001an\u0017xC\u0010[tHǽ,!vVU\\&\u0005argeVTvG(\u0014\\cF6l/؋s^wn82CBL+ȅ\u0002{'/|,6Υ\u00002W\\-r57+|,9Q\ncc\f\u00100\u0006Pnz[YHӳ\\K\u0019\u001dѢVeƈ!-$\n\n**92'/\u0015*/cL\u0017s2z\u00163\u000fN/z9ȵ\u001e(\u0016R\f\u0005/[\u001c\u0012!ۑ\u001a2\bjL&nн\u0006\u000eKf݉Ҵ/\u0013\u0010\u001b.R:\u0010\fhI+;1-Y\n\u0010\u0006R\u0007֣AE]X\u001bz66L\u001f\u0003Zz\u0006\u0005d3Y\u0007^䁭{#q(7$3\u0006E\u001d[z#\u000fn\u001e8ӱ\u001d]\u001d^nAkǛuqn\u0010s8s=c\u00163\f<\u0014\u0000o-\"63\u0015\u0006\u001b>\neA)#\n`Z\u0016,\u001df\u0019=J\f\u0018)XF1\u000e&HlE\u0018%x}\u0005`GԠpڙW\u001e{j4e΋\u0018\u0003\u0003‽ۯ>> fޣ@A\u0019KM\u0019\u0012kE\u0003}zfVd\u001ccT>7\u001c?Z\u001df2x\u0013\u001ecY\\\u0006\u001dX2';c92#\u00199;\nNC|\nFy\u001ex=\f)fZ.8z~69r11\tڶц2\nte;Y\u0013#̯/`&\u001cN3\u0007EANYƙ.5\u0013ߤqY\u0019>1nR8&7\u0005'DkٗŹn\u0018EV\u0018Qۑ\u000e0vG\u0001s\u0015#+aZޛI0-Bf\u000e`SsL\"\u000b\u0014\bjL\u0003KlV\u001b\u0013\u0003(f4e\u0019%-VϞ\u0000?{T1\u0005zwo\u000eSX\f> :l\n[XV/\u0015J@1u\u0010*jaAsX\u000f6&\u0006:Ӧn%\b>Ł\u0014\u0016\u0003\u0002\u0007ӎ;i\u001eTc螞\u0014\u001e\u0016-\u0014rkղY'E>78\u000f[ܵc[GnǦAZT\n9\u000e\u001b\u0012C6c\nYD~4e\u0013KۖX\f*\"y% `&%˵\u0011;\u0004u5v̊\u000e?S~\u0003nj1md\u001aLV.$\n͆\u0015%++\u001cԏpWT0jC2vHCq^~?*\u00123$\fڏo\u0018:Ο+6'\u0011fq5etx\\`Ⱥk\u000bċ\u0002F& \u001e\u0010\u0006\b(qc\u001en\u001aɜۖ}T;6DV\u001am^\u0011X柸H׺A'S\u0007Խ\u000bHٖnB\u00199/UƪPՠN(\u0013\u0017\u0005\\լe\u0002\bTk@-ӒʌB63\u0011H)l>OL\u001b\u001f~*3\u0005֝B[Wdvֆ\u0018ݬv7/\u001b\u0007a6ο#Ud>$\u0000Ϩ\u000f\t3\u0006\u000fJ\u0012!\u001d<-9^QV>Y\"\u0017x%\u00193\u00141S$\u0015\u001a\no+\u0014au,yUi.$r8\u0002~^[\u001aqi-%co54O\u000f\u0016\u0014gW\u0007+8\"RP\u0013WWZ(T,(2-\u0018~$щfmʎ\u0011\u0016xY,]JKKaPȲC,VK\u0017Rd\u000e\b2v/U\u001c\\Z!\f:+>%\u001f<)(\nt\f+KaBKr r\u0017Rza!`\u00162ъh\u000bIj\u001a\u001e0܄\u001a\u0005=K7\u0007\nH\f}3(O=+;F\u0002\u000eLjRBZ[rcQ\n%K$\u0013!$eFٯK2it*%\fIEt\b*i\u0013SdL/guGn5PTs`[ˮ@\"]\\Z:\u0003+WdW6(\u0012%7r,\n4^\f9Ij:\u0015\u0002\u001a \n+yq\u0012\"oC\u0005aXP7]b9XQM~?!tr|EAm6^\u001f//]&ZC]IZq^+$V,QÊEV)!k3r\u0018 ~%\f\u0002#gNNZ)/SbU\u0000h\u0015\u0018yf\u0004S\u001cc{\u0000\\Q\u0013IkSg}(ϋM=@<_gs~K\u0014V4ڂOѐ8reeL9ub8aWe\fѰa&\u0013˥\u000fyZ0\u0019!}G}a/?e(~>m{|1Ɲ84UЂH.ݐB>>ݳγV\\\"B3\u0014\u0015u>9=c\u001c\fb*Y}nXy=(\u001d~45y֎;\u001aX4Z?ˢ\u001c\u001f\u0013|kS\u0000\u000f՟,,1G\u000e\nh\u0017\"oo\u0004IW(94ʷ\u0000>}o\u0013i\u0013\u0018[h|p~wsa;\u001fQS!~\u0018?\u001a\u0003y/\u001f\u0019MJ?YO_O[%\u0004KN?hYX\u0016SN(\bAV\u001ePg\u0004nGĊ'W\u001eA)$\bL\u0012!A\u0004\"5\u0015\u0011REb+),%T鞠yOш\u001fP]\"N.%6'\u001d7!aOǷ\u0012\u0018\u0002'BP)0U~L[엟B׻\u0015_/փ_w#9\u0014-E+>6&\br\b/D}<}=~4}./9>Ђ\u0001u+O`ܷEI\u0015B !Dm%DM4'5*ͭKѮ4q\u0007Hw\u001a!\u0017w՟RUt\u0004\u0010yiQ1Z\u0012uIKR`TZ6&ɠ\u001eK[\u00118|%=>RÊU={R˧ެ:\u001a\b`O\u0015u\fN8?K\u0011-)QPQ[\u0013h~OiEk\u0012v\u00023\u000bv\u0001v\u001aq=8\u00169&ɾ⺅\fkNQ\u0015:W\u0016r\nJ}\u0007'gsyz>x܋GܫܫZwzntoݝA<#\u001ds\u001a\u001eov\u001f[<ӂ.[.SNI<\u0006n6I䠽$\u001d=O4Myށ^^R\t>J^zЃ4y\u001cg\u0016p%UJ (UIZz\u0016:y\u0000-r w4bG\u001b]IK\u001cԑ\u000b\u001dPq\u001b]\u00068^eN\u0007-wʴ9^笤 \u0005T뼔V9RWSy\u001a1jt\u001ej\bq>H8K:_˜P\u0013F\u0015)\n\u001dt+Eͮô5LWK\\\u000fRZk}\u0017\u000eYB[Vk)@?AW\u0018١/\u001c\u0007PK\u0007\bU\u0000C%\u0015\u0000\u0000)\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\"\u0000\t\u0000org/gradle/wrapper/Install$1.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000W\n|[U\u0015ߤ{}}hvKu\u001btmn\u0011`tL*u\nk-+˺ *\"\"\b\"ې/\u0002vi!\u0003S@At\b\u000eQ@E\u0014\u0014yM]6/=܏us|\u0000\u0016\u0002.lKF3\u0016W\u0007{뫃vϊ\u0018e|Q;dx܌F¤^#\u000bd4Q1\"\t/\u001a}>et77/\n.n%=H\u0006\u0012o\u0011f,lL3n4Ɍ'H\u0017/i8a/dnT\u0010:d\f)pQnRp\u0016Y^x\u001d\u0000\tَUc`cK1\nVS8f\"+\u001d4d\u001a-B\u0007Υ;cXd<`)eJ%y\b\t)\bT\u0007\"\bR\u000eSp\u0001.c\f\u0011\u0000;Ew\u0018\n-v$BiLZ\u0013\n\u0002EfF;(Y\u000fJ\u001e#\u0007Y&P\u0017EhՓĒ\u0005c%44{\u001aRxܐ\u0015|F\u0003n#\u0014\u001aw\u001c\u0019\u0010[9|]A90\u0016\u0005WyXIa)*Xۚ\u0019';—E\\M\u000e\u001d:\u001aJ\nfGW\u0018:\u0002ǘ\u001c*\n\u0014Lj\u001d\u000ehz;&'Iۍ\u0002uz\u0005\u000fUU(\u000b\u0015U*0ŰM{XI\nnȐJ\u001doVx:!e-,\u0019\u0002\u001cq\u0007\u001f\tsIw\u001al]YTd\u0012;,(~qn|\"J=ճe\u001fF%Ӏ\b4:G?\u000fپ\u001fQ6'c\u0017[}m\\\u000f\u0007\u001d~tH玴)\u001akH\u001bbI( \"{St<\u0011\u0010yzZnjզ!tNٹX_2\u0003'G.cp\u0012\u00059(YH~q\u0019\u0002/\u001cĬSBo ~ݬz|RZnV\u0016p0\u0005g\u001bq_\u001d@+\u0005ꓤ;^u\u0004\u0007\u0010>psY/ˋlZ/\u000e'Hy.\u0006\u000eW}\u0000_\u0016H\\/\u0015\u0004\u001e5\u001b\u001c\u0010Uu\bt,Rm\u0018wv\u000e\u001cx\fb {|^e\u0018a\u0000^,/܎ʓS/W\u0005\u0014pS\u0018>ӯH^ի2GK\\΁\u00022]\b\u001e'\u000b!<3gSWSxޫ\u0001s<>H*}<\u001a+)+AjW7\u000fN\tRЬ\u0017.\f4\u0017\"궏;)@\u0019R=9 4\u000e\u000bK<܏aG)\u000eg\u0000/H\u000b}eE~\u0011Q\u0010+.\u00132v\u00146{R1$&D)eMJLۊ\u0004ǡW44.\n3^0\"8l^Z\u001b5^OW\u000bvB\t^]\u0012,_i\u001fdUc7,S@cۍ9ނl\nĢqQ_[\u0012w\u0013\"rN\u0011\u001c0$N-\u0013'ʽIy,Mo\b^\u0000>8[\u0015\u0011\u0019OH?^\u0000lb^߿hܻM\u0019Rrwݠ Re؋\u0016nHě\u001diCFmKm榡\u0017SGsg\u001eln3k\u001a?,؎R\u001cȨ8dGY766\u001a^zC\tB|tM@GJrbB?4ɿ32M'ͤK0X\u0011\u0002S7'}-?oP\u00151~_I~\u0003F%~B[*5tFʐ_,-S*Vف_@aeʁ28(al\f#:(c\u0001PK\u0007\bD;\u0002k\u0001\u0000\u0000\u0001\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 \u0000\t\u0000org/gradle/wrapper/Install.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000X\tx\u001bu~C\u0000\u0004\u000edהh8H\u001d\u0011)C\u001c\u001eM\u0011\u0014RG`ɖ\\\tev\u0017hJii\u0012rt['Z\tD\u0015uӤiӤgnڦWއo\u0000 \tiO\u001ffg̛7|;NDENsw=:Of5њζZV~ia1n9]stLkN<=O9Ӛj9GND=F>[0kmamc\u0018=\u000e\u001d\u001d\u0019t\u0013\u0001\u0012\u0011`.heuZ&w\t\u001c0\u001dW\u0015vZiXѓzS56ۂ'kO݂\u0002\u0019댙\u0004\u0010]M\nqZՓN뎳_35yʽՋ\nmSuf!ؙҧpνK.\u0018\u0011p\u0016RZ\u00075wqh\u000eۏ\bۏ\u0004i\u0005ROv\u001fzY\u0018 -o \u001f\tR\u001a )\\y֭j^IL=\u0011Q\u00167>)vLwr\bj\u000eW%\u0007w\u0007v\n\nV\u001fV8D?k8#\nP6SB.A-R\u001a>+\u0003ހaOaA\nz~LbʉRp`\nS\f9a81~lA|\u001e@dQ\u0006Zp`\u001eMM㒸nhLut]:E5\\}\u0000\u0000Aq?s\u000ek\u00055:j1}g\u0017yhJy\u0019%DA$\u000f!sXi@\u0003\u0005\u0004Ay\u0005\u0005ܧ@\u0011\u00051\u0003sG8R*%|Ș(UWW\u0014MмbiY.t\u0019q~U\"~de{sz\u001e\"\u0000?˥g:Ϻ剅!<\u001f~\nye{椟~\u0003\u001a#p\u0012\u0007e\u0010˒\u00054![gV*)\u0000\u000bզee^\tJ6}^WwdxS\u0001p-1˗@\u0015'\u0016Y\u0003\u0005*/۪4\u000f\u0014\n%4j2P/\u001fr\tpvVPg8YtRϟПk\f^u\u001cp;\t%稿\u0013\nIPd&\u0005\u0019** 踢\u0002j\u0019X\u000e\u000b\u0012\b]|l[\\,YcA6?ӿ \u0017\f\u001bt\u0002o+}thܶh\fbM9\u0007\u0018*I]\u000fT\u0005B\u001e\u0015.\u001e_*L<<< \u0003k9?\bV5\u000byU}A݅\t@B1.\u0000R_\u0004ʸ\u0001|UT\u001c\u0010~:xUE\u0003`x=l\u001bn\u000f/f~7(\"\u0014|A]2Jb%$VU\u001ec=\tdEKI\u001cZ(Y,\u0017\u0001HM#\n`\u001cvZ'+\"$6\u0002x\u001c#S\u0000xKfw\u0010)vq\u0007[\u0002&3]ȡ:v\u0018v\fV\u0002ӛ|\u001eh\u0010ТMb\u000brtw\u00185\u0006*Č\u0007͒\u0005 ED\u0011a\u0011i\u0019>u\bI*\u0017@PEG\u0003tD9g%a.\u00166؁=2Z,\u0011\u0014;KS%c^b1y{'K\u0005؍\u0002\u000f{\t\"t\u0006\u001e\u0000\u001f4þ4KE\"\u0013X`8\\yva\u0002AP)1Go\u0015\u0011NB\u001d\u0011EY\u0018\u0011Vȹ\u0005qv\u001cוF~\u001f9R8\u001d\u0016q\\Tu}\u0003\"[d\u0019Q[w\u0004ĈBЉ\u0010\u0015\u0014\u0010Gd|\u001d\"Ź}@9X\n4\u000b@]Lnj8 \u001eF^\u001dяrD;\u0001\u0016\u00145TgYk\u000eE\u00168\u001b\u0012#\u0016h\u0005s$*>+\u00032\\4L}\u001fC\u0015mCV\u001ejN᫑6% -G\u0007\fVJpI@ދv}$u|4DU\nŮ\u0012>\n6\u0016\u0007\u001ehHw\u001e-ҖP|p\u0016i[je5ڑJNӛty[\u001b\u0016TW\u000bDC}WL|5:\u0018y,\u0002ϒ?5艍4Ffk/\u0014i?E\u001f\u0014\u0010T-6Hl\"y֡لMT2Ҕ\u0005\nGwFqVa\u001fM%_g!>E^r\u0011X.;\u0011NS0\u0010\u001baTNX\u0014*m&\u001a(MQ1ϞBe^nPn\u001a7:Mok\\E\t$|!/[Lij|\"=>\u0005kӇZ.ٿ\u0016\u000fytH?~tZ|RGŷn\u0012Xcﲐw\"}(Ї!_>\u0017RCE\u000bP)*%8٤:z\"#/6R\u001f\u001fidRݐZ܀\u0005Zh\u001d6 =sێߝZ\u0001\t\u001f\u0001\u0015|7p]Fg/fV\u0007\nO}k\u0016O]5\"StdZݨ%\n0υ\u0005\u0016A!\u001abT\u0018}\u0004Ygq\u0019}n7vzN\t˲-9z-_>:}C(\u0000-#\u001a/[DsxJY&-#T'\u0011id:|\u0017PK\u0007\b1\"K\u000e\u0000\u0000x\u001c\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u001f\u0000\t\u0000org/gradle/wrapper/Logger.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000N\u0013A\u0014߱B)R@~e-FPcbHLH\u001a5`t{.G\u000f1r!\\\u001a\u001f^\u0017e\u0002:\u001fפQj\u0010\u00187b3,q\u0003?1YPO\u000eyL4&[c\tn\tp',p+\u0002܏\u0017,R\u001e!n@M(la\u0004`\u0014,\n+`2ƗQ\u0007PK\u0007\b\t\u0005<\u0002\u0000\u0000\u001e\u0004\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000&\u0000\t\u0000org/gradle/wrapper/PathAssembler.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000UJ@\u0010gMZE@S+MC+PE\u0010(Lm7փ\u0007-<\t\u001e|\u0000\u001fJ\u001e~,\u0001\u0000ccGO<\u0016r&S.|eJJ\u0015&ȋ\u0004[(\u001ce&\\ 3K\u001d΅v8e\u001a\fT#LFr|JY\u001bWZ\u0013p\bFdѤ ZeRk\"\u001e\nOQ?\u0018t\u001ex428*l\u001aV$\u001aG+\u0012mx'9c\n\n\u0006\u000b\u0016\u0016&\no\u0005ʪ\n-\u0006seTuw\u0013PEXo=3\u0006^?\u000f\u000eaA㊾\u0000#ha.\u0003Ry\u0006@\n\u0006PK\u0007\b0[$\u0001\u0000\u0000j\u0001\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000.\u0000\t\u0000org/gradle/wrapper/PropertiesFileHandler.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000T[W\u001bU\u0014NI3q\u0012ZJ*6\n!$D\bmR*\u0000\u0015zI20\u0019L,}G\u000f>e\u001f|!\u0013.\t\u0001fٗo_η\u001d(<'\u000f\u001e,d~幱&BlD{\u0018cP6\tM\u0014!qL(P\u0007Ruh!\u001dpj8\u001bطk&e/\u0013OG1\u0015n3\u001dSfxë潽\u001439n+,ڢRČ\b0\u0014w\\R70\u0010?Z\u001dX%r$\u00120dqPw\u0015M,1js;S\u0011a67\u001e_wJ\u0006H%)j-?#\u001c>>E(I诶\u0006%V:-\u0014kXamh\u000b+J\u0013\\\"ZGяz?\u0006f$=6v#&\u0012\u0004\u0001PK\u0007\b]&o\u0003\u0000\u0000\u0006\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000-\u0000\t\u0000org/gradle/wrapper/WrapperConfiguration.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000}mO\u0013A\u0010gR\u0016\u0010*r}R[V@'\u0004Ŵm\u001e\\+$\u001a ~\u0006_hbc\u000b?\u001f8ۻ\u001e7;\u0000M\u0002./\u000fbΘZ\u0016RE\u0013KZ.+Ԕ55Y\fu0j0\fV,UYh\fqB\u0015͉u)Ys\u0014t)Z=𕆢Q4\u000e**cJ8{t\u0003|.YfG\u001f\u0010\u0002K칬0\u0002QMRN\nK]^gze75\"K\ng/x\b\u0004O9M)TR{SV2O@(ˆ\u0006\u0011\bT*3S\u0015\u001bx`\u0002\n&q\n5\u0004Fۧ\nUY\u0016\u001a5\u0002r=K\u001b\u0015/4\\cZ$@v\bSE.Sm%\u0015\u001e\u0011_U\\#\u001b\u001f\b0\u0002~Mʋ~a;rն\u000f\u0005Í\u0001\t\u0002N\f\u001f\u0016@w\u0005\bX=\u0001,O\u0014 \b!\u0017 \f{@``֕҅bWk7\u0001O\u0010\u0018\u0002n\u001c6ģnm;U\u001cxo[\u0004\u0018λ\"\u001a\u0011^?䏜\u0015# \u001dŞaCFw\u0013ᇚ@#\u0017`\u0013;i< \u000b\u0017[\u001ee\u0005\u000e6`!b[\u0016[6\u001e4nh\u0001\u0012w\u0018\u000bjB$|\tܛt(\u001alŒ\tѯ!AN\u0010\u001e\u0003'f0 iCh\u0002}o\u0010b!G\u001c+>iw|3/\u000euŧ\u001c;>9W|\u001f\u000e\f+.g;\nO\bv\u000b\u001eugۍ{\u001e\u0003PK\u0007\bRʸH\u0002\u0000\u0000P\u0006\u0000\u0000PK\u0003\u0004\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000(\u0000\t\u0000org/gradle/wrapper/WrapperExecutor.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000Vw\u0013E\u0014>\u0012Bx4-o\u0018iZ\nV)`d.M[@~DA\u0014\u0015\u0014\u0015D~\u001c~xg7i6pNN3wo\u0005`\u0003n3;yx`Hp5\u0011h\u000bēP 3\"ZӼ\u0015.\u0019\u0016%#\u001c\u001f\u0011#6\u0002mII1x(IR&,\u000b\u001bC--\u001b6ޚ'B\u0013ư\u0014^OCds]VS4;u||kp\u0006N\u0018I(\u000f3E#q\u0013CkQ6Gjr]|gy(+\u0012'D\u0004%Rq!LE;iJ\f\u00168\\L*7#{;)\byЌ\f\u000b\n^bT[m#\u001e\u0016UD\t&\f\bCmԊXb\u000em\f\u0004M\u000eG!\u001at)[M0O[֘aq\u0019\"DZ]>->\u001cvc\u0004Ă\u0017\u0004Yt>E~F\nZٴ\u001b]\f˦ZDZ˰(\u0019=~rej\tu/cr&Jͅ;O\"\u001c3NbNL4m\u0014M?\u000f\u0016\u001d\u000eO\u001dN$\u0007?4w\u0013\u000eJTC=\u001c\u00019͵i\u001fN/R\u0018\u0016:2CeC؊\u0011\u0019\u001ac&H(QiUh\u0002aXAIL>x_\u0007C\u0006EC؊0>J8cVM^qTv\u0004YդhvyiV3:\u0017ƟnB+'u-_c7q;EuE_IMOKfqkPW.,V>\u001e򑕪\tCU\\ĖL\\6,5DiUamVQ|}!\\~ɰGJJds?U\u001b\u0019\u001e2Oel|\u0016Ll\u0017~KWZe{MfY6$\u0003\\X$%\u001f\u0019\u0019I7(,\n\u0015O\n9Z~qUM{I\bF*\u001e7K!ff\t\u000e\u0004&=\u0007VT.zIdC\\\u001f\u0010\u0011錺W\u001aqWmIO\u0002.\u0010XH\u0000\nTM`(\u0018\u0016b\t,7\u0006VZx,.\u001fBxBC\u0010}nka\u000bYѓn_7B+,D-=M\u0003Cgi\u0014ύ\u0016XH[x΂iaKX}\u000b'bT4E}/'Zh\u0002\\\u0002he\u000eڲ\u000f\u0007my\b#Tp̖qʖ_H\u0010U<)\b\u0011%\u0015$™XwS(8wC\u0016>p\u001aswHo\u001ei\u00026!Q9$[ױwWw\nγJ\u0016V\u0012Ǻ*|磕&a\u0002\u0017\u0010O\u001a^lKsĺmr\nQM);]\u00039f\u0016+}a\u0001įWJ<ͳ)[\u0006{؛ӱU{rؽ4\"MD$*zy]T>*6nMFh:\u0019\"\u0014\u000eS\u0000PK\u0007\b\u0000#U\u0006\u0000\u0000\f\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u001e\n\u0000\u0000'\u0000\u0000\u0010\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000META-INF/LICENSEUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000i\u000e{\u0000\u0000\u0000\u0000\u0000\u0000\u0014\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u00000\u000e\u0000\u0000META-INF/MANIFEST.MFUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0002\u0004\"\u0001\u0000\u0000p\u0001\u0000\u00001\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u000e\u0000\u0000org/gradle/cli/CommandLineArgumentException.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000ldMn\u0002\u0000\u0000\u0003\u0000\u0000&\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0010\u0000\u0000org/gradle/cli/CommandLineOption.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000k\u0007[\u0002\u0000\u0000\u0004\u0000\u00003\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000K\u0013\u0000\u0000org/gradle/cli/CommandLineParser$AfterOptions.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0005H\u000e.\u0003\u0000\u0000]\u0007\u0000\u0000<\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0010\u0016\u0000\u0000org/gradle/cli/CommandLineParser$BeforeFirstSubCommand.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0006\u0000\u0000b\u000e\u0000\u0000=\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0019\u0000\u0000org/gradle/cli/CommandLineParser$KnownOptionParserState.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000Ā;M\u0002\u0000\u0000\u0004\u0000\u0000<\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001!\u0000\u0000org/gradle/cli/CommandLineParser$MissingOptionArgState.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0004\u0019#\u0002\u0000\u0000J\u0005\u0000\u0000=\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000#\u0000\u0000org/gradle/cli/CommandLineParser$OptionAwareParserState.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\"83|\u0001\u0000\u0000}\u0002\u0000\u00008\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\n'\u0000\u0000org/gradle/cli/CommandLineParser$OptionParserState.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\\w\u0011\u000e\u0002\u0000\u0000C\u0003\u0000\u00003\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u001e)\u0000\u0000org/gradle/cli/CommandLineParser$OptionString.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\n\u0001\u0000\u0000\u0002\u0000\u00002\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000+\u0000\u0000org/gradle/cli/CommandLineParser$ParserState.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000_rJ%t\u0002\u0000\u0000\u0004\u0000\u0000?\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000-\u0000\u0000org/gradle/cli/CommandLineParser$UnknownOptionParserState.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000#\u0004\u0000\u0000c\b\u0000\u0000&\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u00000\u0000\u0000org/gradle/cli/CommandLineParser.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000lA<\u0004\u0000\u0000\u0007\u0000\u0000&\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u00005\u0000\u0000org/gradle/cli/ParsedCommandLine.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000ShRS\u0001\u0000\u0000\u0001\u0000\u0000,\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000=:\u0000\u0000org/gradle/cli/ParsedCommandLineOption.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u000050\u0001\u0003\u0000\u0000\u0004\u0000\u00003\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000;\u0000\u0000org/gradle/internal/file/PathTraversalChecker.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0001\u0000\u0000\u0003\u0002\u0000\u0000A\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000^?\u0000\u0000org/gradle/internal/file/locking/ExclusiveFileAccessManager.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000UQ\u0001\u0000\u0000\u0002\u0000\u0000>\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000]A\u0000\u0000org/gradle/util/internal/WrapperDistributionUrlConverter.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000>\u0001\u0000\u0000\u001e\u0002\u0000\u0000/\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000C\u0000\u0000org/gradle/wrapper/BootstrapMainStarter$1.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u00004>t*\u0003\u0000\u0000\u0004\u0000\u0000A\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000E\u0000\u0000org/gradle/wrapper/Download$DefaultDownloadProgressListener.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u001b\u0002\u0000\u0000\u0011\u0005\u0000\u00004\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000*I\u0000\u0000org/gradle/wrapper/Download$ProxyAuthenticator.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000Af\t\u0000\u0000*\u0012\u0000\u0000!\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000{L\u0000\u0000org/gradle/wrapper/Download.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000P\u0001\u0000\u0000\u0002\u0000\u0000-\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u00009V\u0000\u0000org/gradle/wrapper/GradleUserHomeLookup.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000U\u0000C%\u0015\u0000\u0000)\u0000\u0000*\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000vX\u0000\u0000org/gradle/wrapper/GradleWrapperMain.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000N!\u000b\u0000\u0000\u0015\u0000\u0000\"\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000m\u0000\u0000org/gradle/wrapper/Install$1.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000D;\u0002k\u0001\u0000\u0000\u0001\u0000\u0000-\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000:z\u0000\u0000org/gradle/wrapper/Install$InstallCheck.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u00001\"K\u000e\u0000\u0000x\u001c\u0000\u0000 \u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t|\u0000\u0000org/gradle/wrapper/Install.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\t\u0005<\u0002\u0000\u0000\u001e\u0004\u0000\u0000\u001f\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000Y\u0000\u0000org/gradle/wrapper/Logger.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u00000[$\u0001\u0000\u0000j\u0001\u0000\u0000&\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000org/gradle/wrapper/PathAssembler.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000]&o\u0003\u0000\u0000\u0006\u0000\u0000.\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000l\u0000\u0000org/gradle/wrapper/PropertiesFileHandler.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000RʸH\u0002\u0000\u0000P\u0006\u0000\u0000-\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000Ǔ\u0000\u0000org/gradle/wrapper/WrapperConfiguration.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\b\b\u0000\u0000\u0000!\u0000\u0000#U\u0006\u0000\u0000\f\u0000\u0000(\u0000\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u001a\u0000\u0000org/gradle/wrapper/WrapperExecutor.classUT\u0005\u0000\u0001\u0000\u0000\u0000\u0000PK\u0005\u0006\u0000\u0000\u0000\u0000!\u0000!\u0000\u0010\n\u0000\u0000Ν\u0000\u0000\u0000\u0000", "size": 26315, "language": "unknown" }, "gradle/wrapper/gradle-wrapper.properties": { "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.14.2-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n", "size": 253, "language": "unknown" } }, "_cache_metadata": { "url": "https://github.com/ronelsolomon/java-project.git", "content_type": "github", "cached_at": "2026-03-02T22:50:30.439203", "cache_key": "c4d9948c3b5c70794c0e6d5dbdc042da" } }