├── .gitignore ├── .idea ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── dictionaries │ └── Ashar.xml ├── discord.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── schemas │ └── com.se7en.screentrack.data.database.AppDatabase │ │ └── 1.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── se7en │ │ └── screentrack │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── se7en │ │ │ └── screentrack │ │ │ ├── Constants.kt │ │ │ ├── ScreenTrackApplication.kt │ │ │ ├── SettingsManager.kt │ │ │ ├── Utils.kt │ │ │ ├── adapters │ │ │ ├── AppsUsageAdapter.kt │ │ │ ├── SessionsAdapter.kt │ │ │ ├── TimelineAdapter.kt │ │ │ └── UsageFragmentAdapter.kt │ │ │ ├── data │ │ │ ├── AppUsageManager.kt │ │ │ └── database │ │ │ │ ├── AppDatabase.kt │ │ │ │ ├── Converters.kt │ │ │ │ └── StatsDao.kt │ │ │ ├── di │ │ │ └── ApplicationModule.kt │ │ │ ├── models │ │ │ ├── App.kt │ │ │ ├── AppUsage.kt │ │ │ ├── Day.kt │ │ │ ├── DayStats.kt │ │ │ ├── DayWithDayStats.kt │ │ │ ├── Session.kt │ │ │ ├── SessionMinimal.kt │ │ │ └── UsageData.kt │ │ │ ├── repository │ │ │ ├── AppDetailRepository.kt │ │ │ ├── HomeRepository.kt │ │ │ └── TimelineRepository.kt │ │ │ ├── ui │ │ │ ├── AppDetailFragment.kt │ │ │ ├── AppsListItemDecoration.kt │ │ │ ├── HomeFragment.kt │ │ │ ├── MainActivity.kt │ │ │ ├── PermissionFragment.kt │ │ │ ├── SettingsFragment.kt │ │ │ ├── TimelineFragment.kt │ │ │ └── UsageListFragment.kt │ │ │ └── viewmodels │ │ │ ├── AppDetailViewModel.kt │ │ │ ├── HomeViewModel.kt │ │ │ └── TimelineViewModel.kt │ └── res │ │ ├── drawable │ │ ├── ic_arrow_forward.xml │ │ ├── ic_home.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_settings.xml │ │ ├── ic_tail_arrow_forward.xml │ │ ├── ic_timeline.xml │ │ ├── tab_background.xml │ │ ├── tab_indicator.xml │ │ └── tab_text_color.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── fragment_app_detail.xml │ │ ├── fragment_home.xml │ │ ├── fragment_permission.xml │ │ ├── fragment_settings.xml │ │ ├── fragment_timeline.xml │ │ ├── fragment_usage_list.xml │ │ ├── session_rv_item.xml │ │ ├── timeline_session_rv_item.xml │ │ └── usage_rv_item.xml │ │ ├── menu │ │ └── bottom_nav_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── nav_graph.xml │ │ ├── values-night │ │ └── colors.xml │ │ └── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── se7en │ └── screentrack │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── keystore.jks ├── screenshots ├── app_detail_dark.png ├── app_detail_light.png ├── home_dark.png ├── home_light.png ├── timeline_dark.png └── timeline_light.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | ScreenTrack -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | xmlns:android 17 | 18 | ^$ 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | xmlns:.* 28 | 29 | ^$ 30 | 31 | 32 | BY_NAME 33 | 34 |
35 |
36 | 37 | 38 | 39 | .*:id 40 | 41 | http://schemas.android.com/apk/res/android 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | .*:name 51 | 52 | http://schemas.android.com/apk/res/android 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | name 62 | 63 | ^$ 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | style 73 | 74 | ^$ 75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | .* 84 | 85 | ^$ 86 | 87 | 88 | BY_NAME 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | http://schemas.android.com/apk/res/android 98 | 99 | 100 | ANDROID_ATTRIBUTE_ORDER 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | .* 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 |
117 |
118 | 119 | 121 |
122 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/dictionaries/Ashar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dataset 5 | screentrack 6 | upsert 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScreenTrack-Android 2 | Android App for monitoring app usage. Using MVVM architecture and Dagger Hilt. 3 | 4 | ### Download 5 | Get the latest APK from [releases](https://github.com/ashar-7/ScreenTrack-Android/releases) 6 | 7 | ### Screenshots 8 | 9 | 10 | 11 | 12 | 13 | 14 | ### Libraries used 15 | * [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) 16 | * [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) 17 | * [Navigation](https://developer.android.com/guide/navigation) 18 | * [Room](https://developer.android.com/training/data-storage/room) 19 | * [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) 20 | * [Material Design](https://material.io/) 21 | * [MPAndroidChart](https://github.com/PhilJay/MPAndroidChart) 22 | * [Material Spinner](https://github.com/jaredrummler/MaterialSpinner) 23 | * [ThreeTenABP](https://github.com/JakeWharton/ThreeTenABP) 24 | 25 | #### Data 26 | The data is fetched from [UsageStatsManager](https://developer.android.com/reference/android/app/usage/UsageStatsManager)'s [queryEvents](https://developer.android.com/reference/android/app/usage/UsageStatsManager#queryEvents(long,%20long)) function. 27 | 28 | #### Permissions 29 | To use [UsageStatsManager](https://developer.android.com/reference/android/app/usage/UsageStatsManager), the [Manifest.permission.PACKAGE_USAGE_STATS](https://developer.android.com/reference/android/Manifest.permission#PACKAGE_USAGE_STATS) permission is required. 30 | 31 | #### Accuracy 32 | The data should probably be accurate on most devices but I did experience a few abnormalities in the 33 | usage events of some devices. For instance, some [ACTIVITY_PAUSE](https://developer.android.com/reference/kotlin/android/app/usage/UsageEvents.Event#ACTIVITY_PAUSED:kotlin.Int) events without their corresponding [ACTIVITY_RESUME](https://developer.android.com/reference/kotlin/android/app/usage/UsageEvents.Event#ACTIVITY_RESUMED:kotlin.Int) events (which the app ignores). 34 | 35 | #### License 36 | MIT License 37 | 38 | Copyright (c) 2020 Ashar Khan 39 | 40 | Permission is hereby granted, free of charge, to any person obtaining a copy 41 | of this software and associated documentation files (the "Software"), to deal 42 | in the Software without restriction, including without limitation the rights 43 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 44 | copies of the Software, and to permit persons to whom the Software is 45 | furnished to do so, subject to the following conditions: 46 | 47 | The above copyright notice and this permission notice shall be included in all 48 | copies or substantial portions of the Software. 49 | 50 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 51 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 52 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 53 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 54 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 55 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 56 | SOFTWARE. 57 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | apply plugin: "androidx.navigation.safeargs.kotlin" 6 | apply plugin: 'dagger.hilt.android.plugin' 7 | 8 | android { 9 | compileSdkVersion 31 10 | 11 | defaultConfig { 12 | applicationId "com.se7en.screentrack" 13 | minSdkVersion 22 14 | targetSdkVersion 31 15 | versionCode 1 16 | versionName "1.0" 17 | 18 | javaCompileOptions { 19 | annotationProcessorOptions { 20 | arguments += ["room.schemaLocation":"$projectDir/schemas".toString()] 21 | } 22 | } 23 | 24 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 25 | } 26 | 27 | buildTypes { 28 | release { 29 | minifyEnabled false 30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 31 | } 32 | } 33 | 34 | kotlinOptions { 35 | jvmTarget = "1.8" 36 | } 37 | } 38 | 39 | dependencies { 40 | implementation fileTree(dir: "libs", include: ["*.jar"]) 41 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 42 | implementation 'androidx.core:core-ktx:1.7.0' 43 | implementation 'androidx.appcompat:appcompat:1.4.1' 44 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3' 45 | testImplementation 'junit:junit:4.13.2' 46 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 47 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 48 | 49 | def lifecycle_version = "2.4.1" 50 | def nav_version = "2.4.1" 51 | def room_version = "2.4.2" 52 | 53 | // Lifecycle - ViewModel, LiveData 54 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" 55 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" 56 | 57 | // Navigation Component 58 | implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" 59 | implementation "androidx.navigation:navigation-ui-ktx:$nav_version" 60 | 61 | // Hilt 62 | implementation "com.google.dagger:hilt-android:2.31-alpha" 63 | kapt "com.google.dagger:hilt-android-compiler:2.31-alpha" 64 | // Hilt ViewModel Integration 65 | implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03' 66 | kapt 'androidx.hilt:hilt-compiler:1.0.0' 67 | 68 | // Room 69 | implementation "androidx.room:room-runtime:$room_version" 70 | implementation "androidx.room:room-ktx:$room_version" 71 | kapt "androidx.room:room-compiler:$room_version" 72 | 73 | // Material Components 74 | implementation 'com.google.android.material:material:1.6.0-alpha03' 75 | 76 | // ThreeTenABP 77 | implementation 'com.jakewharton.threetenabp:threetenabp:1.2.4' 78 | 79 | // MPAndroidChart 80 | implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' 81 | 82 | // Material Spinner 83 | implementation 'com.jaredrummler:material-spinner:1.3.1' 84 | } 85 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/schemas/com.se7en.screentrack.data.database.AppDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "14bf3ffa185b2b65b3abbe5c5b1df23c", 6 | "entities": [ 7 | { 8 | "tableName": "Day", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`date`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "date", 13 | "columnName": "date", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "lastUpdated", 19 | "columnName": "lastUpdated", 20 | "affinity": "INTEGER", 21 | "notNull": true 22 | } 23 | ], 24 | "primaryKey": { 25 | "columnNames": [ 26 | "date" 27 | ], 28 | "autoGenerate": false 29 | }, 30 | "indices": [], 31 | "foreignKeys": [] 32 | }, 33 | { 34 | "tableName": "DayStats", 35 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `totalTime` INTEGER NOT NULL, `lastUsed` INTEGER NOT NULL, `dayId` INTEGER NOT NULL, PRIMARY KEY(`packageName`, `dayId`), FOREIGN KEY(`dayId`) REFERENCES `Day`(`date`) ON UPDATE NO ACTION ON DELETE CASCADE )", 36 | "fields": [ 37 | { 38 | "fieldPath": "packageName", 39 | "columnName": "packageName", 40 | "affinity": "TEXT", 41 | "notNull": true 42 | }, 43 | { 44 | "fieldPath": "totalTime", 45 | "columnName": "totalTime", 46 | "affinity": "INTEGER", 47 | "notNull": true 48 | }, 49 | { 50 | "fieldPath": "lastUsed", 51 | "columnName": "lastUsed", 52 | "affinity": "INTEGER", 53 | "notNull": true 54 | }, 55 | { 56 | "fieldPath": "dayId", 57 | "columnName": "dayId", 58 | "affinity": "INTEGER", 59 | "notNull": true 60 | } 61 | ], 62 | "primaryKey": { 63 | "columnNames": [ 64 | "packageName", 65 | "dayId" 66 | ], 67 | "autoGenerate": false 68 | }, 69 | "indices": [ 70 | { 71 | "name": "index_DayStats_dayId", 72 | "unique": false, 73 | "columnNames": [ 74 | "dayId" 75 | ], 76 | "createSql": "CREATE INDEX IF NOT EXISTS `index_DayStats_dayId` ON `${TABLE_NAME}` (`dayId`)" 77 | } 78 | ], 79 | "foreignKeys": [ 80 | { 81 | "table": "Day", 82 | "onDelete": "CASCADE", 83 | "onUpdate": "NO ACTION", 84 | "columns": [ 85 | "dayId" 86 | ], 87 | "referencedColumns": [ 88 | "date" 89 | ] 90 | } 91 | ] 92 | } 93 | ], 94 | "views": [], 95 | "setupQueries": [ 96 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 97 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '14bf3ffa185b2b65b3abbe5c5b1df23c')" 98 | ] 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/se7en/screentrack/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.se7en.screentrack", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 11 | 14 | 17 | 20 | 21 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack 2 | 3 | object Constants { 4 | const val FILTER_KEY = "filter" 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/ScreenTrackApplication.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.jakewharton.threetenabp.AndroidThreeTen 6 | import dagger.hilt.android.HiltAndroidApp 7 | 8 | @HiltAndroidApp 9 | class ScreenTrackApplication: Application() { 10 | override fun onCreate() { 11 | super.onCreate() 12 | 13 | SettingsManager.initTheme(getSharedPreferences(getString(R.string.app_shared_prefs_name), Context.MODE_PRIVATE)) 14 | AndroidThreeTen.init(this) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/SettingsManager.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack 2 | 3 | import android.content.SharedPreferences 4 | import androidx.appcompat.app.AppCompatDelegate 5 | import androidx.core.content.edit 6 | 7 | object SettingsManager { 8 | 9 | private const val THEME_PREFS_KEY = "theme" 10 | 11 | enum class Theme { 12 | LIGHT { 13 | override fun toString() = "Light" 14 | }, 15 | DARK { 16 | override fun toString() = "Dark" 17 | }, 18 | FOLLOW_SYSTEM { 19 | override fun toString() = "Follow system" 20 | } 21 | } 22 | 23 | val themes = listOf(Theme.LIGHT.toString(), Theme.DARK.toString(), Theme.FOLLOW_SYSTEM.toString()) 24 | 25 | fun initTheme(sharedPrefs: SharedPreferences) { 26 | when(sharedPrefs.getInt(THEME_PREFS_KEY, Theme.FOLLOW_SYSTEM.ordinal)) { 27 | Theme.LIGHT.ordinal -> setTheme(Theme.LIGHT, sharedPrefs) 28 | Theme.DARK.ordinal -> setTheme(Theme.DARK, sharedPrefs) 29 | Theme.FOLLOW_SYSTEM.ordinal -> setTheme(Theme.FOLLOW_SYSTEM, sharedPrefs) 30 | 31 | else -> setTheme(Theme.FOLLOW_SYSTEM, sharedPrefs) 32 | } 33 | } 34 | 35 | fun setTheme(theme: Theme, sharedPrefs: SharedPreferences?) { 36 | when(theme) { 37 | Theme.LIGHT -> { 38 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) 39 | } 40 | 41 | Theme.DARK -> { 42 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) 43 | } 44 | 45 | Theme.FOLLOW_SYSTEM -> { 46 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) 47 | } 48 | } 49 | 50 | sharedPrefs?.edit { 51 | putInt(THEME_PREFS_KEY, theme.ordinal) 52 | apply() 53 | } 54 | } 55 | 56 | fun setTheme(string: String, sharedPrefs: SharedPreferences?) { 57 | when(string) { 58 | Theme.LIGHT.toString() -> setTheme(Theme.LIGHT, sharedPrefs) 59 | Theme.DARK.toString() -> setTheme(Theme.DARK, sharedPrefs) 60 | Theme.FOLLOW_SYSTEM.toString() -> setTheme(Theme.FOLLOW_SYSTEM, sharedPrefs) 61 | } 62 | } 63 | 64 | fun getCurrentTheme(sharedPrefs: SharedPreferences?): Theme { 65 | return when(sharedPrefs?.getInt(THEME_PREFS_KEY, Theme.FOLLOW_SYSTEM.ordinal)) { 66 | Theme.LIGHT.ordinal -> Theme.LIGHT 67 | Theme.DARK.ordinal -> Theme.DARK 68 | Theme.FOLLOW_SYSTEM.ordinal -> Theme.FOLLOW_SYSTEM 69 | 70 | else -> Theme.FOLLOW_SYSTEM 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack 2 | 3 | import org.threeten.bp.Duration 4 | import org.threeten.bp.Instant 5 | import org.threeten.bp.ZoneId 6 | import org.threeten.bp.ZonedDateTime 7 | import org.threeten.bp.format.DateTimeFormatter 8 | import org.threeten.bp.temporal.ChronoUnit 9 | 10 | object Utils { 11 | 12 | fun getZonedDateTime(millis: Long): ZonedDateTime = 13 | Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()) 14 | 15 | 16 | fun getZonedDateTime(millis: Long, truncatedTo: ChronoUnit): ZonedDateTime = 17 | Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).truncatedTo(truncatedTo) 18 | 19 | fun getStartOfDayMillis(date: ZonedDateTime) = 20 | date.toLocalDate().atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli() 21 | 22 | fun getUsageTimeString(millis: Long): String { 23 | var timeLeft = Duration.ofMillis(millis) 24 | val hours = timeLeft.toHours() 25 | 26 | timeLeft = timeLeft.minusHours(hours) 27 | val minutes = timeLeft.toMinutes() 28 | 29 | timeLeft = timeLeft.minusMinutes(minutes) 30 | val seconds = timeLeft.seconds 31 | 32 | return when { 33 | hours >= 1 -> { 34 | String.format("%dh %dm %ds", hours, minutes, seconds) 35 | } 36 | minutes >= 1 -> { 37 | String.format("%dm %ds", minutes, seconds) 38 | } 39 | else -> { 40 | // assuming all apps are used for at least >= 1 second 41 | String.format("%ds", seconds) 42 | } 43 | } 44 | } 45 | 46 | fun getLastUsedFormattedDate(millis: Long): String { 47 | val dateTime = getZonedDateTime(millis) 48 | return dateTime.format(DateTimeFormatter.ofPattern("EEE, dd MMM HH:mm:ss")).toString() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/adapters/AppsUsageAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.recyclerview.widget.ListAdapter 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.se7en.screentrack.R 10 | import com.se7en.screentrack.Utils 11 | import com.se7en.screentrack.models.App 12 | import com.se7en.screentrack.models.AppUsage 13 | import kotlinx.android.synthetic.main.usage_rv_item.view.* 14 | 15 | class AppsUsageAdapter( 16 | val listChangedListener: () -> Unit, 17 | val onClick: (app: App) -> Unit 18 | ): 19 | ListAdapter(UsageDiffUtil()) { 20 | 21 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 22 | val view = LayoutInflater.from(parent.context) 23 | .inflate(R.layout.usage_rv_item, parent, false) 24 | 25 | return ViewHolder(view) 26 | } 27 | 28 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 29 | holder.bind(getItem(position)) 30 | } 31 | 32 | override fun onCurrentListChanged( 33 | previousList: MutableList, 34 | currentList: MutableList 35 | ) { 36 | super.onCurrentListChanged(previousList, currentList) 37 | listChangedListener() 38 | } 39 | 40 | inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { 41 | fun bind(item: AppUsage) { 42 | itemView.setOnClickListener { onClick(item.app) } 43 | 44 | itemView.totalUsageTime.text = Utils.getUsageTimeString(item.totalTime) 45 | itemView.appIcon.setImageDrawable(item.app.iconDrawable) 46 | itemView.appName.text = item.app.appName 47 | } 48 | } 49 | 50 | class UsageDiffUtil: DiffUtil.ItemCallback() { 51 | override fun areItemsTheSame(oldItem: AppUsage, newItem: AppUsage): Boolean { 52 | return oldItem.app.packageName == newItem.app.packageName 53 | } 54 | 55 | override fun areContentsTheSame(oldItem: AppUsage, newItem: AppUsage): Boolean { 56 | return (oldItem.app.packageName == newItem.app.packageName 57 | && oldItem.totalTime == newItem.totalTime) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/adapters/SessionsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.recyclerview.widget.ListAdapter 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.se7en.screentrack.R 10 | import com.se7en.screentrack.Utils 11 | import com.se7en.screentrack.models.SessionMinimal 12 | import kotlinx.android.synthetic.main.session_rv_item.view.* 13 | import org.threeten.bp.format.DateTimeFormatter 14 | 15 | class SessionsAdapter( 16 | val listChangedListener: (isEmpty: Boolean) -> Unit 17 | ): ListAdapter(SessionDiffUtil()) { 18 | 19 | inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { 20 | fun bind(item: SessionMinimal) { 21 | with(itemView) { 22 | val timeFormat = DateTimeFormatter.ofPattern("HH:mm:ss") 23 | val dateFormat = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy") 24 | 25 | val start = Utils.getZonedDateTime(item.startMillis) 26 | sessionStartTime.text = start.format(timeFormat) 27 | sessionStartDate.text = start.format(dateFormat) 28 | 29 | val end = Utils.getZonedDateTime(item.endMillis) 30 | sessionEndTime.text = end.format(timeFormat) 31 | sessionEndDate.text = end.format(dateFormat) 32 | } 33 | } 34 | } 35 | 36 | override fun onCurrentListChanged( 37 | previousList: MutableList, 38 | currentList: MutableList 39 | ) { 40 | super.onCurrentListChanged(previousList, currentList) 41 | listChangedListener(currentList.isEmpty()) 42 | } 43 | 44 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 45 | return ViewHolder( 46 | LayoutInflater.from(parent.context).inflate(R.layout.session_rv_item, parent, false) 47 | ) 48 | } 49 | 50 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 51 | holder.bind(getItem(position)) 52 | } 53 | 54 | class SessionDiffUtil: DiffUtil.ItemCallback() { 55 | override fun areItemsTheSame(oldItem: SessionMinimal, newItem: SessionMinimal): Boolean { 56 | return oldItem.packageName == newItem.packageName 57 | } 58 | 59 | override fun areContentsTheSame(oldItem: SessionMinimal, newItem: SessionMinimal): Boolean { 60 | return (oldItem.startMillis == newItem.startMillis 61 | && oldItem.endMillis == newItem.endMillis) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/adapters/TimelineAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.recyclerview.widget.ListAdapter 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.se7en.screentrack.R 10 | import com.se7en.screentrack.Utils 11 | import com.se7en.screentrack.models.App 12 | import com.se7en.screentrack.models.Session 13 | import kotlinx.android.synthetic.main.timeline_session_rv_item.view.* 14 | import org.threeten.bp.format.DateTimeFormatter 15 | 16 | class TimelineAdapter( 17 | val onClick: (app: App) -> Unit 18 | ): ListAdapter(SessionDiffUtil()) { 19 | 20 | inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { 21 | fun bind(item: Session) { 22 | with(itemView) { 23 | setOnClickListener { onClick(item.app) } 24 | 25 | val timeFormat = DateTimeFormatter.ofPattern("HH:mm:ss") 26 | 27 | val start = Utils.getZonedDateTime(item.startMillis) 28 | sessionStartTime.text = start.format(timeFormat) 29 | 30 | val end = Utils.getZonedDateTime(item.endMillis) 31 | sessionEndTime.text = end.format(timeFormat) 32 | 33 | appIcon.setImageDrawable(item.app.iconDrawable) 34 | appName.text = item.app.appName 35 | } 36 | } 37 | } 38 | 39 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 40 | return ViewHolder( 41 | LayoutInflater.from(parent.context).inflate(R.layout.timeline_session_rv_item, parent, false) 42 | ) 43 | } 44 | 45 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 46 | holder.bind(getItem(position)) 47 | } 48 | 49 | class SessionDiffUtil: DiffUtil.ItemCallback() { 50 | override fun areItemsTheSame(oldItem: Session, newItem: Session): Boolean { 51 | return (oldItem.startMillis == newItem.startMillis 52 | && oldItem.endMillis == newItem.endMillis 53 | && oldItem.app.packageName == newItem.app.packageName) 54 | } 55 | 56 | override fun areContentsTheSame(oldItem: Session, newItem: Session): Boolean { 57 | return (oldItem.startMillis == newItem.startMillis 58 | && oldItem.endMillis == newItem.endMillis 59 | && oldItem.app.packageName == newItem.app.packageName) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/adapters/UsageFragmentAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.adapters 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.Fragment 5 | import androidx.viewpager2.adapter.FragmentStateAdapter 6 | import com.se7en.screentrack.Constants 7 | import com.se7en.screentrack.data.AppUsageManager 8 | import com.se7en.screentrack.ui.UsageListFragment 9 | 10 | class UsageFragmentAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { 11 | 12 | override fun getItemCount(): Int = AppUsageManager.FILTER.values().size 13 | 14 | override fun createFragment(position: Int): Fragment { 15 | // Return a NEW fragment instance in createFragment(int) 16 | val fragment = UsageListFragment() 17 | fragment.arguments = Bundle().apply { 18 | putInt(Constants.FILTER_KEY, position) 19 | } 20 | return fragment 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/data/AppUsageManager.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.data 2 | 3 | import android.app.usage.UsageEvents 4 | import android.app.usage.UsageStatsManager 5 | import android.content.Context 6 | import com.se7en.screentrack.models.* 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.withContext 10 | import org.threeten.bp.ZoneId 11 | import org.threeten.bp.ZonedDateTime 12 | import org.threeten.bp.temporal.ChronoUnit 13 | import javax.inject.Inject 14 | import kotlin.math.max 15 | 16 | class AppUsageManager @Inject constructor( 17 | @ApplicationContext private val context: Context 18 | ) { 19 | private val usageStatsManager = context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager? 20 | 21 | enum class FILTER { TODAY, THIS_WEEK } 22 | 23 | suspend fun getUsageList(dayWithStats: DayWithDayStats): List { 24 | return withContext(Dispatchers.Default) { 25 | val usageList = arrayListOf() 26 | 27 | dayWithStats.dayStats.forEach { 28 | val app = App.fromContext(context, it.packageName) 29 | usageList.add( 30 | AppUsage( 31 | app, 32 | it.totalTime, 33 | it.lastUsed 34 | ) 35 | ) 36 | } 37 | 38 | return@withContext usageList.sortedByDescending { it.totalTime } 39 | } 40 | } 41 | 42 | suspend fun getUsageList(daysWithStats: List): List { 43 | return withContext(Dispatchers.Default) { 44 | val statsMap = mutableMapOf() 45 | 46 | daysWithStats.forEach { 47 | for (stats in it.dayStats) { 48 | (statsMap[stats.packageName] ?: AppUsage( 49 | App.fromContext(context, stats.packageName), 0L, 0L 50 | )).let { usage -> 51 | usage.totalTime += stats.totalTime 52 | usage.lastUsed = max(stats.lastUsed, usage.lastUsed) 53 | statsMap[stats.packageName] = usage 54 | } 55 | } 56 | } 57 | 58 | return@withContext statsMap.values.sortedByDescending { it.totalTime } 59 | } 60 | } 61 | 62 | suspend fun getDayWithStatsForWeek(): List { 63 | return withContext(Dispatchers.IO) { 64 | val now = ZonedDateTime.now(ZoneId.systemDefault()) 65 | val nowLocalDate = now.toLocalDate() 66 | val appsWithDayStats = arrayListOf() 67 | 68 | for (i in 0..6) { 69 | val date = 70 | nowLocalDate.minusDays(i.toLong()).atStartOfDay(ZoneId.systemDefault()) 71 | 72 | appsWithDayStats.add(getDayWithStats(date)) 73 | } 74 | 75 | return@withContext appsWithDayStats 76 | } 77 | } 78 | 79 | private fun getDayWithStats( 80 | date: ZonedDateTime = ZonedDateTime.now(ZoneId.systemDefault()) 81 | ): DayWithDayStats { 82 | val statsList = arrayListOf() 83 | 84 | val utc = ZoneId.of("UTC") 85 | val defaultZone = ZoneId.systemDefault() 86 | val startDate = date.toLocalDate().atStartOfDay(defaultZone).withZoneSameInstant(utc) 87 | val timeStartMillis = startDate.toInstant().toEpochMilli() 88 | val todayDate = ZonedDateTime.now(ZoneId.systemDefault()).truncatedTo(ChronoUnit.DAYS) 89 | val timeEndMillis = if (date.truncatedTo(ChronoUnit.DAYS).isEqual(todayDate)) { 90 | System.currentTimeMillis() 91 | } else startDate.plusDays(1).minusSeconds(1).toInstant().toEpochMilli() 92 | 93 | if (usageStatsManager != null) { 94 | val eventsMap = mutableMapOf>() 95 | val events = usageStatsManager.queryEvents(timeStartMillis, timeEndMillis) 96 | while (events.hasNextEvent()) { 97 | val event = UsageEvents.Event() 98 | events.getNextEvent(event) 99 | val packageName = event.packageName 100 | 101 | (eventsMap[packageName] ?: mutableListOf()).let { 102 | it.add(event) 103 | eventsMap[packageName] = it 104 | } 105 | } 106 | 107 | eventsMap.forEach { (packageName, events) -> 108 | val pm = context.packageManager 109 | 110 | if (pm.getLaunchIntentForPackage(packageName) != null) { 111 | var startTime = 0L 112 | var endTime = 0L 113 | var totalTime = 0L 114 | var lastUsed = 0L 115 | var isInitialized = false 116 | events.forEach { event -> 117 | when (event.eventType) { 118 | UsageEvents.Event.ACTIVITY_RESUMED -> { // same as MOVE_TO_FOREGROUND 119 | // start time 120 | isInitialized = true 121 | startTime = event.timeStamp 122 | endTime = 0L 123 | } 124 | 125 | UsageEvents.Event.ACTIVITY_PAUSED -> { // same as MOVE_TO_BACKGROUND 126 | // end time 127 | if (startTime == 0L) { 128 | if(!isInitialized) { 129 | startTime = timeStartMillis 130 | endTime = event.timeStamp 131 | lastUsed = endTime 132 | } 133 | } else { 134 | endTime = event.timeStamp 135 | lastUsed = endTime 136 | } 137 | } 138 | } 139 | 140 | // If both start and end times exist, add the time to totalTime 141 | // and reset start and end times 142 | if (startTime != 0L && endTime != 0L) { 143 | totalTime += endTime - startTime 144 | startTime = 0L; endTime = 0L 145 | } 146 | } 147 | 148 | // If the end time was not found, it's likely that the app is still running 149 | // so assume the end time to be now 150 | if (startTime != 0L && endTime == 0L) { 151 | lastUsed = timeEndMillis 152 | totalTime += lastUsed - startTime 153 | } 154 | 155 | // If total time is more than 1 second 156 | if (totalTime >= 1000) { 157 | val stats = DayStats( 158 | packageName, 159 | totalTime, 160 | lastUsed, 161 | date 162 | ) 163 | statsList.add(stats) 164 | } 165 | } 166 | } 167 | } 168 | 169 | return DayWithDayStats( 170 | Day( 171 | date, System.currentTimeMillis() 172 | ), 173 | statsList 174 | ) 175 | } 176 | 177 | suspend fun getSessions(packageName: String, date: ZonedDateTime): List { 178 | return withContext(Dispatchers.Default) { 179 | val sessions = arrayListOf() 180 | val utc = ZoneId.of("UTC") 181 | val defaultZone = ZoneId.systemDefault() 182 | val startDate = date.toLocalDate().atStartOfDay(defaultZone).withZoneSameInstant(utc) 183 | val timeStart = startDate.toInstant().toEpochMilli() 184 | val todayDate = ZonedDateTime.now(ZoneId.systemDefault()).truncatedTo(ChronoUnit.DAYS) 185 | val timeEnd = if (date.truncatedTo(ChronoUnit.DAYS).isEqual(todayDate)) { 186 | System.currentTimeMillis() 187 | } else startDate.plusDays(1).minusSeconds(1).toInstant().toEpochMilli() 188 | 189 | if(usageStatsManager != null) { 190 | val events = usageStatsManager.queryEvents(timeStart, timeEnd) 191 | var startTime = 0L 192 | var endTime = 0L 193 | var isInitialized = false 194 | while(events.hasNextEvent()) { 195 | val event = UsageEvents.Event() 196 | events.getNextEvent(event) 197 | if(event.packageName == packageName) { 198 | when (event.eventType) { 199 | UsageEvents.Event.ACTIVITY_RESUMED -> { // same as MOVE_TO_FOREGROUND 200 | isInitialized = true 201 | startTime = event.timeStamp 202 | endTime = 0L 203 | } 204 | 205 | UsageEvents.Event.ACTIVITY_PAUSED -> { // same as MOVE_TO_BACKGROUND 206 | // end time 207 | if(startTime == 0L) { 208 | if(!isInitialized) { 209 | startTime = timeStart 210 | endTime = event.timeStamp 211 | } 212 | } else { 213 | endTime = event.timeStamp 214 | } 215 | } 216 | } 217 | 218 | if (startTime != 0L && endTime != 0L) { 219 | // we have a session 220 | val session = SessionMinimal( 221 | startTime, 222 | endTime, 223 | packageName 224 | ) 225 | 226 | sessions.add(session) 227 | 228 | startTime = 0L; endTime = 0L 229 | } 230 | } 231 | } 232 | 233 | // If the end time was not found, it's likely that the app is still running 234 | // so assume the end time to be now 235 | if (startTime != 0L && endTime == 0L) { 236 | endTime = timeEnd 237 | if (endTime > System.currentTimeMillis()) 238 | endTime = System.currentTimeMillis() 239 | } 240 | 241 | if (startTime != 0L && endTime != 0L) { 242 | // we have a session 243 | val session = SessionMinimal( 244 | startTime, 245 | endTime, 246 | packageName 247 | ) 248 | 249 | sessions.add(session) 250 | } 251 | } 252 | 253 | return@withContext sessions.reversed() 254 | } 255 | } 256 | 257 | suspend fun getSessions( 258 | date: ZonedDateTime 259 | ): List { 260 | return withContext(Dispatchers.Default) { 261 | val sessions = arrayListOf() 262 | val utc = ZoneId.of("UTC") 263 | val defaultZone = ZoneId.systemDefault() 264 | val startDate = date.toLocalDate().atStartOfDay(defaultZone).withZoneSameInstant(utc) 265 | val timeStart = startDate.toInstant().toEpochMilli() 266 | val todayDate = ZonedDateTime.now(ZoneId.systemDefault()).truncatedTo(ChronoUnit.DAYS) 267 | val timeEnd = if (date.truncatedTo(ChronoUnit.DAYS).isEqual(todayDate)) { 268 | System.currentTimeMillis() 269 | } else startDate.plusDays(1).minusSeconds(1).toInstant().toEpochMilli() 270 | 271 | if (usageStatsManager != null) { 272 | val eventsMap = mutableMapOf>() 273 | val events = usageStatsManager.queryEvents(timeStart, timeEnd) 274 | while (events.hasNextEvent()) { 275 | val event = UsageEvents.Event() 276 | events.getNextEvent(event) 277 | val packageName = event.packageName 278 | 279 | (eventsMap[packageName] ?: mutableListOf()).let { 280 | it.add(event) 281 | eventsMap[packageName] = it 282 | } 283 | } 284 | 285 | eventsMap.forEach { (packageName, events) -> 286 | val pm = context.packageManager 287 | 288 | if (pm.getLaunchIntentForPackage(packageName) != null) { 289 | var startTime = 0L 290 | var endTime = 0L 291 | var isInitialized = false 292 | events.forEach { event -> 293 | when (event.eventType) { 294 | UsageEvents.Event.ACTIVITY_RESUMED -> { // same as MOVE_TO_FOREGROUND 295 | isInitialized = true 296 | startTime = event.timeStamp 297 | endTime = 0L 298 | } 299 | 300 | UsageEvents.Event.ACTIVITY_PAUSED -> { // same as MOVE_TO_BACKGROUND 301 | // end time 302 | if(startTime == 0L) { 303 | if(!isInitialized) { 304 | startTime = timeStart 305 | endTime = event.timeStamp 306 | } 307 | } else { 308 | endTime = event.timeStamp 309 | } 310 | } 311 | } 312 | 313 | if (startTime != 0L && endTime != 0L) { 314 | // we have a session 315 | val session = Session( 316 | startTime, 317 | endTime, 318 | App.fromContext(context, packageName) 319 | ) 320 | 321 | sessions.add(session) 322 | 323 | startTime = 0L; endTime = 0L 324 | } 325 | } 326 | 327 | // If the end time was not found, it's likely that the app is still running 328 | // so assume the end time to be now 329 | if (startTime != 0L && endTime == 0L) { 330 | endTime = timeEnd 331 | } 332 | 333 | if (startTime != 0L && endTime != 0L) { 334 | // we have a session 335 | val session = Session( 336 | startTime, 337 | endTime, 338 | App.fromContext(context, packageName) 339 | ) 340 | 341 | sessions.add(session) 342 | } 343 | } 344 | } 345 | } 346 | 347 | return@withContext sessions.sortedByDescending { 348 | it.endMillis 349 | } 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/data/database/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.data.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | import com.se7en.screentrack.models.DayStats 7 | import com.se7en.screentrack.models.Day 8 | 9 | @Database(entities = [Day::class, DayStats::class], version = 1) 10 | @TypeConverters(Converters::class) 11 | abstract class AppDatabase: RoomDatabase() { 12 | abstract fun statsDao(): StatsDao 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/data/database/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.data.database 2 | 3 | import androidx.room.TypeConverter 4 | import com.se7en.screentrack.Utils 5 | import org.threeten.bp.ZonedDateTime 6 | import org.threeten.bp.temporal.ChronoUnit 7 | 8 | 9 | class Converters { 10 | 11 | @TypeConverter 12 | fun fromDate(date: ZonedDateTime): Long { 13 | return date.truncatedTo(ChronoUnit.DAYS).toInstant().toEpochMilli() 14 | } 15 | 16 | @TypeConverter 17 | fun longToDate(millis: Long): ZonedDateTime { 18 | return Utils.getZonedDateTime(millis, ChronoUnit.DAYS) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/data/database/StatsDao.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.data.database 2 | 3 | import androidx.room.* 4 | import com.se7en.screentrack.models.Day 5 | import com.se7en.screentrack.models.DayStats 6 | import com.se7en.screentrack.models.DayWithDayStats 7 | import kotlinx.coroutines.flow.Flow 8 | import org.threeten.bp.ZonedDateTime 9 | 10 | @Dao 11 | abstract class StatsDao { 12 | 13 | @Insert(onConflict = OnConflictStrategy.IGNORE) 14 | abstract suspend fun insert(day: Day): Long 15 | 16 | @Insert(onConflict = OnConflictStrategy.IGNORE) 17 | abstract suspend fun insert(dayStats: DayStats): Long 18 | 19 | @Update(onConflict = OnConflictStrategy.IGNORE) 20 | abstract suspend fun update(day: Day) 21 | 22 | @Update(onConflict = OnConflictStrategy.IGNORE) 23 | abstract suspend fun update(dayStats: DayStats) 24 | 25 | @Transaction 26 | open suspend fun upsert(dayWithDayStats: List) { 27 | dayWithDayStats.forEach { 28 | if(insert(it.day) == (-1).toLong()) { 29 | update(it.day) 30 | } 31 | 32 | it.dayStats.forEach { stats -> 33 | if(insert(stats) == (-1).toLong()) { 34 | update(stats) 35 | } 36 | } 37 | } 38 | } 39 | 40 | @Delete 41 | abstract suspend fun delete(day: Day) 42 | 43 | @Delete 44 | abstract suspend fun delete(vararg dayStats: DayStats) 45 | 46 | @Transaction 47 | @Delete 48 | open suspend fun delete(daysWithDayStats: List) { 49 | daysWithDayStats.forEach { 50 | delete(it.day) 51 | } 52 | } 53 | 54 | @Transaction 55 | @Query("SELECT * FROM Day") 56 | abstract fun getDaysWithDayStats(): Flow?> 57 | 58 | @Transaction 59 | @Query("SELECT * FROM Day WHERE date IS NOT :exceptDate") 60 | abstract suspend fun getDaysWithDayStats(exceptDate: ZonedDateTime): List 61 | 62 | @Transaction 63 | @Query("SELECT * FROM Day WHERE date IS :date") 64 | abstract fun getDayWithDayStats(date: ZonedDateTime): Flow 65 | 66 | @Query("SELECT * FROM DayStats WHERE packageName IS :packageName") 67 | abstract fun getDayStats(packageName: String): Flow> 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/di/ApplicationModule.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.se7en.screentrack.data.database.AppDatabase 6 | import com.se7en.screentrack.data.database.StatsDao 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.android.qualifiers.ApplicationContext 11 | import dagger.hilt.components.SingletonComponent 12 | import javax.inject.Singleton 13 | 14 | @InstallIn(SingletonComponent::class) 15 | @Module 16 | object ApplicationModule { 17 | 18 | @Singleton 19 | @Provides 20 | fun provideAppDatabase( 21 | @ApplicationContext appContext: Context 22 | ): AppDatabase { 23 | return Room.databaseBuilder( 24 | appContext, 25 | AppDatabase::class.java, 26 | "app-database" 27 | ).build() 28 | } 29 | 30 | @Provides 31 | fun provideStatsDao( 32 | database: AppDatabase 33 | ): StatsDao { 34 | return database.statsDao() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/models/App.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.models 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import android.graphics.drawable.Drawable 6 | import android.util.Log 7 | 8 | data class App( 9 | val packageName: String, 10 | val appName: String, 11 | var iconDrawable: Drawable? 12 | ) { 13 | fun setDrawableIfNull(context: Context): App { 14 | if(iconDrawable == null) { 15 | try { 16 | val pm = context.packageManager 17 | iconDrawable = pm.getApplicationIcon(packageName) 18 | } catch (e: Exception) { 19 | Log.d("App", "Failed to get app info for $packageName") 20 | } 21 | } 22 | 23 | return this 24 | } 25 | 26 | companion object { 27 | fun fromContext( 28 | context: Context, 29 | packageName: String 30 | ): App { 31 | var appIcon: Drawable? = null 32 | var appName: String 33 | try { 34 | val pm = context.packageManager 35 | appIcon = pm.getApplicationIcon(packageName) 36 | val info = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA) 37 | appName = pm.getApplicationLabel(info).toString() 38 | } catch (e: Exception) { 39 | appName = "$packageName (uninstalled)" 40 | } 41 | 42 | return App( 43 | packageName, 44 | appName, 45 | appIcon 46 | ) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/models/AppUsage.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.models 2 | 3 | data class AppUsage( 4 | val app: App, 5 | var totalTime: Long, 6 | var lastUsed: Long 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/models/Day.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.models 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import org.threeten.bp.ZonedDateTime 6 | 7 | @Entity 8 | data class Day( 9 | @PrimaryKey val date: ZonedDateTime, 10 | val lastUpdated: Long 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/models/DayStats.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.models 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.ForeignKey 6 | import org.threeten.bp.ZonedDateTime 7 | 8 | @Entity( 9 | primaryKeys = ["packageName", "dayId"], 10 | foreignKeys = [ 11 | ForeignKey( 12 | entity = Day::class, 13 | parentColumns = ["date"], 14 | childColumns = ["dayId"], 15 | onDelete = ForeignKey.CASCADE 16 | ) 17 | ] 18 | ) 19 | data class DayStats( 20 | val packageName: String, 21 | val totalTime: Long, 22 | val lastUsed: Long, 23 | @ColumnInfo(index = true) 24 | val dayId: ZonedDateTime 25 | ) 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/models/DayWithDayStats.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.models 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Relation 5 | 6 | data class DayWithDayStats( 7 | @Embedded val day: Day, 8 | @Relation( 9 | entity = DayStats::class, 10 | parentColumn = "date", 11 | entityColumn = "dayId" 12 | ) 13 | val dayStats: List 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/models/Session.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.models 2 | 3 | data class Session( 4 | val startMillis: Long, 5 | val endMillis: Long, 6 | val app: App 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/models/SessionMinimal.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.models 2 | 3 | data class SessionMinimal( 4 | val startMillis: Long, 5 | val endMillis: Long, 6 | val packageName: String 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/models/UsageData.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.models 2 | 3 | import com.se7en.screentrack.data.AppUsageManager 4 | 5 | data class UsageData( 6 | val filter: AppUsageManager.FILTER, 7 | val usageList: List 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/repository/AppDetailRepository.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.repository 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.asLiveData 5 | import com.se7en.screentrack.data.AppUsageManager 6 | import com.se7en.screentrack.data.database.StatsDao 7 | import com.se7en.screentrack.models.DayStats 8 | import com.se7en.screentrack.models.SessionMinimal 9 | import kotlinx.coroutines.flow.filterNotNull 10 | import org.threeten.bp.ZonedDateTime 11 | import javax.inject.Inject 12 | 13 | 14 | class AppDetailRepository @Inject constructor( 15 | private val statsDao: StatsDao, 16 | private val appUsageManager: AppUsageManager 17 | ) { 18 | 19 | fun getDayStats(packageName: String): LiveData> { 20 | return statsDao.getDayStats(packageName).filterNotNull().asLiveData() 21 | } 22 | 23 | suspend fun getSessions(packageName: String, date: ZonedDateTime): List { 24 | return appUsageManager.getSessions(packageName, date) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/repository/HomeRepository.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.repository 2 | 3 | import android.util.Log 4 | import com.se7en.screentrack.Utils 5 | import com.se7en.screentrack.data.AppUsageManager 6 | import com.se7en.screentrack.data.database.StatsDao 7 | import com.se7en.screentrack.models.UsageData 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.filterNotNull 10 | import kotlinx.coroutines.flow.map 11 | import org.threeten.bp.temporal.ChronoUnit 12 | import javax.inject.Inject 13 | 14 | 15 | class HomeRepository @Inject constructor( 16 | private val statsDao: StatsDao, 17 | private val appUsageManager: AppUsageManager 18 | ) { 19 | 20 | suspend fun getTodayUsageData(): Flow { 21 | val today = Utils.getZonedDateTime(System.currentTimeMillis(), ChronoUnit.DAYS) 22 | return statsDao.getDayWithDayStats(today).filterNotNull().map { dayWithStats -> 23 | UsageData( 24 | AppUsageManager.FILTER.TODAY, 25 | appUsageManager.getUsageList(dayWithStats) 26 | ) 27 | } 28 | } 29 | 30 | suspend fun getWeekUsageData(): Flow { 31 | val today = Utils.getZonedDateTime(System.currentTimeMillis(), ChronoUnit.DAYS) 32 | val weekStart = today.minusDays(6).truncatedTo(ChronoUnit.DAYS) 33 | 34 | return statsDao.getDaysWithDayStats().filterNotNull().map { 35 | it.forEach { stats -> 36 | if(stats.day.date.isBefore(weekStart)) { 37 | // remove this day's data 38 | statsDao.delete(stats.day) 39 | } 40 | } 41 | 42 | UsageData( 43 | AppUsageManager.FILTER.THIS_WEEK, 44 | appUsageManager.getUsageList(it) 45 | ) 46 | } 47 | } 48 | 49 | suspend fun updateData() { 50 | Log.d("HomeRepository", "updating...") 51 | statsDao.upsert(appUsageManager.getDayWithStatsForWeek()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/repository/TimelineRepository.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.repository 2 | 3 | import com.se7en.screentrack.data.AppUsageManager 4 | import com.se7en.screentrack.models.Session 5 | import org.threeten.bp.ZonedDateTime 6 | import javax.inject.Inject 7 | 8 | 9 | class TimelineRepository @Inject constructor( 10 | private val appUsageManager: AppUsageManager 11 | ) { 12 | 13 | suspend fun getSessions(date: ZonedDateTime): List { 14 | return appUsageManager.getSessions(date) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/ui/AppDetailFragment.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.ui 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.view.View 6 | import androidx.core.content.ContextCompat 7 | import androidx.fragment.app.Fragment 8 | import androidx.fragment.app.viewModels 9 | import androidx.lifecycle.Observer 10 | import androidx.navigation.fragment.navArgs 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import com.github.mikephil.charting.data.BarData 13 | import com.github.mikephil.charting.data.BarDataSet 14 | import com.github.mikephil.charting.data.BarEntry 15 | import com.github.mikephil.charting.data.Entry 16 | import com.github.mikephil.charting.formatter.ValueFormatter 17 | import com.github.mikephil.charting.highlight.Highlight 18 | import com.github.mikephil.charting.listener.OnChartValueSelectedListener 19 | import com.se7en.screentrack.R 20 | import com.se7en.screentrack.Utils 21 | import com.se7en.screentrack.adapters.SessionsAdapter 22 | import com.se7en.screentrack.models.App 23 | import com.se7en.screentrack.models.DayStats 24 | import com.se7en.screentrack.viewmodels.AppDetailViewModel 25 | import dagger.hilt.android.AndroidEntryPoint 26 | import kotlinx.android.synthetic.main.fragment_app_detail.* 27 | import org.threeten.bp.format.TextStyle 28 | import org.threeten.bp.temporal.ChronoUnit 29 | import java.util.* 30 | 31 | 32 | @AndroidEntryPoint 33 | class AppDetailFragment: Fragment(R.layout.fragment_app_detail) { 34 | 35 | private val viewModel: AppDetailViewModel by viewModels() 36 | private val args by navArgs() 37 | private val sessionsAdapter = SessionsAdapter(::listChangedListener) 38 | 39 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 40 | setupViewModelObservers() 41 | 42 | sessionRecyclerView.apply { 43 | adapter = sessionsAdapter 44 | layoutManager = LinearLayoutManager(context) 45 | 46 | addItemDecoration(AppsListItemDecoration()) 47 | } 48 | 49 | val app = App.fromContext(requireContext(), args.packageName) 50 | appName.text = app.appName 51 | appIcon.setImageDrawable(app.iconDrawable) 52 | } 53 | 54 | private fun setupViewModelObservers() { 55 | viewModel.getDayStats(args.packageName).observe(viewLifecycleOwner, Observer { 56 | val today = Utils.getZonedDateTime(System.currentTimeMillis(), ChronoUnit.DAYS) 57 | var lastUsedTime = 0L 58 | var todayTotal = 0L 59 | var weekTotal = 0L 60 | val entries = arrayListOf() 61 | val labels = arrayListOf() 62 | it.forEachIndexed { index, stats -> 63 | val entry = BarEntry(index.toFloat(), stats.totalTime.toFloat()) 64 | val label = stats.dayId.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()) 65 | entries.add(entry) 66 | labels.add(label) 67 | 68 | if(stats.dayId.truncatedTo(ChronoUnit.DAYS).isEqual(today)) { 69 | todayTotal = stats.totalTime 70 | } 71 | 72 | weekTotal += stats.totalTime 73 | if(stats.lastUsed > lastUsedTime) lastUsedTime = stats.lastUsed 74 | } 75 | 76 | setupChart(entries, labels, it) 77 | 78 | lastUsed.text = getString(R.string.last_used_template, Utils.getLastUsedFormattedDate( 79 | lastUsedTime 80 | )) 81 | usedToday.text = Utils.getUsageTimeString(todayTotal) 82 | usedThisWeek.text = Utils.getUsageTimeString(weekTotal) 83 | average.text = Utils.getUsageTimeString(weekTotal / 7) 84 | 85 | Log.d("AppDetailFragment", it.toString()) 86 | }) 87 | 88 | viewModel.sessionsLiveData.observe(viewLifecycleOwner, Observer { 89 | Log.d("AppDetailFragment", it.toString()) 90 | 91 | sessionsAdapter.submitList(it) 92 | }) 93 | } 94 | 95 | private fun setupChart( 96 | entries: List, 97 | labels: List, 98 | dayStats: List 99 | ) { 100 | val datasetValueFormatter = object : ValueFormatter() { 101 | override fun getFormattedValue(value: Float): String { 102 | return Utils.getUsageTimeString(value.toLong()) 103 | } 104 | } 105 | 106 | val xAxisValueFormatter = object: ValueFormatter() { 107 | override fun getFormattedValue(value: Float): String { 108 | return labels[value.toInt()] 109 | } 110 | } 111 | 112 | val textColor = ContextCompat.getColor(requireContext(), R.color.material_on_surface_emphasis_high_type) 113 | val barDataSet = BarDataSet(entries, "Usage") 114 | barDataSet.valueFormatter = datasetValueFormatter 115 | barDataSet.highLightColor = ContextCompat.getColor(requireContext(), R.color.colorAccent) 116 | barDataSet.highLightAlpha = 255 117 | barDataSet.valueTextColor = textColor 118 | barDataSet.color = ContextCompat.getColor(requireContext(), R.color.colorPrimary) 119 | 120 | barChart.xAxis.valueFormatter = xAxisValueFormatter 121 | barChart.xAxis.granularity = 1f 122 | barChart.xAxis.textColor = textColor 123 | barChart.legend.textColor = textColor 124 | barChart.description.isEnabled = false 125 | barChart.isDoubleTapToZoomEnabled = false 126 | barChart.axisLeft.isEnabled = false 127 | barChart.axisRight.isEnabled = false 128 | 129 | barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener { 130 | override fun onNothingSelected() { 131 | sessionsAdapter.submitList(listOf()) 132 | Log.d("AppDetailFragment", "nothing selected") 133 | } 134 | 135 | override fun onValueSelected(e: Entry?, h: Highlight?) { 136 | Log.d("AppDetailFragment", "something selected ${e?.x}") 137 | e?.let { 138 | viewModel.fetchSessions(args.packageName, dayStats[it.x.toInt()].dayId) 139 | } 140 | } 141 | }) 142 | barChart.data = BarData(barDataSet) 143 | barChart.setDrawGridBackground(false) 144 | barChart.invalidate() 145 | } 146 | 147 | private fun listChangedListener(isEmpty: Boolean) { 148 | selectBarTextView.visibility = if(isEmpty) View.VISIBLE else View.GONE 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/ui/AppsListItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.ui 2 | 3 | import android.graphics.Rect 4 | import android.view.View 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | class AppsListItemDecoration: RecyclerView.ItemDecoration() { 8 | override fun getItemOffsets( 9 | outRect: Rect, 10 | view: View, 11 | parent: RecyclerView, 12 | state: RecyclerView.State 13 | ) { 14 | val position = parent.getChildAdapterPosition(view) 15 | if (position == 0 || position == state.itemCount) 16 | with(outRect) { 17 | left = 8 18 | right = 8 19 | } 20 | else 21 | with(outRect) { 22 | left = 8 23 | right = 8 24 | top = 1 25 | bottom = 1 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/ui/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.ui 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import com.google.android.material.tabs.TabLayoutMediator 7 | import com.se7en.screentrack.R 8 | import com.se7en.screentrack.adapters.UsageFragmentAdapter 9 | import com.se7en.screentrack.data.AppUsageManager 10 | import kotlinx.android.synthetic.main.fragment_home.* 11 | 12 | 13 | class HomeFragment: Fragment(R.layout.fragment_home) { 14 | 15 | private lateinit var usageFragmentAdapter: UsageFragmentAdapter 16 | 17 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 18 | usageFragmentAdapter = UsageFragmentAdapter(this) 19 | pager.adapter = usageFragmentAdapter 20 | TabLayoutMediator(timeFilterLayout, pager) { tab, position -> 21 | when(AppUsageManager.FILTER.values()[position]) { 22 | AppUsageManager.FILTER.TODAY -> { 23 | tab.text = getString(R.string.today) 24 | } 25 | 26 | AppUsageManager.FILTER.THIS_WEEK -> { 27 | tab.text = getString(R.string.this_week) 28 | } 29 | } 30 | }.attach() 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.ui 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.navigation.NavController 7 | import androidx.navigation.NavDestination 8 | import androidx.navigation.findNavController 9 | import androidx.navigation.ui.AppBarConfiguration 10 | import androidx.navigation.ui.setupWithNavController 11 | import com.se7en.screentrack.R 12 | import dagger.hilt.android.AndroidEntryPoint 13 | import kotlinx.android.synthetic.main.activity_main.* 14 | 15 | @AndroidEntryPoint 16 | class MainActivity : AppCompatActivity(), NavController.OnDestinationChangedListener { 17 | 18 | private val navController by lazy { findNavController(R.id.navHost) } 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | setContentView(R.layout.activity_main) 23 | 24 | setSupportActionBar(toolbar) 25 | toolbar.setupWithNavController( 26 | navController, 27 | AppBarConfiguration.Builder( 28 | setOf(R.id.permissionFragment, R.id.homeFragment, R.id.timelineFragment, R.id.settingsFragment) 29 | ).build() 30 | ) 31 | 32 | bottomNav.setupWithNavController(navController) 33 | } 34 | 35 | override fun onResume() { 36 | super.onResume() 37 | navController.addOnDestinationChangedListener(this) 38 | } 39 | 40 | override fun onPause() { 41 | super.onPause() 42 | navController.removeOnDestinationChangedListener(this) 43 | } 44 | 45 | override fun onDestinationChanged( 46 | controller: NavController, 47 | destination: NavDestination, 48 | arguments: Bundle? 49 | ) { 50 | when(destination.id) { 51 | R.id.homeFragment -> { 52 | toolbar_title.text = getString(R.string.home) 53 | bottomNav.visibility = View.VISIBLE 54 | } 55 | 56 | R.id.timelineFragment -> { 57 | toolbar_title.text = getString(R.string.timeline) 58 | bottomNav.visibility = View.VISIBLE 59 | } 60 | 61 | R.id.settingsFragment -> { 62 | toolbar_title.text = getString(R.string.settings) 63 | bottomNav.visibility = View.VISIBLE 64 | } 65 | 66 | R.id.permissionFragment -> { 67 | toolbar_title.text = getString(R.string.app_name) 68 | bottomNav.visibility = View.GONE 69 | } 70 | 71 | R.id.appDetailFragment -> { 72 | toolbar_title.text = "" 73 | bottomNav.visibility = View.GONE 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/ui/PermissionFragment.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.ui 2 | 3 | import android.app.AppOpsManager 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.provider.Settings 9 | import android.view.View 10 | import androidx.appcompat.app.AlertDialog 11 | import androidx.fragment.app.Fragment 12 | import androidx.navigation.fragment.findNavController 13 | import com.se7en.screentrack.R 14 | import kotlinx.android.synthetic.main.fragment_permission.* 15 | 16 | class PermissionFragment: Fragment(R.layout.fragment_permission) { 17 | 18 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 19 | showDialogOrProceed() 20 | 21 | settingsButton.setOnClickListener { 22 | openSettings() 23 | } 24 | 25 | proceedButton.setOnClickListener { 26 | showDialogOrProceed() 27 | } 28 | } 29 | 30 | private fun showDialogOrProceed() { 31 | if(!hasUsageAccessPermission()) 32 | showUsageAccessPermissionDialog() 33 | else { 34 | findNavController().navigate(R.id.action_permissionFragment_to_homeFragment) 35 | } 36 | } 37 | 38 | private fun showUsageAccessPermissionDialog() { 39 | AlertDialog.Builder(requireContext()) 40 | .setTitle("Permission") 41 | .setMessage("In order to use the app, " + 42 | "please grant the App Usage Access permission in settings") 43 | .setNegativeButton("No") { _, _ -> 44 | activity?.finish() 45 | } 46 | .setPositiveButton("Okay") { _, _ -> 47 | openSettings() 48 | } 49 | .create().show() 50 | } 51 | 52 | private fun openSettings() { 53 | val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS) 54 | if(intent.resolveActivity(requireActivity().packageManager) != null) { 55 | startActivity(intent) 56 | } 57 | } 58 | 59 | private fun hasUsageAccessPermission(): Boolean { 60 | val appOpsManager = context?.getSystemService( 61 | Context.APP_OPS_SERVICE 62 | ) as AppOpsManager? ?: return false 63 | 64 | val mode = when { 65 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> appOpsManager.unsafeCheckOpNoThrow( 66 | AppOpsManager.OPSTR_GET_USAGE_STATS, 67 | requireContext().applicationInfo.uid, 68 | requireContext().packageName 69 | ) 70 | else -> appOpsManager.checkOpNoThrow( 71 | AppOpsManager.OPSTR_GET_USAGE_STATS, 72 | requireContext().applicationInfo.uid, 73 | requireContext().applicationInfo.packageName 74 | ) 75 | } 76 | 77 | return mode == AppOpsManager.MODE_ALLOWED 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/ui/SettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.ui 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.fragment.app.Fragment 7 | import com.se7en.screentrack.R 8 | import com.se7en.screentrack.SettingsManager 9 | import kotlinx.android.synthetic.main.fragment_settings.* 10 | 11 | class SettingsFragment: Fragment(R.layout.fragment_settings) { 12 | 13 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 14 | themeSpinner.setItems(SettingsManager.themes) 15 | themeSpinner.setOnItemSelectedListener { _, _, _, item -> 16 | SettingsManager.setTheme(item.toString(), getSharedPreferences()) 17 | } 18 | 19 | themeSpinner.selectedIndex = SettingsManager.themes.indexOf( 20 | SettingsManager.getCurrentTheme(getSharedPreferences()).toString() 21 | ) 22 | } 23 | 24 | private fun getSharedPreferences() = activity?.getSharedPreferences( 25 | getString(R.string.app_shared_prefs_name), Context.MODE_PRIVATE 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/ui/TimelineFragment.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.ui 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import androidx.fragment.app.activityViewModels 7 | import androidx.lifecycle.Observer 8 | import androidx.navigation.fragment.findNavController 9 | import androidx.recyclerview.widget.LinearLayoutManager 10 | import com.se7en.screentrack.R 11 | import com.se7en.screentrack.adapters.TimelineAdapter 12 | import com.se7en.screentrack.models.App 13 | import com.se7en.screentrack.viewmodels.TimelineViewModel 14 | import kotlinx.android.synthetic.main.fragment_timeline.* 15 | import org.threeten.bp.ZonedDateTime 16 | import org.threeten.bp.format.DateTimeFormatter 17 | 18 | class TimelineFragment: Fragment(R.layout.fragment_timeline) { 19 | 20 | private val viewModel: TimelineViewModel by activityViewModels() 21 | private val timelineAdapter = TimelineAdapter(::onItemClick) 22 | 23 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 24 | val dateFormat = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy") 25 | todayDate.text = ZonedDateTime.now().format(dateFormat) 26 | 27 | timelineRecyclerView.apply { 28 | adapter = timelineAdapter 29 | 30 | layoutManager = LinearLayoutManager(context) 31 | } 32 | 33 | setupViewModelObservers() 34 | } 35 | 36 | private fun setupViewModelObservers() { 37 | viewModel.getSessions().observe(viewLifecycleOwner, Observer { 38 | timelineAdapter.submitList(it) 39 | }) 40 | } 41 | 42 | private fun onItemClick(app: App) { 43 | val action = TimelineFragmentDirections.actionTimelineFragmentToAppDetailFragment( 44 | packageName = app.packageName, 45 | appName = app.appName 46 | ) 47 | findNavController().navigate(action) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/ui/UsageListFragment.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.ui 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.view.View 6 | import androidx.fragment.app.Fragment 7 | import androidx.fragment.app.activityViewModels 8 | import androidx.lifecycle.Observer 9 | import androidx.navigation.fragment.findNavController 10 | import androidx.recyclerview.widget.LinearLayoutManager 11 | import com.se7en.screentrack.Constants 12 | import com.se7en.screentrack.R 13 | import com.se7en.screentrack.adapters.AppsUsageAdapter 14 | import com.se7en.screentrack.data.AppUsageManager 15 | import com.se7en.screentrack.models.App 16 | import com.se7en.screentrack.viewmodels.HomeViewModel 17 | import kotlinx.android.synthetic.main.fragment_usage_list.* 18 | 19 | class UsageListFragment: Fragment(R.layout.fragment_usage_list) { 20 | 21 | private val usageAdapter = AppsUsageAdapter(::onCurrentListChanged, ::onItemClick) 22 | private val viewModel: HomeViewModel by activityViewModels() 23 | 24 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 25 | setupViewModelObservers() 26 | 27 | usageRecyclerView.apply { 28 | adapter = usageAdapter 29 | layoutManager = LinearLayoutManager(context) 30 | 31 | addItemDecoration(AppsListItemDecoration()) 32 | } 33 | 34 | } 35 | 36 | private fun setupViewModelObservers() { 37 | val filter = when(arguments?.getInt(Constants.FILTER_KEY)) { 38 | 0 -> AppUsageManager.FILTER.TODAY 39 | 1 -> AppUsageManager.FILTER.THIS_WEEK 40 | 41 | else -> AppUsageManager.FILTER.TODAY 42 | } 43 | 44 | viewModel.getUsageLiveData(filter).observe(viewLifecycleOwner, Observer { usageData -> 45 | Log.d("UsageListFragment", usageData.toString()) 46 | 47 | usageAdapter.submitList(usageData.usageList) 48 | }) 49 | } 50 | 51 | private fun onCurrentListChanged() { 52 | usageRecyclerView.scrollToPosition(0) 53 | } 54 | 55 | private fun onItemClick(app: App) { 56 | val action = HomeFragmentDirections.actionHomeFragmentToAppDetailFragment( 57 | packageName = app.packageName, 58 | appName = app.appName 59 | ) 60 | findNavController().navigate(action) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/viewmodels/AppDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.viewmodels 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.se7en.screentrack.models.DayStats 8 | import com.se7en.screentrack.models.SessionMinimal 9 | import com.se7en.screentrack.repository.AppDetailRepository 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.launch 12 | import org.threeten.bp.ZonedDateTime 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class AppDetailViewModel @Inject constructor( 17 | private val repository: AppDetailRepository 18 | ): ViewModel() { 19 | 20 | val sessionsLiveData = MutableLiveData>() 21 | 22 | fun getDayStats(packageName: String): LiveData> { 23 | return repository.getDayStats(packageName) 24 | } 25 | 26 | fun fetchSessions(packageName: String, date: ZonedDateTime) { 27 | viewModelScope.launch { 28 | sessionsLiveData.postValue(repository.getSessions(packageName, date)) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/viewmodels/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.viewmodels 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.se7en.screentrack.data.AppUsageManager 7 | import com.se7en.screentrack.models.UsageData 8 | import com.se7en.screentrack.repository.HomeRepository 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.flow.collect 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class HomeViewModel @Inject constructor( 16 | private val repository: HomeRepository 17 | ): ViewModel() { 18 | 19 | private val todayUsageData = MutableLiveData() 20 | private val last7DaysUsageData = MutableLiveData() 21 | 22 | fun getUsageLiveData(filter: AppUsageManager.FILTER) = 23 | when(filter) { 24 | AppUsageManager.FILTER.TODAY -> todayUsageData 25 | AppUsageManager.FILTER.THIS_WEEK -> last7DaysUsageData 26 | } 27 | 28 | init { 29 | viewModelScope.launch { 30 | repository.getTodayUsageData().collect { 31 | todayUsageData.value = it 32 | } 33 | } 34 | viewModelScope.launch { 35 | repository.getWeekUsageData().collect { 36 | last7DaysUsageData.value = it 37 | } 38 | } 39 | viewModelScope.launch { repository.updateData() } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/se7en/screentrack/viewmodels/TimelineViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack.viewmodels 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.se7en.screentrack.models.Session 8 | import com.se7en.screentrack.repository.TimelineRepository 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.launch 11 | import org.threeten.bp.ZonedDateTime 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class TimelineViewModel @Inject constructor( 16 | private val repository: TimelineRepository 17 | ): ViewModel() { 18 | 19 | private val sessions = MutableLiveData>() 20 | 21 | init { 22 | viewModelScope.launch { 23 | fetchSessions(ZonedDateTime.now()) 24 | } 25 | } 26 | 27 | private suspend fun fetchSessions(date: ZonedDateTime) { 28 | sessions.value = repository.getSessions(date) 29 | } 30 | 31 | fun getSessions(): LiveData> = sessions 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_forward.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tail_arrow_forward.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_timeline.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tab_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tab_indicator.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tab_text_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 27 | 28 | 29 | 30 | 43 | 44 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_app_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 20 | 26 | 27 | 37 | 43 | 44 | 49 | 54 | 55 | 61 | 62 | 63 | 64 | 65 | 74 | 80 | 81 | 86 | 87 | 88 | 97 | 103 | 104 | 109 | 110 | 111 | 120 | 126 | 127 | 132 | 133 | 134 | 143 | 144 | 155 | 156 | 163 | 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 19 | 20 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_permission.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | 27 | 28 | 42 | 43 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 21 | 22 | 33 | 34 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_timeline.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_usage_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/session_rv_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | 20 | 21 | 27 | 28 | 29 | 30 | 35 | 36 | 44 | 45 | 50 | 51 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/res/layout/timeline_session_rv_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 27 | 28 | 37 | 38 | 45 | 46 | 58 | 59 | 64 | 65 | 71 | 72 | 73 | 74 | 86 | 87 | 92 | 93 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /app/src/main/res/layout/usage_rv_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 30 | 31 | 38 | 39 | 47 | 48 | 49 | 50 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/res/menu/bottom_nav_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 15 | 16 | 17 | 21 | 22 | 25 | 26 | 29 | 30 | 31 | 32 | 36 | 42 | 43 | 44 | 48 | 51 | 52 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #212121 5 | #121212 6 | 7 | @color/material_on_surface_emphasis_high_type 8 | 9 | #121212 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #0478af 4 | #004c7f 5 | #AF3A04 6 | 7 | #ffffff 8 | 9 | #ffffff 10 | #efefef 11 | 12 | #ffffff 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #0478AF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ScreenTrack 3 | This Week 4 | Today 5 | App Icon 6 | Arrow 7 | Average 8 | Last used: %1s 9 | Sessions 10 | Select a day from the chart to view sessions of that day 11 | This app requires the App Usage Access permission to work. Once you\'ve granted the permission, tap on Proceed to start using the app. 12 | Proceed 13 | Settings 14 | current_tab 15 | Home 16 | Timeline 17 | End 18 | Start 19 | Theme 20 | app_prefs 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 19 | 20 | 30 | 31 | 34 | 35 | 38 | 39 | 42 | 43 | 46 | -------------------------------------------------------------------------------- /app/src/test/java/com/se7en/screentrack/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.se7en.screentrack 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = "1.6.10" 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | 12 | def nav_version = "2.4.1" 13 | classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" 14 | 15 | classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha' 16 | 17 | // NOTE: Do not place your application dependencies here; they belong 18 | // in the individual module build.gradle files 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | google() 25 | jcenter() 26 | maven { url 'https://jitpack.io' } 27 | } 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Mar 07 23:54:17 IST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/keystore.jks -------------------------------------------------------------------------------- /screenshots/app_detail_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/screenshots/app_detail_dark.png -------------------------------------------------------------------------------- /screenshots/app_detail_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/screenshots/app_detail_light.png -------------------------------------------------------------------------------- /screenshots/home_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/screenshots/home_dark.png -------------------------------------------------------------------------------- /screenshots/home_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/screenshots/home_light.png -------------------------------------------------------------------------------- /screenshots/timeline_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/screenshots/timeline_dark.png -------------------------------------------------------------------------------- /screenshots/timeline_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashar-7/ScreenTrack-Android/f899258b1e0aab3e2b0aa3f3e63d9a2b0299cfc9/screenshots/timeline_light.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name = "ScreenTrack" --------------------------------------------------------------------------------