From 098b46fdae2dcd7b651b1ca2cacc28965bfde323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Dom=C3=ADnguez?= Date: Thu, 16 May 2024 21:48:40 +0200 Subject: [PATCH] Revert "Remove external components" This reverts commit f7fc3c88a9b97c80eafcb24fb7a8d3ebe2643788. --- app/src/main/AndroidManifest.xml | 7 + .../bienestaremocional/data/FirstExecution.kt | 3 + .../bienestaremocional/data/crypto/Crypto.kt | 8 +- .../data/phonecalls/PhoneInfo.kt | 67 ++++++ .../data/remote/RemoteAPI.kt | 3 + .../data/trafficstats/main.kt | 26 +++ .../bienestaremocional/data/usage/Usage.kt | 206 ++++++++++++++++++ .../data/worker/UploadPhoneDataWorker.kt | 74 +++++++ .../data/worker/UploadTrafficDataWorker.kt | 78 +++++++ .../data/worker/UploadUsageInfoWorker.kt | 56 +++++ .../data/worker/WorkAdministrator.kt | 34 +++ .../data/worker/WorkAdministratorImpl.kt | 111 ++++++++++ .../repository/remote/RemoteRepository.kt | 3 + .../repository/remote/RemoteRepositoryImpl.kt | 12 + .../ui/screens/permission/PermissionScreen.kt | 79 +++++++ .../screens/permission/PermissionViewModel.kt | 4 + .../ui/screens/splash/SplashViewModel.kt | 12 +- app/src/main/res/values-en/strings.xml | 8 +- app/src/main/res/values/strings.xml | 10 +- 19 files changed, 797 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/es/upm/bienestaremocional/data/phonecalls/PhoneInfo.kt create mode 100644 app/src/main/java/es/upm/bienestaremocional/data/trafficstats/main.kt create mode 100644 app/src/main/java/es/upm/bienestaremocional/data/usage/Usage.kt create mode 100644 app/src/main/java/es/upm/bienestaremocional/data/worker/UploadPhoneDataWorker.kt create mode 100644 app/src/main/java/es/upm/bienestaremocional/data/worker/UploadTrafficDataWorker.kt create mode 100644 app/src/main/java/es/upm/bienestaremocional/data/worker/UploadUsageInfoWorker.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c771fdaa..036d130b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,13 @@ + + + + + + diff --git a/app/src/main/java/es/upm/bienestaremocional/data/FirstExecution.kt b/app/src/main/java/es/upm/bienestaremocional/data/FirstExecution.kt index 61fb052a..bbfaa938 100644 --- a/app/src/main/java/es/upm/bienestaremocional/data/FirstExecution.kt +++ b/app/src/main/java/es/upm/bienestaremocional/data/FirstExecution.kt @@ -25,6 +25,9 @@ suspend fun firstTimeExecution( workAdministrator.scheduleDailyNightNotificationWorker() workAdministrator.scheduleOneOffNotificationWorker() workAdministrator.scheduleUploadWorker() + workAdministrator.scheduleUploadPhoneDataWorker() + workAdministrator.scheduleUploadTrafficDataWorker() + //insert values in last upload table val now = obtainTimestamp(Instant.now(), null) diff --git a/app/src/main/java/es/upm/bienestaremocional/data/crypto/Crypto.kt b/app/src/main/java/es/upm/bienestaremocional/data/crypto/Crypto.kt index 71971fde..01db3579 100644 --- a/app/src/main/java/es/upm/bienestaremocional/data/crypto/Crypto.kt +++ b/app/src/main/java/es/upm/bienestaremocional/data/crypto/Crypto.kt @@ -37,4 +37,10 @@ private fun generateRawUUID(): String = UUID.randomUUID().toString() * Generates a User ID using randomUUID and sha256 hash * @return User ID in hexadecimal format */ -fun generateUID(): String = sha512hash(generateRawUUID()) \ No newline at end of file +fun generateUID(): String = sha512hash(generateRawUUID()) + +/** + * Encrypt private user data + * @return User ID in String format + */ +fun securePrivateData(message: String): String = sha512hash(message) \ No newline at end of file diff --git a/app/src/main/java/es/upm/bienestaremocional/data/phonecalls/PhoneInfo.kt b/app/src/main/java/es/upm/bienestaremocional/data/phonecalls/PhoneInfo.kt new file mode 100644 index 00000000..7b3f5488 --- /dev/null +++ b/app/src/main/java/es/upm/bienestaremocional/data/phonecalls/PhoneInfo.kt @@ -0,0 +1,67 @@ +package es.upm.bienestaremocional.data.phonecalls + +import android.Manifest.permission.READ_CALL_LOG +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.database.Cursor +import android.provider.CallLog.Calls.* +import androidx.core.content.ContextCompat +import es.upm.bienestaremocional.data.crypto.securePrivateData + +class PhoneInfo { + + fun getCallLogs(context: Context): String { + //check permissions + if (checkPermissions(context)) { + val c = context.applicationContext + val projection = arrayOf(CACHED_NAME, NUMBER, DATE, DURATION) + + val cursor = c.contentResolver.query( + CONTENT_URI, + projection, + null, + null, + null, + null + ) + return cursorToList(cursor) + } + return "N/A" + } + + private fun cursorToList(cursor: Cursor?): String { + var message = "{" + cursor?.use { + while (it.moveToNext()) { + var json: String = " \"Call\": {\"Name\": \"" + securePrivateData( + it.getStringFromColumn(CACHED_NAME) + ) + + "\", \"Number\": \"" + securePrivateData(it.getStringFromColumn(NUMBER)) + + "\", \"Date\": \"" + it.getStringFromColumn(DATE) + + "\", \"Duration\": \"" + it.getStringFromColumn(DURATION) + + "\"}" + + if (it.moveToNext()) + json += "," + + message += json + } + message += "}" + } + return message + } + + @SuppressLint("Range") + private fun Cursor.getStringFromColumn(columnName: String) = + getString(getColumnIndex(columnName)) + + private fun checkPermissions(context: Context): Boolean { + val permission = ContextCompat.checkSelfPermission( + context, + READ_CALL_LOG + ) + + return permission == PackageManager.PERMISSION_GRANTED + } +} \ No newline at end of file diff --git a/app/src/main/java/es/upm/bienestaremocional/data/remote/RemoteAPI.kt b/app/src/main/java/es/upm/bienestaremocional/data/remote/RemoteAPI.kt index 8c88dffa..294e6e8b 100644 --- a/app/src/main/java/es/upm/bienestaremocional/data/remote/RemoteAPI.kt +++ b/app/src/main/java/es/upm/bienestaremocional/data/remote/RemoteAPI.kt @@ -29,4 +29,7 @@ interface RemoteAPI { @POST("/one_off_questionnaires") suspend fun postOneOffQuestionnaires(@Body data: OneOffQuestionnairesRequest): Response + + @POST("/bg_data") + suspend fun postBackgroundData(@Body message: String): Response } \ No newline at end of file diff --git a/app/src/main/java/es/upm/bienestaremocional/data/trafficstats/main.kt b/app/src/main/java/es/upm/bienestaremocional/data/trafficstats/main.kt new file mode 100644 index 00000000..cbfcf9f8 --- /dev/null +++ b/app/src/main/java/es/upm/bienestaremocional/data/trafficstats/main.kt @@ -0,0 +1,26 @@ +package es.upm.bienestaremocional.data.trafficstats + +import android.annotation.SuppressLint +import android.net.TrafficStats + +class Traffic { + + fun init(): String { + if (TrafficStats.getTotalRxBytes() != TrafficStats.UNSUPPORTED.toLong() && + TrafficStats.getTotalTxBytes() != TrafficStats.UNSUPPORTED.toLong()) { + return run() + } + return "{\"WiFI\": \"N/A Kb\", \"Mobile\": \"N/A Kb\"}" + } + + @SuppressLint("SetTextI18n") + fun run(): String { + val mobile = TrafficStats.getMobileRxBytes() + TrafficStats.getMobileTxBytes() + val total = TrafficStats.getTotalRxBytes() + TrafficStats.getTotalTxBytes() + val wiFi: Long = (total - mobile) / 1024 + val mobileData: Long = mobile / 1024 + + return "{\"WiFI\": \"$wiFi Kb\", \"Mobile\": \"$mobileData Kb\"}" + } + +} \ No newline at end of file diff --git a/app/src/main/java/es/upm/bienestaremocional/data/usage/Usage.kt b/app/src/main/java/es/upm/bienestaremocional/data/usage/Usage.kt new file mode 100644 index 00000000..65c73be5 --- /dev/null +++ b/app/src/main/java/es/upm/bienestaremocional/data/usage/Usage.kt @@ -0,0 +1,206 @@ +package es.upm.bienestaremocional.data.usage + +import android.app.usage.UsageStats +import android.app.usage.UsageStatsManager +import android.content.Context +import android.content.Context.USAGE_STATS_SERVICE +import android.os.Build +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.* +import androidx.annotation.RequiresApi +import java.util.* + +class Usage( + private val logTag: String +) { + private var mUsageStatsManager: UsageStatsManager? = null + private var mAdapter: Usage.UsageStatsAdapter? = null + + var usageInfo: String = "" + + @RequiresApi(Build.VERSION_CODES.Q) + internal inner class UsageStatsAdapter : BaseAdapter() { + private val mPackageStats = ArrayList() + + override fun getCount(): Int { + return mPackageStats.size + } + + override fun getItem(position: Int): Any { + return mPackageStats[position] + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getView(position: Int, convertView: View, parent: ViewGroup): View { + + return convertView + } + + init { + val cal = Calendar.getInstance() + cal.add(Calendar.HOUR, -6) + val stats = mUsageStatsManager!!.queryUsageStats( + UsageStatsManager.INTERVAL_BEST, + cal.timeInMillis, System.currentTimeMillis() + ) + if (stats != null) { + val statCount = stats.size + + for (i in 0 until statCount) { + val pkgStats = stats[i] + val type = findApp(pkgStats.packageName) + var message: String + if ((type != "") && (pkgStats.totalTimeVisible > 0)) { + if (usageInfo != "") + usageInfo += ", " + message = "\"Apps\": { \"AppName\": \"" + pkgStats.packageName + + "\", \"firstTimeStamp\": " + pkgStats.firstTimeStamp + + ", \"lastTimeStamp\": " + pkgStats.lastTimeStamp + + ", \"lastTimeUsed\": " + pkgStats.lastTimeUsed + + ", \"lastTimeVisible\": " + pkgStats.lastTimeVisible + + ", \"totalTimeVisible\": " + pkgStats.totalTimeVisible + + ", \"AppType\": \"" + type + "\"}" + + usageInfo += message + } + } + } + else + { + Log.d(logTag, "Usage data not available") + } + } + } + + @RequiresApi(Build.VERSION_CODES.Q) + fun getAppUsage(context: Context): String { + mUsageStatsManager = context.getSystemService(USAGE_STATS_SERVICE) as UsageStatsManager + + mAdapter = UsageStatsAdapter() + + if (usageInfo != "") + return usageInfo + + return "\"Apps\": \"N/A\"" + } + + fun findApp(appName: String): String { + val type = "N/A" + val rRSS = listOf( + "facebook", + "twitter", + "instagram", + "tiktok", + "snapchat", + "whatsapp", + "messenger", + "telegram" + ) + val dating = listOf("tinder", "badoo", "meetic", "bumble", "grindr") + val games = listOf( + "candy", + "mine", + "treasure", + "crush", + "sudoku", + "game", + "pokemongo", + "impact", + "scape", + "among", + "otome", + "madness", + "zombies" + ) + val entertaining = listOf( + "youtube", + "netflix", + "hbo", + "disney", + "prime", + "video", + "ivoox", + "tiktok", + "audible", + "book", + "crunchyroll", + "firefox", + "opera", + "chrome", + "9gag", + "los40", + "spotify", + "rtve", + "bbc", + "duolingo" + ) + val house = listOf( + "santander", + "bbva", + "bankinter", + "openbank", + "repsol", + "naturgy", + "iberdrola", + "tapo", + "tplink", + "aeat", + "amazon", + "cl@ve", + "sodexo", + "zooplus", + "wallapop" + ) + val work = listOf( + "office", + "word", + "excel", + "powerpoint", + "authenticator", + "teams", + "slack", + "zoom", + "moodle" + ) + + val rRSSCount = rRSS.size + for (i in 0 until rRSSCount) { + if (appName.contains(rRSS[i])) + return "RRSS" + } + val gamesCount = games.size + for (i in 0 until gamesCount) { + if (appName.contains(games[i])) + return "games" + } + val datingCount = dating.size + for (i in 0 until datingCount) { + if (appName.contains(dating[i])) + return "dating" + } + val houseCount = house.size + for (i in 0 until houseCount) { + if (appName.contains(house[i])) + return "house" + } + val workCount = work.size + for (i in 0 until workCount) { + if (appName.contains(work[i])) + return "work" + } + val entertainingCount = entertaining.size + for (i in 0 until entertainingCount) { + if (appName.contains(entertaining[i])) + return "entertaining" + } + + return type + } + + +} \ No newline at end of file diff --git a/app/src/main/java/es/upm/bienestaremocional/data/worker/UploadPhoneDataWorker.kt b/app/src/main/java/es/upm/bienestaremocional/data/worker/UploadPhoneDataWorker.kt new file mode 100644 index 00000000..7c1f4365 --- /dev/null +++ b/app/src/main/java/es/upm/bienestaremocional/data/worker/UploadPhoneDataWorker.kt @@ -0,0 +1,74 @@ +package es.upm.bienestaremocional.data.worker + +import android.Manifest.permission.READ_CALL_LOG +import android.content.Context +import android.content.pm.PackageManager +import android.util.Log +import androidx.core.content.ContextCompat +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import es.upm.bienestaremocional.data.info.AppInfo +import es.upm.bienestaremocional.data.phonecalls.PhoneInfo +import es.upm.bienestaremocional.domain.repository.remote.RemoteRepository +import java.time.Duration +import java.time.LocalDateTime +import javax.inject.Named + +@HiltWorker +class UploadPhoneDataWorker @AssistedInject constructor( + @Assisted val appContext: Context, + @Assisted workerParams: WorkerParameters, + @Named("logTag") private val logTag: String, + private val appInfo: AppInfo, + private val remoteRepository: RemoteRepository +) : CoroutineWorker(appContext, workerParams) { + companion object : Schedulable { + //No offset + override val initialTime: LocalDateTime? = null + + override val tag: String = "upload_phone_data" + + override val repeatInterval: Duration = Duration.ofSeconds(7200) + } + + override suspend fun doWork(): Result { + Log.d(logTag, "Executing Upload Phone Data Worker") + + val permissionCall = ContextCompat.checkSelfPermission( + appContext, + READ_CALL_LOG + ) + + lateinit var result: Result + + // Indicate whether the work finished successfully with the Result + if (permissionCall == PackageManager.PERMISSION_GRANTED) { + val phone = PhoneInfo() + val listCalls = phone.getCallLogs(appContext) + val userId = appInfo.getUserID() + val currentTime = System.currentTimeMillis()/1000 + + val message = "{ \"userId\": \"$userId\", \"timestamp\": \"$currentTime\", \"databg\": { \"PhoneInfo\":$listCalls}}" + val success = remoteRepository.postBackgroundData(message) + + result = if (success) { + Log.d(logTag, "Inserted phone info") + Result.success() + } + else { + Log.d(logTag, "Not inserted phone info") + Result.retry() + } + } + else { + Log.d("hola", "permissionCall $permissionCall") + Log.d(logTag, "Not enough permissions to post phone data") + result = Result.failure() + } + + return result + } +} diff --git a/app/src/main/java/es/upm/bienestaremocional/data/worker/UploadTrafficDataWorker.kt b/app/src/main/java/es/upm/bienestaremocional/data/worker/UploadTrafficDataWorker.kt new file mode 100644 index 00000000..c6556cff --- /dev/null +++ b/app/src/main/java/es/upm/bienestaremocional/data/worker/UploadTrafficDataWorker.kt @@ -0,0 +1,78 @@ +package es.upm.bienestaremocional.data.worker + +import android.Manifest.permission.ACCESS_COARSE_LOCATION +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.content.Context +import android.content.pm.PackageManager +import android.util.Log +import androidx.core.content.ContextCompat +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import es.upm.bienestaremocional.data.info.AppInfo +import es.upm.bienestaremocional.data.trafficstats.Traffic +import es.upm.bienestaremocional.domain.repository.remote.RemoteRepository +import java.time.Duration +import java.time.LocalDateTime +import javax.inject.Named + +@HiltWorker +class UploadTrafficDataWorker @AssistedInject constructor( + @Assisted val appContext: Context, + @Assisted workerParams: WorkerParameters, + @Named("logTag") private val logTag: String, + private val appInfo: AppInfo, + private val remoteRepository: RemoteRepository +) : CoroutineWorker(appContext, workerParams) { + companion object : Schedulable { + //No offset + override val initialTime: LocalDateTime? = null + + override val tag: String = "upload_traffic_data" + + override val repeatInterval: Duration = Duration.ofSeconds(3600) + } + + override suspend fun doWork(): Result { + Log.d(logTag, "Executing Upload Traffic Data Worker") + + val permissionCoarse = ContextCompat.checkSelfPermission( + appContext, + ACCESS_COARSE_LOCATION + ) + val permissionFine = ContextCompat.checkSelfPermission( + appContext, + ACCESS_FINE_LOCATION + ) + + lateinit var result: Result + + // Indicate whether the work finished successfully with the Result + if (permissionCoarse == PackageManager.PERMISSION_GRANTED && permissionFine == PackageManager.PERMISSION_GRANTED) { + val traffic = Traffic() + val trafficMessage = traffic.init() + val userId = appInfo.getUserID() + val currentTime = System.currentTimeMillis()/1000 + val message = + "{ \"userId\": \"$userId\", \"timestamp\": \"$currentTime\", \"databg\": { \"InternetInfo\": $trafficMessage}}" + val success = remoteRepository.postBackgroundData(message) + + result = if (success) { + Log.d(logTag, "Inserted internet info") + Result.success() + } + else { + Log.d(logTag, "Not inserted internet info") + Result.retry() + } + } + else { + Log.d(logTag, "Not enough permissions to post traffic data") + result = Result.failure() + } + + return result + } +} diff --git a/app/src/main/java/es/upm/bienestaremocional/data/worker/UploadUsageInfoWorker.kt b/app/src/main/java/es/upm/bienestaremocional/data/worker/UploadUsageInfoWorker.kt new file mode 100644 index 00000000..5ff0ecff --- /dev/null +++ b/app/src/main/java/es/upm/bienestaremocional/data/worker/UploadUsageInfoWorker.kt @@ -0,0 +1,56 @@ +package es.upm.bienestaremocional.data.worker + +import android.content.Context +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import es.upm.bienestaremocional.data.info.AppInfo +import es.upm.bienestaremocional.data.usage.Usage +import es.upm.bienestaremocional.di.AppModule +import es.upm.bienestaremocional.domain.repository.remote.RemoteRepository +import javax.inject.Named + +@HiltWorker +class UploadUsageInfoWorker @AssistedInject constructor( + @Assisted val appContext: Context, + @Assisted workerParams: WorkerParameters, + @Named("logTag") private val logTag: String, + private val remoteRepository: RemoteRepository +) : CoroutineWorker(appContext, workerParams) { + companion object : OneTimeWorker { + override val tag: String = "upload_usage_info_data" + } + + @RequiresApi(Build.VERSION_CODES.Q) + override suspend fun doWork(): Result { + Log.d(logTag, "Executing Upload Usage Info Worker") + + lateinit var result: Result + + //Execute app usage + val usage = Usage(logTag = logTag) + val listApps = usage.getAppUsage(appContext) + + val appInfo: AppInfo = AppModule.provideAppInfo(appContext) + val currentTime = System.currentTimeMillis()/1000 + + val userId = appInfo.getUserID() + val message = "{ \"userId\": \"$userId\", \"timestamp\": \"$currentTime\", \"databg\": {\"UsageInfo\": {$listApps}}}" + val success = remoteRepository.postBackgroundData(message) + + result = if (success) { + Log.d(logTag, "Inserted usage info") + Result.success() + } + else { + Log.d(logTag, "Not inserted usage info") + Result.failure() + } + return result + } +} diff --git a/app/src/main/java/es/upm/bienestaremocional/data/worker/WorkAdministrator.kt b/app/src/main/java/es/upm/bienestaremocional/data/worker/WorkAdministrator.kt index ab71f239..2e292c63 100644 --- a/app/src/main/java/es/upm/bienestaremocional/data/worker/WorkAdministrator.kt +++ b/app/src/main/java/es/upm/bienestaremocional/data/worker/WorkAdministrator.kt @@ -1,5 +1,7 @@ package es.upm.bienestaremocional.data.worker +import android.os.Build +import androidx.annotation.RequiresApi import androidx.lifecycle.LiveData import androidx.work.WorkInfo @@ -48,6 +50,38 @@ interface WorkAdministrator { */ fun cancelUploadWorker() + /** + * Schedule [UploadPhoneDataWorker] + */ + fun scheduleUploadPhoneDataWorker() + + /** + * Cancel [UploadPhoneDataWorker] + */ + fun cancelUploadPhoneDataWorker() + + /** + * Schedule [UploadTrafficDataWorker] + */ + fun scheduleUploadTrafficDataWorker() + + /** + * Cancel [UploadTrafficDataWorker] + */ + fun cancelUploadTrafficDataWorker() + + /** + * Schedule [UploadUsageInfoWorker] + */ + @RequiresApi(Build.VERSION_CODES.Q) + fun scheduleUploadUsageInfoWorker() + + /** + * Cancel [UploadUsageInfoWorker] + */ + @RequiresApi(Build.VERSION_CODES.Q) + fun cancelUploadUsageInfoWorker() + /** * Query workers status */ diff --git a/app/src/main/java/es/upm/bienestaremocional/data/worker/WorkAdministratorImpl.kt b/app/src/main/java/es/upm/bienestaremocional/data/worker/WorkAdministratorImpl.kt index 4dd0b6ae..1395d774 100644 --- a/app/src/main/java/es/upm/bienestaremocional/data/worker/WorkAdministratorImpl.kt +++ b/app/src/main/java/es/upm/bienestaremocional/data/worker/WorkAdministratorImpl.kt @@ -1,11 +1,16 @@ package es.upm.bienestaremocional.data.worker import android.content.Context +import android.os.Build import android.util.Log +import androidx.annotation.RequiresApi import androidx.lifecycle.LiveData import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy import androidx.work.ListenableWorker +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest import androidx.work.PeriodicWorkRequest import androidx.work.WorkInfo import androidx.work.WorkManager @@ -88,6 +93,36 @@ class WorkAdministratorImpl( Log.d(logTag, "The request with tag $tag has been cancelled") } + /** + * Schedules an one time request + * @param workerClass Class associated with the worker to request + * @param oneTimeWorker Info to schedule the one time request + * @param constraints Optional constraints that must be fulfilled to execute the request + */ + private fun oneTimeRequest( + workerClass: Class, + oneTimeWorker: OneTimeWorker, + constraints: Constraints? = null, + ) { + Log.d(logTag, "Setting one time request with tag: ${oneTimeWorker.tag}") + + // Build request. If we have received constants, set them + + val requestBuilder = OneTimeWorkRequest.Builder(workerClass) + + constraints?.let { + requestBuilder.setConstraints(it) + } + + workManager.enqueueUniqueWork( + oneTimeWorker.tag, + ExistingWorkPolicy.KEEP, + requestBuilder.build() + ) + + Log.d(logTag, "The request shall be triggered") + } + override fun scheduleDailyMorningNotificationWorker() { with(DailyMorningNotificationWorker) { @@ -143,6 +178,79 @@ class WorkAdministratorImpl( override fun cancelUploadWorker() = cancelRequest(UploadWorker.tag) + /** + * Schedule [UploadPhoneDataWorker] + */ + override fun scheduleUploadPhoneDataWorker() { + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + with(UploadPhoneDataWorker) + { + scheduleRequest( + workerClass = UploadPhoneDataWorker::class.java, + schedulable = this, + constraints = constraints + ) + } + } + + /** + * Cancel [UploadPhoneDataWorker] + */ + override fun cancelUploadPhoneDataWorker() { + cancelRequest(UploadPhoneDataWorker.tag) + } + + /** + * Schedule [UploadTrafficDataWorker] + */ + override fun scheduleUploadTrafficDataWorker() { + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + with(UploadTrafficDataWorker) + { + scheduleRequest( + workerClass = UploadTrafficDataWorker::class.java, + schedulable = this, + constraints = constraints + ) + } + } + + /** + * Cancel [UploadTrafficDataWorker] + */ + override fun cancelUploadTrafficDataWorker() { + cancelRequest(UploadTrafficDataWorker.tag) + } + + @RequiresApi(Build.VERSION_CODES.Q) + override fun scheduleUploadUsageInfoWorker() { + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + with(UploadUsageInfoWorker) + { + oneTimeRequest( + workerClass = UploadUsageInfoWorker::class.java, + oneTimeWorker = this, + constraints = constraints + ) + } + } + + override fun cancelUploadUsageInfoWorker() { + cancelRequest(UploadUsageInfoWorker.tag) + } + override fun queryWorkerStatus(): LiveData> { return workManager.getWorkInfosLiveData( WorkQuery.fromUniqueWorkNames( @@ -150,6 +258,9 @@ class WorkAdministratorImpl( DailyNightNotificationWorker.tag, OneOffNotificationWorker.tag, UploadWorker.tag, + UploadPhoneDataWorker.tag, + UploadTrafficDataWorker.tag, + UploadUsageInfoWorker.tag ) ) } diff --git a/app/src/main/java/es/upm/bienestaremocional/domain/repository/remote/RemoteRepository.kt b/app/src/main/java/es/upm/bienestaremocional/domain/repository/remote/RemoteRepository.kt index 07dad915..7ff61940 100644 --- a/app/src/main/java/es/upm/bienestaremocional/domain/repository/remote/RemoteRepository.kt +++ b/app/src/main/java/es/upm/bienestaremocional/domain/repository/remote/RemoteRepository.kt @@ -16,6 +16,9 @@ interface RemoteRepository { suspend fun postUserData(userDataRequest: UserDataRequest): UserDataResponse? suspend fun postDailyQuestionnaires(dailyQuestionnairesRequest: DailyQuestionnairesRequest): DailyQuestionnairesResponse? + suspend fun postOneOffQuestionnaires(oneOffQuestionnairesRequest: OneOffQuestionnairesRequest): OneOffQuestionnairesResponse? + + suspend fun postBackgroundData(message: String): Boolean } \ No newline at end of file diff --git a/app/src/main/java/es/upm/bienestaremocional/domain/repository/remote/RemoteRepositoryImpl.kt b/app/src/main/java/es/upm/bienestaremocional/domain/repository/remote/RemoteRepositoryImpl.kt index d3814361..4c086774 100644 --- a/app/src/main/java/es/upm/bienestaremocional/domain/repository/remote/RemoteRepositoryImpl.kt +++ b/app/src/main/java/es/upm/bienestaremocional/domain/repository/remote/RemoteRepositoryImpl.kt @@ -105,4 +105,16 @@ class RemoteRepositoryImpl( } return response } + + override suspend fun postBackgroundData(message: String): Boolean { + try { + remoteAPI.postBackgroundData(message) + + } + catch (e: Exception) { + Log.e(logTag, "response failed with exception $e") + return false + } + return true + } } diff --git a/app/src/main/java/es/upm/bienestaremocional/ui/screens/permission/PermissionScreen.kt b/app/src/main/java/es/upm/bienestaremocional/ui/screens/permission/PermissionScreen.kt index 4edd7295..eb8d7dbc 100644 --- a/app/src/main/java/es/upm/bienestaremocional/ui/screens/permission/PermissionScreen.kt +++ b/app/src/main/java/es/upm/bienestaremocional/ui/screens/permission/PermissionScreen.kt @@ -1,7 +1,14 @@ package es.upm.bienestaremocional.ui.screens.permission +import android.Manifest.permission.ACCESS_COARSE_LOCATION +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.Manifest.permission.READ_CALL_LOG +import android.content.Context +import android.content.pm.PackageManager import android.os.Build +import android.provider.Settings import android.widget.Toast +import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement @@ -25,6 +32,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat import androidx.health.connect.client.HealthConnectClient import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination @@ -116,6 +124,58 @@ private fun PermissionScreen( textAlign = TextAlign.Justify ) + Text( + text = stringResource(id = R.string.permission_log_call_description), + textAlign = TextAlign.Justify + ) + TextButton( + onClick = { + checkAndRequestPermission( + context, + READ_CALL_LOG, + launcher + ) + } + ) { + Text(text = stringResource(R.string.permission_log_call)) + } + + Text( + text = stringResource(id = R.string.permission_location_description), + textAlign = TextAlign.Justify + ) + TextButton( + onClick = { + checkAndRequestPermission( + context, + ACCESS_FINE_LOCATION, + launcher + ) + checkAndRequestPermission( + context, + ACCESS_COARSE_LOCATION, + launcher + ) + } + ) { + Text(text = stringResource(R.string.permission_location)) + } + + Text( + text = stringResource(id = R.string.permission_usage_description), + textAlign = TextAlign.Justify + ) + TextButton( + onClick = { + openForeignActivity( + context = context, + action = Settings.ACTION_USAGE_ACCESS_SETTINGS + ) + } + ) { + Text(text = stringResource(id = R.string.permission_usage)) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { Text( text = stringResource(id = R.string.permission_notification_description), @@ -154,6 +214,25 @@ private fun PermissionScreen( } } + +fun checkAndRequestPermission( + context: Context, + permission: String, + launcher: ManagedActivityResultLauncher +) { + val permissionCheckResult = ContextCompat.checkSelfPermission(context, permission) + if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) { + Toast.makeText( + context, + R.string.permission_granted, + Toast.LENGTH_LONG + ).show() + } + else { + launcher.launch(permission) + } +} + @Preview( showBackground = true, group = "Light Theme" diff --git a/app/src/main/java/es/upm/bienestaremocional/ui/screens/permission/PermissionViewModel.kt b/app/src/main/java/es/upm/bienestaremocional/ui/screens/permission/PermissionViewModel.kt index 40b67502..9e8536e0 100644 --- a/app/src/main/java/es/upm/bienestaremocional/ui/screens/permission/PermissionViewModel.kt +++ b/app/src/main/java/es/upm/bienestaremocional/ui/screens/permission/PermissionViewModel.kt @@ -34,6 +34,10 @@ class PermissionViewModel @Inject constructor( workAdministrator = workAdministrator, lastUploadRepository = lastUploadRepository ) + //Execute app usage + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + workAdministrator.scheduleUploadUsageInfoWorker() + } } } } \ No newline at end of file diff --git a/app/src/main/java/es/upm/bienestaremocional/ui/screens/splash/SplashViewModel.kt b/app/src/main/java/es/upm/bienestaremocional/ui/screens/splash/SplashViewModel.kt index 847d5e51..3a38cab2 100644 --- a/app/src/main/java/es/upm/bienestaremocional/ui/screens/splash/SplashViewModel.kt +++ b/app/src/main/java/es/upm/bienestaremocional/ui/screens/splash/SplashViewModel.kt @@ -1,5 +1,6 @@ package es.upm.bienestaremocional.ui.screens.splash +import android.os.Build import androidx.compose.runtime.Composable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -8,6 +9,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import es.upm.bienestaremocional.data.healthconnect.HealthConnectAvailability import es.upm.bienestaremocional.data.info.AppInfo import es.upm.bienestaremocional.data.settings.AppSettings +import es.upm.bienestaremocional.data.worker.WorkAdministrator import es.upm.bienestaremocional.ui.screens.destinations.ErrorScreenDestination import es.upm.bienestaremocional.ui.screens.destinations.HomeScreenDestination import es.upm.bienestaremocional.ui.screens.destinations.OnboardingScreenDestination @@ -24,6 +26,7 @@ class SplashViewModel @Inject constructor( private val healthConnectAvailability: HealthConnectAvailability, private val appSettings: AppSettings, private val appInfo: AppInfo, + private val workAdministrator: WorkAdministrator, ) : ViewModel() { private val _state: MutableStateFlow = MutableStateFlow(SplashState.Init) @@ -34,7 +37,14 @@ class SplashViewModel @Inject constructor( @Composable fun getDarkTheme() = runBlocking { appSettings.getTheme().first() }.themeIsDark() - fun onLoading() { + suspend fun onLoading() { + //Execute app usage only if is not first execution. In first execution the permission + //should be required before scheduling + if (!appInfo.getFirstTime().first()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + workAdministrator.scheduleUploadUsageInfoWorker() + } + } _state.value = SplashState.Redirect } diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 908176d0..b56a5c30 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -55,7 +55,7 @@ You will receive a notification every time a new quiz is available: at 9 am and 9 pm daily, and every two weeks at 3pm. For an optimal user experience, open the application regularly. - The app may use data such as your heart rate, but you have the full control of your personal data. + The app may use data such as your mobile usage or heart rate, but you have the full control of your personal data. "You can consult anonymised statistics about your community. Moreover, you can collaborate with the development of the application. \n This is a placeholder example. Your app should display an appropriate privacy policy for how\n you will handle and use Health Connect data. This sample app does not store or transmit any\n data outside of Health Connect. Co-author of the MFP Emotional Wellbeing System and main author of the application. @@ -221,9 +221,15 @@ Review Permissions Permission granted. You can disable it in the Android settings menu + Call listing permissions + Location permissions Notification permission + Access for device usage permission Health Connect permissions Hello! To ensure the correct functioning of the app it is necessary to grant the following permissions. The app will adapt to the permissions you grant. However, the information is anonymised so that no one (including the developers) can identify you. + The call listing permission is used to access the device\'s call log for the purpose of extracting anonymised statistics. Phone numbers are transformed to identifiers not related to that number. + The location permission is needed to be able to access data from mobile networks. Is not used to geolocate, only to determinate the usage profile. + The device usage access permission is used to find out which applications you have used and to extract related statistics on usage only. The contents of the apps are never used. This permission is required for the sending of notifications, such as those sent when a new questionnaire is available. If you have a wearable compatible with Health Connected, please check the permissions to access the wearable data. \n\nTo grant permission, in the list that appears when you click on the button, select the name of this app (Emotional Wellbeing) to grant it. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21a35d7e..5d49ab9f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -84,7 +84,7 @@ Tus datos, tu privacidad - La aplicación puede utilizar datos como tus pulsaciones cardíacas, pero en todo momento tienes el control de los datos. + La aplicación puede utilizar datos como tu uso del móvil o tus pulsaciones cardíacas, pero en todo momento tienes el control de los datos. El poder de la comunidad Es posible consultar estadísticas anonimizadas sobre tu comunidad. Además puedes colaborar con el desarrollo de la aplicación. @@ -279,9 +279,17 @@ Permisos Permiso concedido. Puede deshabilitarlo en el menú de configuración de Android + Permisos de listado de llamadas + Permisos de ubicación + Permisos de acceso al uso del dispositivo Permisos de notificaciones Permisos de Salud Conectada ¡Hola! Para que la app pueda realizar todas sus funciones es necesario otorgar los siguientes permisos. La aplicación se adaptará a los permisos que concedas. No obstante, la información está anonimizada para que nadie (incluidos los desarrolladores) pueda identificarle. + El permiso del listado de llamadas se utiliza para acceder al registro de llamadas del dispositivo, con la finalidad de extraer estadísticas anonimizadas. Los números de teléfono son transformados a identificadores no relacionados con dicho número. + El permiso de ubicación se necesita para poder acceder a los datos de las redes móviles. No lo usamos para geolocalizar, únicamente para determinar el perfil de uso. + El permiso de acceso al uso del dispositivo se utiliza para conocer qué aplicaciones has utilizado y extraer estadísticas relacionadas únicamente sobre el uso. Nunca se utilizan los contenidos de las mismas. + \n\nPara otorgar el permiso, en el listado que aparece al pulsar el botón, seleccione el nombre de esta app (Bienestar Emocional) para concederlo. + Este permiso se necesita para el envío de notificaciones, como las que se envían al abrirse un nuevo cuestionario. Si dispone de una pulsera compatible con Salud Conectada, por favor revise los permisos para acceder a los datos de la pulsera. Cuando esté listo, pulse el botón para ir a la ventana principal.