DEV Community

Devika Android
Devika Android

Posted on

Digital Wellbeing : Total uses time and Categories

In today’s fast-paced digital world, we spend a significant amount of time on our smartphones. While technology makes life easier, excessive screen time can negatively impact our productivity, focus, and mental health.

A Digital Wellbeing app helps users monitor and manage their daily screen usage, enabling them to build healthier digital habits and maintain a better work-life balance.

Manifest File

 <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
        tools:ignore="ProtectedPermissions" />

    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
        tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.DigitWellBeing">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".MyAccessibilityService"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
            android:exported="true">

            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService"/>
            </intent-filter>

            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_config"/>
        </service>
    </application>
Enter fullscreen mode Exit fullscreen mode

Model Class

data class AppUsage(
    val appName: String,
    val packageName: String,
    val usageTime: Long,
    val category: String
)
Enter fullscreen mode Exit fullscreen mode

Main Activity

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var adapter: AppUsageAdapter
    val categories = listOf(
        "All Categories",
        "No Specified",
        "Education/Business",
        "Entertainment",
        "Family",
        "Game",
        "Health/Fitness",
        "Social Networking",
        "System Apps",
        "Utility"
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
    }


    private fun initView() {

        val spinnerAdapter = ArrayAdapter(
            this,
            android.R.layout.simple_spinner_item,
            categories
        )

        spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        binding.spinnerCategories.adapter = spinnerAdapter

        adapter = AppUsageAdapter(emptyList(), { itemData ->

            if (Settings.canDrawOverlays(this) && isAccessibilityServiceEnabled(this)) {
                showTimerDialog(itemData)
            } else {
                if (!Settings.canDrawOverlays(this)) {
                    val intent = Intent(
                        Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                        Uri.parse("package:$packageName")
                    )
                    startActivity(intent)
                }

                if (!isAccessibilityServiceEnabled(this)) {
                    startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
                }
            }

        })
        binding.recyclerApps.layoutManager = LinearLayoutManager(this)
        binding.recyclerApps.adapter = adapter

        val calendar = Calendar.getInstance()
        calendar.set(Calendar.HOUR_OF_DAY, 0)
        calendar.set(Calendar.MINUTE, 0)
        calendar.set(Calendar.SECOND, 0)

        val startTime = calendar.timeInMillis
        val endTime = System.currentTimeMillis()

        val usageStatsManager = getSystemService(USAGE_STATS_SERVICE) as UsageStatsManager

        val stats = usageStatsManager.queryUsageStats(
            UsageStatsManager.INTERVAL_DAILY,
            startTime,
            endTime
        )

        val pm = packageManager
        val appMap = mutableMapOf<String, AppUsage>()

        for (usage in stats) {
            val totalTime = usage.totalTimeInForeground

            if (totalTime > 0) {
                try {
                    val packageName = usage.packageName

                    val appInfo = pm.getApplicationInfo(packageName, 0)
                    val appName = pm.getApplicationLabel(appInfo).toString()

                    val category = getCategoryName(appInfo.category, packageName)

                    if (appMap.containsKey(packageName)) {
                        val existing = appMap[packageName]!!
                        appMap[packageName] = existing.copy(
                            usageTime = existing.usageTime + totalTime
                        )
                    } else {
                        appMap[packageName] = AppUsage(
                            appName = appName,
                            packageName = packageName,
                            usageTime = totalTime,
                            category = category
                        )
                    }

                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }
        }

        val appList = appMap.values
            .sortedByDescending { it.usageTime }

        binding.spinnerCategories.onItemSelectedListener =
            object : AdapterView.OnItemSelectedListener {
                override fun onItemSelected(
                    parent: AdapterView<*>,
                    view: View?,
                    position: Int,
                    id: Long
                ) {

                    val selectedCategory = categories[position]
                    val filteredList = filterApps(selectedCategory, appList)

                    adapter.updateList(filteredList)
                }

                override fun onNothingSelected(parent: AdapterView<*>) {}
            }

        val totalUsageTime = appList.sumOf { it.usageTime }

        val formattedTime = formatTime(totalUsageTime)

        binding.tvTotalTime.text = "Total Uses Time:- $formattedTime"
    }

    override fun onResume() {
        super.onResume()
        if (!isUsageAccessGranted(this)) {
            startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
        } else {
            initView()
        }
    }

    fun filterApps(category: String, list: List<AppUsage>): List<AppUsage> {
        return if (category == "All Categories") {
            list
        } else {
            list.filter { it.category == category }
        }
    }

    fun isUsageAccessGranted(context: Context): Boolean {
        val appOps = context.getSystemService(APP_OPS_SERVICE) as AppOpsManager

        val mode = appOps.checkOpNoThrow(
            AppOpsManager.OPSTR_GET_USAGE_STATS,
            android.os.Process.myUid(),
            context.packageName
        )

        return if (mode == AppOpsManager.MODE_DEFAULT) {
            context.checkCallingOrSelfPermission(
                android.Manifest.permission.PACKAGE_USAGE_STATS
            ) == PackageManager.PERMISSION_GRANTED
        } else {
            mode == AppOpsManager.MODE_ALLOWED
        }
    }

    fun isAccessibilityServiceEnabled(context: Context): Boolean {
        val expectedComponent = ComponentName(context, MyAccessibilityService::class.java)
        val enabledServices = Settings.Secure.getString(
            context.contentResolver,
            Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
        ) ?: return false

        return enabledServices.contains(expectedComponent.flattenToString())
    }

    fun formatTime(ms: Long): String {
        val seconds = ms / 1000
        val minutes = seconds / 60
        val hours = minutes / 60

        return "${hours}h ${minutes % 60}m"
    }

    fun getCategoryName(category: Int, packageName: String): String {
        return when (category) {
            ApplicationInfo.CATEGORY_GAME -> "Game"
            ApplicationInfo.CATEGORY_SOCIAL -> "Social Networking"
            ApplicationInfo.CATEGORY_PRODUCTIVITY -> "Education/Business"
            ApplicationInfo.CATEGORY_AUDIO,
            ApplicationInfo.CATEGORY_VIDEO -> "Entertainment"

            ApplicationInfo.CATEGORY_NEWS -> "Education/Business"
            ApplicationInfo.CATEGORY_MAPS -> "Utility"
            ApplicationInfo.CATEGORY_ACCESSIBILITY -> "Utility"
            ApplicationInfo.CATEGORY_IMAGE -> "Entertainment"
            ApplicationInfo.CATEGORY_UNDEFINED -> {
                if (isSystemApp(packageName)) "System Apps"
                else "No Specified"
            }

            else -> "No Specified"
        }
    }

    fun isSystemApp(packageName: String): Boolean {
        return try {
            val appInfo = packageManager.getApplicationInfo(packageName, 0)
            (appInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0
        } catch (e: Exception) {
            false
        }
    }

    fun showTimerDialog(app: AppUsage) {
        val dialog = Dialog(this)
        val binding = DialogTimerDesignBinding.inflate(LayoutInflater.from(this), null, false)
        dialog.setContentView(binding.root)

        binding.btnSet.setOnClickListener {
            val minutes = binding.etMinutes.text.toString().toIntOrNull() ?: 0

            if (minutes > 0) {
                val endTime = System.currentTimeMillis() + minutes * 60 * 1000

                val pref = getSharedPreferences("app_limits", MODE_PRIVATE)
                pref.edit { putLong(app.packageName, endTime) }

                Toast.makeText(this, "Timer Set", Toast.LENGTH_SHORT).show()
                dialog.dismiss()
            }
        }

        dialog.show()
    }
}
Enter fullscreen mode Exit fullscreen mode

Serviece Class

package com.example.digitwellbeing

import android.accessibilityservice.AccessibilityService
import android.graphics.PixelFormat
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
import android.widget.Button

class MyAccessibilityService : AccessibilityService() {

    private var windowManager: WindowManager? = null
    private var overlayView: View? = null
    private var currentPackage: String? = null
    private var isHandlerStarted = false

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {
        Log.d("ACCESS_SERVICE", "Event: ${event?.packageName}")

        val pkg = event?.packageName?.toString() ?: return

        currentPackage = pkg

        // 🔥 Start handler here (ONLY ONCE)
        if (!isHandlerStarted) {
            isHandlerStarted = true
            handler.post(runnable)
        }

        if (event?.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED ||
            event?.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {

            currentPackage = event.packageName?.toString()
        }
    }

    override fun onInterrupt() {
        TODO("Not yet implemented")
    }

    override fun onServiceConnected() {
        super.onServiceConnected()
        Log.d("ACCESS_SERVICE", "Service Connected ✅")
        handler.post(runnable)
    }

    private val handler = Handler(Looper.getMainLooper())

    private val runnable = object : Runnable {
        override fun run() {

            currentPackage?.let { packageName ->

                if (packageName == this@MyAccessibilityService.packageName) return

                val pref = getSharedPreferences("app_limits", MODE_PRIVATE)
                val endTime = pref.getLong(packageName, 0)

                if (endTime > 0 && System.currentTimeMillis() > endTime) {
                    showOverlay()
                }
            }

            handler.postDelayed(this, 1000) // every 1 sec
        }
    }


    private fun showOverlay() {
        Log.d("BLOCK_CHECK", "LIMIT EXCEEDED 🚫")

        if (overlayView != null) return

        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager

        val inflater = LayoutInflater.from(this)
        overlayView = inflater.inflate(R.layout.overlay_block, null)

        val params = WindowManager.LayoutParams(
            WindowManager.LayoutParams.MATCH_PARENT,
            WindowManager.LayoutParams.MATCH_PARENT,
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
            0,
            PixelFormat.TRANSLUCENT
        )

        windowManager?.addView(overlayView, params)

        overlayView?.findViewById<Button>(R.id.btnClose)?.setOnClickListener {
            removeOverlay()
        }
    }

    private fun removeOverlay() {
        if (overlayView != null) {
            windowManager?.removeView(overlayView)
            overlayView = null
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Resources (accessibility_config)

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="false"
    android:description="@string/accessibility_desc"
    android:notificationTimeout="100" />

<string name="accessibility_desc">
    This app monitors app usage to block apps after time limit.
</string>

Enter fullscreen mode Exit fullscreen mode

This app acts as a personal assistant that guides users toward mindful usage. It encourages breaks, limits unnecessary usage, and promotes a healthier lifestyle.Digital wellbeing is not about avoiding technology but using it wisely. With the help of this app, users can take control of their digital life and create a more balanced and productive routine.

Top comments (0)