├── .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 |
5 |
6 |
7 |
8 |
9 |
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 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
--------------------------------------------------------------------------------
/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"
--------------------------------------------------------------------------------