commit c7b3f50a63a665ae7d6ded18e1d3680da3e7f0e4 Author: Paul Aumann Date: Wed Jun 28 10:06:27 2023 +0200 Initial commit for assignment 09 diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..c5e0eca --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof diff --git a/app/.gitignore b/app/.gitignore new file mode 100755 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100755 index 0000000..ae73db0 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,70 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'de.luh.hci.mi.experiencesampling' + compileSdk 33 + + defaultConfig { + applicationId "de.luh.hci.mi.experiencesampling" + minSdk 27 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary true + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion '1.3.2' + } + packagingOptions { + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.8.0' + implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' + implementation 'androidx.activity:activity-compose:1.5.1' + implementation platform('androidx.compose:compose-bom:2022.10.00') + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.ui:ui-graphics' + implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.compose.material3:material3' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + debugImplementation 'androidx.compose.ui:ui-tooling' + debugImplementation 'androidx.compose.ui:ui-test-manifest' + + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1" + implementation "androidx.navigation:navigation-compose:2.7.0-alpha01" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100755 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/de/luh/hci/mi/experiencesampling/ExampleInstrumentedTest.kt b/app/src/androidTest/java/de/luh/hci/mi/experiencesampling/ExampleInstrumentedTest.kt new file mode 100755 index 0000000..27a70f1 --- /dev/null +++ b/app/src/androidTest/java/de/luh/hci/mi/experiencesampling/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package de.luh.hci.mi.experiencesampling + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("de.luh.hci.mi.experiencesampling", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100755 index 0000000..06c3794 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/ExperienceSampling.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/ExperienceSampling.kt new file mode 100755 index 0000000..d9c7dc4 --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/ExperienceSampling.kt @@ -0,0 +1,71 @@ +package de.luh.hci.mi.experiencesampling + +import android.app.AlarmManager +import android.app.Application +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.compose.runtime.mutableStateOf +import de.luh.hci.mi.experiencesampling.data.CouchDB +import de.luh.hci.mi.experiencesampling.data.Repository +import de.luh.hci.mi.experiencesampling.data.RepositoryImpl +import de.luh.hci.mi.experiencesampling.notifications.NotificationTrigger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob + +// Time between notifications in milliseconds. +const val samplingInterval = 10000L // milliseconds + +// Unique identifier for experimenter. Used to distinguish between entries in database. +const val experimenter = "2386452183276023461634920" + +// The application instance exists as long as the app executes and is therefore used for app-wide +// data that is used in activities. +class ExperienceSampling : Application() { + // officially: use Dagger-Hilt dependency injection, rather than manual dependency injection + // https://developer.android.com/training/dependency-injection + // https://developer.android.com/training/dependency-injection/manual + + private lateinit var remoteDb: CouchDB + lateinit var repository: Repository + private set + + // Coroutine scope for submitting data to server. Continues even when navigating away from UI. + val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + override fun onCreate() { + super.onCreate() + remoteDb = CouchDB("https://couchdb.hci.uni-hannover.de/mobint/") + repository = RepositoryImpl(applicationContext, remoteDb) + } + + // Whether or not periodic experience sampling is on. + val periodicSampling = mutableStateOf(false) + + // Schedules the next notification delay ms from now. + fun scheduleNextNotification(delay: Long) { + val am = getSystemService(Context.ALARM_SERVICE) as AlarmManager + val i = Intent(this, NotificationTrigger::class.java) + val pi = PendingIntent.getBroadcast(this, 0, i, PendingIntent.FLAG_MUTABLE) + am.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + delay, + pi + ) + } + + // Cancels any registered notification. + fun cancelNotification() { + val am = getSystemService(Context.ALARM_SERVICE) as AlarmManager + val i = Intent(this, NotificationTrigger::class.java) + val pi = PendingIntent.getBroadcast(this, 0, i, PendingIntent.FLAG_MUTABLE) + am.cancel(pi) + } + + // Logs a debug message. + private fun log(msg: String) { + Log.d(this.javaClass.simpleName, msg) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/MainActivity.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/MainActivity.kt new file mode 100755 index 0000000..4451678 --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/MainActivity.kt @@ -0,0 +1,75 @@ +package de.luh.hci.mi.experiencesampling + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import de.luh.hci.mi.experiencesampling.ui.sampling.SamplingScreen +import de.luh.hci.mi.experiencesampling.ui.sampling.SamplingViewModel +import de.luh.hci.mi.experiencesampling.ui.start.StartScreen +import de.luh.hci.mi.experiencesampling.ui.start.StartViewModel +import de.luh.hci.mi.experiencesampling.ui.statistics.StatisticsScreen +import de.luh.hci.mi.experiencesampling.ui.statistics.StatisticsViewModel +import de.luh.hci.mi.experiencesampling.ui.theme.ExperienceSamplingTheme + + +// The main activity sets the content root, which is a navigation graph. Each "composable" in the +// navigation graph is a separate page/screen of the app. +// https://developer.android.com/jetpack/compose/navigation +// https://developer.android.com/guide/navigation/principles +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + log("onCreate") + setContent { + ExperienceSamplingTheme { + // The NavController manages navigation in the navigation graph (e.g, back and up). + val navController = rememberNavController() + // The NavHost composable is a container for navigation destinations. + NavHost( + navController = navController, + startDestination = Routes.START + ) { + // The NavGraph has a composable for each navigation destination. + // Each destination is associated with a route (a string). + // ViewModels can be scoped to navigation graphs. + + // navigation destination + composable(route = Routes.START) { + StartScreen( + onNavigate = navController::navigate, // function used to navigate to another destination + viewModel(factory = StartViewModel.Factory) // create view model (or get from cache) + ) + } + + // navigation destination + composable(route = Routes.STATISTICS) { + StatisticsScreen( + navigateBack = navController::popBackStack, // executed when save button is pressed + viewModel(factory = StatisticsViewModel.Factory) // create view model (or get from cache) + ) + } + + // navigation destination + composable(route = Routes.SAMPLING) { + SamplingScreen( + navigateBack = navController::popBackStack, // executed when save button is pressed + viewModel(factory = SamplingViewModel.Factory) // create view model (or get from cache) + ) + } + + } + } + } + } + + // Logs a debug message. + private fun log(msg: String) { + Log.d(this.javaClass.simpleName, msg) + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/Routes.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/Routes.kt new file mode 100755 index 0000000..e1c3178 --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/Routes.kt @@ -0,0 +1,8 @@ +package de.luh.hci.mi.experiencesampling + +// Each navigation destination (screen) has a "route" associated to it. +object Routes { + const val START = "start" + const val STATISTICS = "statistics" + const val SAMPLING = "sampling" +} \ No newline at end of file diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/data/CouchDB.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/data/CouchDB.kt new file mode 100755 index 0000000..5f901ea --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/data/CouchDB.kt @@ -0,0 +1,68 @@ +package de.luh.hci.mi.experiencesampling.data + +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import java.io.BufferedReader +import java.io.BufferedWriter +import java.io.InputStreamReader +import java.io.OutputStreamWriter +import java.net.HttpURLConnection +import java.net.URL + +/** + * Stores and retrieves JSON documents on/from the DB server. + * http://couchdb.apache.org + * https://www.json.org/json-en.html + * http://developer.android.com/reference/org/json/JSONObject.html + */ +class CouchDB(private val dbUrl: String) { + + // Gets a JSON object from the server given a resource path. + // Example path: _design/esm/_view/ctxt?key=%22Programmieren+1%22 + // Throws an exception on failure. + suspend fun get(resourcePath: String): JSONObject = + withContext(Dispatchers.IO) { + Log.d(javaClass.simpleName, "get: " + Thread.currentThread().name) + // simulate delay + // delay(3000L) + val url = URL(dbUrl + resourcePath) + val con = url.openConnection() as HttpURLConnection + try { + val reader = BufferedReader(InputStreamReader(con.inputStream)) + val obj = JSONObject(reader.readText()) + reader.close() + return@withContext obj + } finally { + con.disconnect() + } + } + + // Stores the given JSON object at the indicated location on the server. + // Returns the server's response. Throws an exception on failure. + suspend fun put(resourcePath: String, doc: JSONObject): JSONObject = + withContext(Dispatchers.IO) { + Log.d(javaClass.simpleName, "put: " + Thread.currentThread().name) + // simulate delay + // delay(7000L) + val url = URL(dbUrl + resourcePath) + val con = url.openConnection() as HttpURLConnection + try { + con.doOutput = true + con.requestMethod = "PUT" + con.setChunkedStreamingMode(0) // write in chunks + con.setRequestProperty("Content-type", "application/json") + con.setRequestProperty("Accept", "application/json") + val writer = BufferedWriter(OutputStreamWriter(con.outputStream)) + writer.write(doc.toString()) + writer.close() + val reader = BufferedReader(InputStreamReader(con.inputStream)) + val obj = JSONObject(reader.readText()) + reader.close() + return@withContext obj + } finally { + con.disconnect() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/data/Rating.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/data/Rating.kt new file mode 100755 index 0000000..2bf47ec --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/data/Rating.kt @@ -0,0 +1,15 @@ +package de.luh.hci.mi.experiencesampling.data + +// A questionnaire item. +data class Item( + val title: String, + val question: String, + val negativeEnd: String, + val positiveEnd: String +) + +// A rating associated with a questionnaire item. +data class Rating( + val item: Item, + val value: Float +) diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/data/Repository.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/data/Repository.kt new file mode 100755 index 0000000..8ebe2d7 --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/data/Repository.kt @@ -0,0 +1,20 @@ +package de.luh.hci.mi.experiencesampling.data + +// A repository is an interface to (a category of) the app's data. +// The repository interface isolates the data layer from the rest of the app. +// https://developer.android.com/topic/architecture/data-layer +// https://developer.android.com/codelabs/basic-android-kotlin-training-repository-pattern +interface Repository { + + // Contexts a user may be in. + fun getContexts(): List + + // Questionnaire items. + fun getItems(): List + + // Submits the list of ratings. May throw an IOException. + suspend fun submitRatings(context: String, ratings: List) + + // Retrieves the ratings for the given context. May throw an IOException. + suspend fun getRatings(context: String): Map +} diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/data/RepositoryImpl.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/data/RepositoryImpl.kt new file mode 100755 index 0000000..c58723c --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/data/RepositoryImpl.kt @@ -0,0 +1,124 @@ +package de.luh.hci.mi.experiencesampling.data + +import android.content.Context +import android.util.Log +import de.luh.hci.mi.experiencesampling.R +import de.luh.hci.mi.experiencesampling.experimenter +import org.json.JSONObject +import java.net.URLEncoder + +// Implementation of the repository interface. It relies on a database as a data source, +// which is provided via the constructor (dependency injection). +// https://developer.android.com/topic/architecture/data-layer +// https://developer.android.com/codelabs/basic-android-kotlin-training-repository-pattern +class RepositoryImpl( + context: Context, // application context (not the user's context) + private val db: CouchDB +) : Repository { + + // Fixed list of contexts a user may be in. + private val contexts = listOf( + context.getString(R.string.context_1), + context.getString(R.string.context_2), + context.getString(R.string.context_3) + ) + + // Fixed list of questionnaire items. + private val items = listOf( + Item( + context.getString(R.string.understanding), + context.getString(R.string.understanding_question), + context.getString(R.string.complicated), + context.getString(R.string.easy) + ), + Item( + context.getString(R.string.interest), + context.getString(R.string.interest_question), + context.getString(R.string.not_interesting), + context.getString(R.string.interesting) + ), + Item( + context.getString(R.string.lecturer), + context.getString(R.string.lecturer_question), + context.getString(R.string.boring), + context.getString(R.string.exciting) + ), + Item( + context.getString(R.string.slides), + context.getString(R.string.slides_question), + context.getString(R.string.obstructive), + context.getString(R.string.supportive) + ), + ) + + override fun getContexts(): List { + return contexts + } + + override fun getItems(): List { + return items + } + + override suspend fun submitRatings(context: String, ratings: List) { + Log.d(javaClass.simpleName, "submitRatings: " + Thread.currentThread().name) + // store in online database + // create JSON document and send to server + val obj = JSONObject() + // timestamp + val time = System.currentTimeMillis() + obj.put("time", time) + obj.put("experimenter", experimenter) + obj.put("context", context) + for (rating in ratings) { + obj.put(rating.item.title, rating.value) + } + Log.d(javaClass.simpleName, obj.toString()) + val docId = "esm$time" + val response = db.put(docId, obj) + Log.d(javaClass.simpleName, response.toString()) + } + + /* + Example query: + https://couchdb.hci.uni-hannover.de/mobint/_design/esm/_view/ctxtexp?key=["Mobile Interaktion","MyExperimenterId"] + + Example get response: + {"total_rows":8,"offset":0,"rows":[ + {"id":"esm1686831673152","key":"Mensa","value":{"_id":"esm1686831673152","_rev":"1-dacc2a8f08698a838d291870f737f282","time":1686831673152,"experimenter":"MyExperimenterId","context":"Mensa","Mood":5,"Understanding":4,"Difficulty":3}}, + {"id":"esm1686834330948","key":"Mensa","value":{"_id":"esm1686834330948","_rev":"1-bceece5c4ae08cdf34dd5f2f2ff0ee5a","time":1686834330948,"experimenter":"MyExperimenterId","context":"Mensa","Mood":1,"Understanding":1,"Difficulty":1}} + ]} + */ + + override suspend fun getRatings(context: String): Map { + // query context for all experimenters + // val key = URLEncoder.encode("\"$context\"", "UTF-8") + // val query = "_design/esm/_view/ctxt?key=$key" + + // query context for one experimenter only + val key = URLEncoder.encode("[\"$context\",\"$experimenter\"]", "UTF-8") + val query = "_design/esm/_view/ctxtexp?key=$key" + Log.d(javaClass.simpleName, query) + val obj = db.get(query) + + // compute the average ratings per item from the set of retrieved values + val averageRatings: MutableMap = mutableMapOf() + val rows = obj.getJSONArray("rows") + val n = rows.length() + if (n > 0) { + for (i in 0 until n) { + val row = rows.getJSONObject(i).getJSONObject("value") + Log.d(javaClass.simpleName, "row: $row") + for (item in getItems()) { + val rating = row.getDouble(item.title).toFloat() + val sum = averageRatings[item] ?: 0f + averageRatings[item] = sum + rating + } + } + for (item in getItems()) { + val sum = averageRatings[item] ?: 0f + averageRatings[item] = sum / n + } + } + return averageRatings + } +} \ No newline at end of file diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/notifications/NotificationTrigger.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/notifications/NotificationTrigger.kt new file mode 100755 index 0000000..78f56cc --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/notifications/NotificationTrigger.kt @@ -0,0 +1,87 @@ +package de.luh.hci.mi.experiencesampling.notifications + +import android.Manifest +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Color +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import de.luh.hci.mi.experiencesampling.R + +// This component receives alarms and triggers a notification for each alarm. +// The alarm is registered in ExperienceSampling::scheduleNextNotification. +// https://developer.android.com/develop/ui/views/notifications +class NotificationTrigger : BroadcastReceiver() { + + // Theis broadcast receiver is registered to receive alarms. + override fun onReceive(context: Context, intent: Intent) { + showNotification(context) + } + + // Notifications are posted on notification channels, so create one if needed. + private fun createNotificationChannel(context: Context) { + val channelId = context.getString(R.string.channel_id) + val notificationManager = NotificationManagerCompat.from(context) + var channel = notificationManager.getNotificationChannel(channelId) + log("channel: $channel") + if (channel == null) { + val name = context.getString(R.string.channel_name) + val description = context.getString(R.string.channel_description) + val importance = NotificationManager.IMPORTANCE_DEFAULT + channel = NotificationChannel(channelId, name, importance) + channel.description = description + channel.enableLights(true) + channel.lightColor = Color.RED + channel.enableVibration(true) + channel.vibrationPattern = longArrayOf(0, 200, 500, 400, 500, 400, 500, 200, 500) + notificationManager.createNotificationChannel(channel) + } + } + + // Shows a notification. Clicking on the notification starts the sampling activity. + private fun showNotification(context: Context) { + createNotificationChannel(context) + + val intent = Intent(context, SamplingActivity::class.java) + intent.action = Intent.ACTION_VIEW + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + val pendingIntent: PendingIntent = + PendingIntent.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val channelId = context.getString(R.string.channel_id) + val builder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.notification_icon) + .setContentTitle(context.getString(R.string.what_experience)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(pendingIntent) // fire this intent when the user taps the notification + .setAutoCancel(true) // remove the notification when the user taps it + + // https://developer.android.com/training/permissions/requesting + val notificationManager = NotificationManagerCompat.from(context) + val permissionGranted = ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + log("permissionGranted: $permissionGranted") + + notificationManager.notify(0, builder.build()) + } + + // Logs a debug message. + private fun log(msg: String) { + Log.d(this.javaClass.simpleName, msg) + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/notifications/SamplingActivity.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/notifications/SamplingActivity.kt new file mode 100755 index 0000000..8d80f39 --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/notifications/SamplingActivity.kt @@ -0,0 +1,34 @@ +package de.luh.hci.mi.experiencesampling.notifications + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.lifecycle.viewmodel.compose.viewModel +import de.luh.hci.mi.experiencesampling.ui.sampling.SamplingScreen +import de.luh.hci.mi.experiencesampling.ui.sampling.SamplingViewModel +import de.luh.hci.mi.experiencesampling.ui.theme.ExperienceSamplingTheme + +// Shows the survey UI and submits the entered data to database server. +// Started as a response to a notification. +class SamplingActivity : ComponentActivity() { + // Associates the UI with the activity. + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + log("onCreate (response to notification)") + setContent { + ExperienceSamplingTheme { + SamplingScreen( + navigateBack = this::finish, // end activity when input is done + viewModel(factory = SamplingViewModel.Factory) // create view model (or get from cache) + ) + } + } + } + + // Logs a debug message. + private fun log(msg: String) { + Log.d(this.javaClass.simpleName, msg) + } + +} diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/sampling/SamplingScreen.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/sampling/SamplingScreen.kt new file mode 100755 index 0000000..3c2b447 --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/sampling/SamplingScreen.kt @@ -0,0 +1,109 @@ +package de.luh.hci.mi.experiencesampling.ui.sampling + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import de.luh.hci.mi.experiencesampling.R +import de.luh.hci.mi.experiencesampling.ui.statistics.RadioGroup +import kotlinx.coroutines.launch + +// UI for showing the questionnaire and taking an experience sample. +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SamplingScreen( + navigateBack: () -> Unit, // navigate back to previous screen + viewModel: SamplingViewModel // the view model of this screen +) { + // needed for showing a popup message + val snackbarHostState = remember { + SnackbarHostState() + } + // needed for showing a popup message + val scope = rememberCoroutineScope() + val thankYouMessage = stringResource(R.string.thank_you) + + Scaffold( + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + floatingActionButton = { + FloatingActionButton(onClick = { + println("FAB: " + Thread.currentThread().name) + viewModel.submit() + scope.launch { + println("sl: " + Thread.currentThread().name) + snackbarHostState.showSnackbar(thankYouMessage) + navigateBack() + } + }) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = stringResource(R.string.submit) + ) + } + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(Modifier.padding(8.dp)) + Text(stringResource(R.string.context), fontSize = 24.sp) + Text(stringResource(R.string.context_question)) + RadioGroup(viewModel.contexts, viewModel.selectedContext, viewModel::onContextSelected) + + LazyColumn( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + items(viewModel.items) { item -> + Spacer(Modifier.padding(8.dp)) + Text(item.title, fontSize = 24.sp) + if (item.question.isNotBlank()) { + Text(item.question) + } + Row { + Text(item.negativeEnd) + Spacer(modifier = Modifier.weight(1f)) + Text(item.positiveEnd) + } + val sliderState = viewModel.sliderState(item) + Slider( + value = sliderState.value, + onValueChange = { sliderState.value = it; println(it) }, + steps = 4, + valueRange = 0f..5f + ) + } + } + Spacer(Modifier.padding(48.dp)) + } + } +} diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/sampling/SamplingViewModel.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/sampling/SamplingViewModel.kt new file mode 100755 index 0000000..b59423a --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/sampling/SamplingViewModel.kt @@ -0,0 +1,102 @@ +package de.luh.hci.mi.experiencesampling.ui.sampling + +import android.util.Log +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import de.luh.hci.mi.experiencesampling.ExperienceSampling +import de.luh.hci.mi.experiencesampling.data.Item +import de.luh.hci.mi.experiencesampling.data.Rating +import de.luh.hci.mi.experiencesampling.data.Repository +import de.luh.hci.mi.experiencesampling.samplingInterval +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.io.IOException +import kotlin.math.roundToInt + +// ViewModel for the sampling screen. +class SamplingViewModel constructor( + private val app: ExperienceSampling, + private val repository: Repository, // the underlying repository (data model) + private val applicationScope: CoroutineScope, +) : ViewModel() { + + // contexts from data model, const + val contexts = repository.getContexts() + + // questionnaire items from data model, const + val items = repository.getItems() + + // the currently selected context + var selectedContext by mutableStateOf(contexts[0]) + private set + + fun onContextSelected(context: String) { + selectedContext = context + } + + // the current slider positions + private val sliderStates = mutableMapOf>() + + fun sliderState(item: Item): MutableState { + var slider = sliderStates[item] + if (slider == null) { + slider = mutableFloatStateOf(0f) + sliderStates[item] = slider + } + return slider + } + + // Submits the sample to the repository. + fun submit() { + // submit sample as a list of ratings + val sample = items.map { + Rating(it, sliderState(it).value.roundToInt().toFloat()) + } + // https://medium.com/androiddevelopers/coroutines-patterns-for-work-that-shouldnt-be-cancelled-e26c40f142ad + // The submission should not be cancelled when the user navigates away from the screen. + // Thus an app-level coroutine scope is used here. + applicationScope.launch { + try { + println("applicationScope.launch: " + Thread.currentThread().name) + repository.submitRatings(selectedContext, sample) + } catch (ex: IOException) { + log("exception trying to submit: $ex") + } + } + } + + // Called when this ViewModel is no longer used and will be destroyed. Can be used for cleanup. + override fun onCleared() { + log("onCleared") + // Navigating away from the sampling screen: Register next notification for sampling. + if (app.periodicSampling.value) { + app.scheduleNextNotification(samplingInterval) + } + } + + // Logs a debug message. + private fun log(msg: String) { + Log.d(this.javaClass.simpleName, msg) + } + + companion object { + // Companion object for creating the view model in the right lifecycle scope. + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val app = this[APPLICATION_KEY] as ExperienceSampling + val repository = app.repository + val scope = app.applicationScope + SamplingViewModel(app, repository, scope) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/start/StartScreen.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/start/StartScreen.kt new file mode 100755 index 0000000..1280b63 --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/start/StartScreen.kt @@ -0,0 +1,52 @@ +package de.luh.hci.mi.experiencesampling.ui.start + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import de.luh.hci.mi.experiencesampling.R +import de.luh.hci.mi.experiencesampling.Routes +import de.luh.hci.mi.experiencesampling.ui.theme.Purple40 +import de.luh.hci.mi.experiencesampling.ui.theme.Purple80 + +@Composable +fun StartScreen( + onNavigate: (route: String) -> Unit, // used to navigate to another screen + viewModel: StartViewModel, +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(Modifier.padding(24.dp)) + Button(onClick = { onNavigate(Routes.SAMPLING) }) { + Text(stringResource(R.string.enter_experience_sample)) + } + Spacer(Modifier.padding(24.dp)) + Button(onClick = { onNavigate(Routes.STATISTICS) }) { + Text(stringResource(R.string.show_statistics)) + } + Spacer(Modifier.padding(24.dp)) + Button( + onClick = { viewModel.togglePeriodicSampling() }, + colors = ButtonDefaults.buttonColors(if (viewModel.periodicSampling) Purple80 else Purple40) + ) { + if (viewModel.periodicSampling) { + Text(stringResource(R.string.stop_periodic_sampling)) + } else { + Text(stringResource(R.string.start_periodic_sampling)) + } + } + Spacer(Modifier.padding(24.dp)) + } +} diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/start/StartViewModel.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/start/StartViewModel.kt new file mode 100755 index 0000000..1c59873 --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/start/StartViewModel.kt @@ -0,0 +1,56 @@ +package de.luh.hci.mi.experiencesampling.ui.start + +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import de.luh.hci.mi.experiencesampling.ExperienceSampling +import de.luh.hci.mi.experiencesampling.data.Repository +import de.luh.hci.mi.experiencesampling.samplingInterval + +// ViewModel for the start screen. +class StartViewModel constructor( + private val app: ExperienceSampling, + private val repository: Repository, // the underlying repository (data model) +) : ViewModel() { + + var periodicSampling by app.periodicSampling + + fun togglePeriodicSampling() { + if (app.periodicSampling.value) { + log("stop sampling") + app.periodicSampling.value = false + app.cancelNotification() + } else { + log("start sampling") + app.scheduleNextNotification(samplingInterval) + app.periodicSampling.value = true + } + } + + // Called when this ViewModel is no longer used and will be destroyed. Can be used for cleanup. + override fun onCleared() { + log("onCleared") + } + + // Logs a debug message. + private fun log(msg: String) { + Log.d(this.javaClass.simpleName, msg) + } + + companion object { + // Companion object for creating the view model in the right lifecycle scope. + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val app = this[APPLICATION_KEY] as ExperienceSampling + val repository = app.repository + StartViewModel(app, repository) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/statistics/StatisticsScreen.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/statistics/StatisticsScreen.kt new file mode 100755 index 0000000..49210d0 --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/statistics/StatisticsScreen.kt @@ -0,0 +1,114 @@ +package de.luh.hci.mi.experiencesampling.ui.statistics + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.selection.selectable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Icon +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import de.luh.hci.mi.experiencesampling.ui.theme.Pink40 +import kotlin.math.roundToInt + +// The statistics screen shows average ratings for the selected context. +@Composable +fun StatisticsScreen( + navigateBack: () -> Unit, + viewModel: StatisticsViewModel +) { + // todo: implement + LazyColumn { + items(viewModel.items) { item -> + Text(item.title) + val ratingState = viewModel.ratingState(item) + RatingBar( + rating = ratingState.value, + startLabel = item.negativeEnd, + endLabel = item.positiveEnd + ) + } + } +} + +@Composable +fun RadioGroup(options: List, selectedOption: String, onOptionSelected: (String) -> Unit) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + options.forEach { option -> + Row( + Modifier + .fillMaxWidth() + .selectable( + selected = (option == selectedOption), + onClick = { onOptionSelected(option) } + ), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = (option == selectedOption), + onClick = { onOptionSelected(option) } + ) + Text(text = option) + } + } + } +} + +@Composable +fun RatingBar( + modifier: Modifier = Modifier, + rating: Float = 0f, + stars: Int = 5, + color: Color = Pink40, + startLabel: String = "", + endLabel: String = "" +) { + assert(stars in 0..10) + assert(rating in 0f..10f) + assert(rating <= stars) + val ratingStars = rating.roundToInt() + val restStars = stars - ratingStars + Column(horizontalAlignment = Alignment.CenterHorizontally) { + if (startLabel.isNotBlank() || endLabel.isNotBlank()) { + Text("(%.1f)".format(rating), modifier = modifier.padding(start = 8.dp)) + } + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(startLabel, modifier = modifier.padding(end = 8.dp)) + repeat(ratingStars) { + Icon(imageVector = Icons.Filled.Star, contentDescription = null, tint = color) + } + repeat(restStars) { + Icon( + imageVector = Icons.Filled.Star, + contentDescription = null, + tint = Color.LightGray + ) + } + Text(endLabel, modifier = modifier.padding(start = 8.dp)) + } + } +} + +@Preview +@Composable +fun RatingPreview() { + RatingBar(rating = 2.5f, startLabel = "very bad", endLabel = "very good") +} \ No newline at end of file diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/statistics/StatisticsViewModel.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/statistics/StatisticsViewModel.kt new file mode 100755 index 0000000..b1a33b0 --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/statistics/StatisticsViewModel.kt @@ -0,0 +1,102 @@ +package de.luh.hci.mi.experiencesampling.ui.statistics + +import android.util.Log +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import de.luh.hci.mi.experiencesampling.ExperienceSampling +import de.luh.hci.mi.experiencesampling.data.Item +import de.luh.hci.mi.experiencesampling.data.Repository +import kotlinx.coroutines.launch +import java.io.IOException + +// ViewModel for the statistics screen. +class StatisticsViewModel constructor( + private val repository: Repository, // the underlying repository (data model) +) : ViewModel() { + + // contexts from data model, const + val contexts = repository.getContexts() + + // questionnaire items from data model, const + val items = repository.getItems() + + // the currently selected context + var selectedContext by mutableStateOf(contexts[0]) + private set + + fun onContextSelected(context: String) { + if (context != selectedContext) { + selectedContext = context + viewModelScope.launch { + try { + ratings = repository.getRatings(context) + for (item in items) { + val rs = ratingState(item) + rs.value = ratings[item] ?: 0f + } + } catch (ex: IOException) { + log("exception trying to get ratings (onContextSelected): $ex") + } + } + } + } + + private var ratings: Map = mapOf() + private val ratingStates = mutableMapOf>() + + init { + viewModelScope.launch { + try { + ratings = repository.getRatings(selectedContext) + for (item in items) { + val rs = ratingState(item) + rs.value = ratings[item] ?: 0f + } + } catch (ex: IOException) { + log("exception trying to get ratings (init): $ex") + } + } + } + + fun ratingState(item: Item): MutableState { + var rating = ratingStates[item] + if (rating == null) { + rating = mutableFloatStateOf(0f) + ratingStates[item] = rating + } + return rating + } + + // Called when this ViewModel is no longer used and will be destroyed. Can be used for cleanup. + override fun onCleared() { + log("onCleared") + ratings = mapOf() + ratingStates.clear() + } + + // Logs a debug message. + private fun log(msg: String) { + Log.d(this.javaClass.simpleName, msg) + } + + companion object { + // Companion object for creating the view model in the right lifecycle scope. + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val app = this[APPLICATION_KEY] as ExperienceSampling + val repository = app.repository + StatisticsViewModel(repository) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/theme/Color.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/theme/Color.kt new file mode 100755 index 0000000..343cc71 --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package de.luh.hci.mi.experiencesampling.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/theme/Theme.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/theme/Theme.kt new file mode 100755 index 0000000..2441023 --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/theme/Theme.kt @@ -0,0 +1,70 @@ +package de.luh.hci.mi.experiencesampling.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun ExperienceSamplingTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/theme/Type.kt b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/theme/Type.kt new file mode 100755 index 0000000..9b58027 --- /dev/null +++ b/app/src/main/java/de/luh/hci/mi/experiencesampling/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package de.luh.hci.mi.experiencesampling.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100755 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100755 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/notification_icon.png b/app/src/main/res/drawable/notification_icon.png new file mode 100755 index 0000000..1bfbe9d Binary files /dev/null and b/app/src/main/res/drawable/notification_icon.png differ diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100755 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100755 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100755 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100755 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100755 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100755 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100755 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100755 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100755 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100755 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100755 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100755 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100755 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100755 index 0000000..2d1576a --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,45 @@ + + ExperienceSampling + Submit + Context + Experience Sampling + Experience Sampling + This channel sends notifications that request inputting experience samples. + What is your current experience? + Mobile Interaktion + Programmieren 1 + Mensa + Thank you! + What is your current context? + Enter Experience Sample + Show Statistics + Stop Periodic Sampling + Start Periodic Sampling + + + Understanding + How easy to comprehend is the lecture material presented? + Interest + How interesting is today\'s topic for you? + Lecturer + How would you rate the lecturers preseting style? + Slides + Rate how the slides complement the presented material. + School Grade + Rate today\'s lecture using school grades + Overall + Did you enjoy today\'s lecture overall? + + + complicated + easy + not interesting + interesting + boring + exciting + obstructive + supportive + + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100755 index 0000000..35a6af6 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +