From 0b6a6ad5ee907bb0470074b91dc8a47bcbf8aa17 Mon Sep 17 00:00:00 2001 From: Pawan Nagar Date: Sat, 5 Oct 2024 17:33:37 +0530 Subject: [PATCH] More accurate screen usage along with bug fixes --- .../generics/SafeServiceConnection.java | 9 -- .../android/helpers/DeviceAppsHelper.java | 2 +- .../android/helpers/ScreenUsageHelper.java | 61 ++++----- .../android/helpers/SharedPrefsHelper.java | 14 ++- .../android/models/AggregatedUsage.java | 25 ++++ .../android/receivers/DeviceBootReceiver.java | 44 +++++-- .../alarm/MidnightResetReceiver.java | 45 ++++--- .../services/EmergencyPauseService.java | 3 +- .../services/MindfulAccessibilityService.java | 20 +-- .../mindful/android/utils/AppConstants.java | 5 - .../android/widgets/DeviceUsageWidget.java | 119 +++++++++++------- lib/main.dart | 16 ++- pubspec.lock | 28 ++--- 13 files changed, 231 insertions(+), 160 deletions(-) create mode 100644 android/app/src/main/java/com/mindful/android/models/AggregatedUsage.java diff --git a/android/app/src/main/java/com/mindful/android/generics/SafeServiceConnection.java b/android/app/src/main/java/com/mindful/android/generics/SafeServiceConnection.java index e6c1248..3b4df66 100644 --- a/android/app/src/main/java/com/mindful/android/generics/SafeServiceConnection.java +++ b/android/app/src/main/java/com/mindful/android/generics/SafeServiceConnection.java @@ -100,15 +100,6 @@ public void unBindService() { } } - /** - * Starts the service if it is not already running. - */ - public void startOnly(String action) { - if (!Utils.isServiceRunning(mContext, mServiceClass.getName())) { - mContext.startService(new Intent(mContext, mServiceClass).setAction(action)); - } - } - /** * Starts and binds the service if it is not already running. */ diff --git a/android/app/src/main/java/com/mindful/android/helpers/DeviceAppsHelper.java b/android/app/src/main/java/com/mindful/android/helpers/DeviceAppsHelper.java index 1255134..3f2e9ed 100644 --- a/android/app/src/main/java/com/mindful/android/helpers/DeviceAppsHelper.java +++ b/android/app/src/main/java/com/mindful/android/helpers/DeviceAppsHelper.java @@ -151,7 +151,7 @@ private static List fetchAppsAndUsage(@NonNull Context context) { long screenUsageStart = screenUsageCal.getTimeInMillis(); long dataUsageStart = dataUsageCal.getTimeInMillis(); - HashMap screenUsageOneDay = ScreenUsageHelper.fetchUsageForInterval(usageStatsManager, screenUsageStart, screenUsageStart + ms24Hours); + HashMap screenUsageOneDay = ScreenUsageHelper.fetchUsageForInterval(usageStatsManager, screenUsageStart, screenUsageStart + ms24Hours, null); HashMap mobileUsageOneDay = NetworkUsageHelper.fetchMobileUsageForInterval(networkStatsManager, dataUsageStart, dataUsageStart + ms24Hours); HashMap wifiUsageOneDay = NetworkUsageHelper.fetchWifiUsageForInterval(networkStatsManager, dataUsageStart, dataUsageStart + ms24Hours); diff --git a/android/app/src/main/java/com/mindful/android/helpers/ScreenUsageHelper.java b/android/app/src/main/java/com/mindful/android/helpers/ScreenUsageHelper.java index e6f3f5f..062fa7e 100644 --- a/android/app/src/main/java/com/mindful/android/helpers/ScreenUsageHelper.java +++ b/android/app/src/main/java/com/mindful/android/helpers/ScreenUsageHelper.java @@ -17,6 +17,7 @@ import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.Calendar; import java.util.HashMap; @@ -30,14 +31,17 @@ public class ScreenUsageHelper { /** * Fetches screen usage statistics for a specified time interval using usage events. + * If the target package is not null then this method will fetch usage for that app only + * otherwise for all device apps. * * @param usageStatsManager The UsageStatsManager used to query screen usage data. * @param start The start time of the interval in milliseconds. * @param end The end time of the interval in milliseconds. + * @param targetedPackage The package name of the app for fetching its screen usage. * @return A map with package names as keys and their corresponding screen usage time in seconds as values. */ @NonNull - public static HashMap fetchUsageForInterval(@NonNull UsageStatsManager usageStatsManager, long start, long end) { + public static HashMap fetchUsageForInterval(@NonNull UsageStatsManager usageStatsManager, long start, long end, @Nullable String targetedPackage) { HashMap oneDayUsageMap = new HashMap<>(); UsageEvents usageEvents = usageStatsManager.queryEvents(start, end); Map lastResumedEvents = new HashMap<>(); @@ -47,20 +51,30 @@ public static HashMap fetchUsageForInterval(@NonNull UsageStatsMan while (usageEvents.hasNextEvent()) { UsageEvents.Event currentEvent = new UsageEvents.Event(); // Do not move this from while loop usageEvents.getNextEvent(currentEvent); - String packageName = currentEvent.getPackageName(); int eventType = currentEvent.getEventType(); + String packageName = currentEvent.getPackageName(); + /// If target package is not null + if (targetedPackage != null && !packageName.equals(targetedPackage)) continue; + + String className = currentEvent.getClassName(); + String eventKey = packageName + className; + if (eventType == UsageEvents.Event.ACTIVITY_RESUMED) { - lastResumedEvents.put(packageName, currentEvent); + lastResumedEvents.put(eventKey, currentEvent); isFirstEvent = false; - } else if (eventType == UsageEvents.Event.ACTIVITY_PAUSED) { + } else if (eventType == UsageEvents.Event.ACTIVITY_STOPPED || eventType == UsageEvents.Event.ACTIVITY_PAUSED) { Long screenTime = oneDayUsageMap.getOrDefault(packageName, 0L); - UsageEvents.Event lastResumedEvent = lastResumedEvents.get(packageName); - if (lastResumedEvent != null && lastResumedEvent.getPackageName().equals(packageName)) { + UsageEvents.Event lastResumedEvent = lastResumedEvents.get(eventKey); + + if (lastResumedEvent != null + && lastResumedEvent.getPackageName().equals(packageName) + && lastResumedEvent.getClassName().equals(className) + ) { // Calculate usage from the last ACTIVITY_RESUMED to this ACTIVITY_PAUSED screenTime += (currentEvent.getTimeStamp() - lastResumedEvent.getTimeStamp()); oneDayUsageMap.put(packageName, screenTime); - lastResumedEvents.remove(packageName); + lastResumedEvents.remove(eventKey); } else if (isFirstEvent) { // Fallback logic in case no matching ACTIVITY_RESUMED was found. May be this app was opened before START time screenTime += (currentEvent.getTimeStamp() - start); @@ -90,36 +104,9 @@ public static long fetchAppUsageTodayTillNow(@NonNull UsageStatsManager usageSta long start = midNightCal.getTimeInMillis(); long end = System.currentTimeMillis(); - UsageEvents usageEvents = usageStatsManager.queryEvents(start, end); - UsageEvents.Event lastAppResumeEvent = null; - long screenTime = 0; - - - while (usageEvents.hasNextEvent()) { - UsageEvents.Event currentEvent = new UsageEvents.Event(); // Do not move this from while loop - usageEvents.getNextEvent(currentEvent); - - // Skip it if the event does not belong to the specified app - if (!currentEvent.getPackageName().equals(packageName)) continue; - - int eventType = currentEvent.getEventType(); - if (eventType == UsageEvents.Event.ACTIVITY_RESUMED) { - lastAppResumeEvent = currentEvent; - } else if (eventType == UsageEvents.Event.ACTIVITY_PAUSED) { - if (lastAppResumeEvent != null) { - screenTime += (currentEvent.getTimeStamp() - lastAppResumeEvent.getTimeStamp()); - } else { - screenTime += (currentEvent.getTimeStamp() - start); - } - } - } - - // If the app is still open (no PAUSED event yet), calculate time till `end` - if (lastAppResumeEvent != null && lastAppResumeEvent.getTimeStamp() < end) { - screenTime += (end - lastAppResumeEvent.getTimeStamp()); - } + long screenTime = fetchUsageForInterval(usageStatsManager, start, end, packageName).getOrDefault(packageName, 0L); - Log.d("Time", "fetchAppUsageFromEvents: package: " + packageName + " screen time seconds : " + (screenTime / 1000)); - return (screenTime / 1000); + Log.d("Time", "fetchAppUsageFromEvents: package: " + packageName + " screen time seconds : " + screenTime); + return screenTime; } } diff --git a/android/app/src/main/java/com/mindful/android/helpers/SharedPrefsHelper.java b/android/app/src/main/java/com/mindful/android/helpers/SharedPrefsHelper.java index cbeeaae..89de025 100644 --- a/android/app/src/main/java/com/mindful/android/helpers/SharedPrefsHelper.java +++ b/android/app/src/main/java/com/mindful/android/helpers/SharedPrefsHelper.java @@ -12,6 +12,8 @@ package com.mindful.android.helpers; +import static com.mindful.android.services.EmergencyPauseService.DEFAULT_EMERGENCY_PASSES_COUNT; + import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; @@ -20,7 +22,6 @@ import com.mindful.android.models.BedtimeSettings; import com.mindful.android.models.WellBeingSettings; -import com.mindful.android.utils.AppConstants; import com.mindful.android.utils.Utils; import org.jetbrains.annotations.Contract; @@ -66,8 +67,8 @@ public static String fetchLocale(Context context) { /** * Stores the current locale * - * @param context The application context. - * @param languageCode The language code for locale. + * @param context The application context. + * @param languageCode The language code for locale. */ public static void storeLocale(Context context, String languageCode) { checkAndInitializePrefs(context); @@ -114,7 +115,10 @@ public static void registerListener(Context context, SharedPreferences.OnSharedP */ public static void unregisterListener(Context context, SharedPreferences.OnSharedPreferenceChangeListener callback) { checkAndInitializePrefs(context); - mSharedPrefs.unregisterOnSharedPreferenceChangeListener(callback); + try { + mSharedPrefs.unregisterOnSharedPreferenceChangeListener(callback); + } catch (Exception ignored) { + } } /** @@ -235,7 +239,7 @@ public static int fetchNotificationAskCount(@NonNull Context context) { */ public static int fetchEmergencyPassesCount(@NonNull Context context) { if (mSharedPrefs == null) checkAndInitializePrefs(context); - return mSharedPrefs.getInt(PREF_KEY_EMERGENCY_PASSES_COUNT, AppConstants.DEFAULT_EMERGENCY_PASSES_COUNT); + return mSharedPrefs.getInt(PREF_KEY_EMERGENCY_PASSES_COUNT, DEFAULT_EMERGENCY_PASSES_COUNT); } /** diff --git a/android/app/src/main/java/com/mindful/android/models/AggregatedUsage.java b/android/app/src/main/java/com/mindful/android/models/AggregatedUsage.java new file mode 100644 index 0000000..741a810 --- /dev/null +++ b/android/app/src/main/java/com/mindful/android/models/AggregatedUsage.java @@ -0,0 +1,25 @@ +/* + * + * * + * * * Copyright (c) 2024 Mindful (https://github.com/akaMrNagar/Mindful) + * * * Author : Pawan Nagar (https://github.com/akaMrNagar) + * * * + * * * This source code is licensed under the GPL-2.0 license license found in the + * * * LICENSE file in the root directory of this source tree. + * * + * + */ + +package com.mindful.android.models; + +public class AggregatedUsage { + public final int totalScreenUsageMins; + public final int totalMobileUsageMBs; + public final int totalWifiUsageMBs; + + public AggregatedUsage(int totalScreenUsageMins, int totalMobileUsageMBs, int totalWifiUsageMBs) { + this.totalScreenUsageMins = totalScreenUsageMins; + this.totalMobileUsageMBs = totalMobileUsageMBs; + this.totalWifiUsageMBs = totalWifiUsageMBs; + } +} diff --git a/android/app/src/main/java/com/mindful/android/receivers/DeviceBootReceiver.java b/android/app/src/main/java/com/mindful/android/receivers/DeviceBootReceiver.java index a94604c..411e181 100644 --- a/android/app/src/main/java/com/mindful/android/receivers/DeviceBootReceiver.java +++ b/android/app/src/main/java/com/mindful/android/receivers/DeviceBootReceiver.java @@ -16,9 +16,11 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.os.Build; import android.util.Log; -import com.mindful.android.generics.SafeServiceConnection; +import androidx.annotation.NonNull; + import com.mindful.android.helpers.AlarmTasksSchedulingHelper; import com.mindful.android.helpers.SharedPrefsHelper; import com.mindful.android.services.MindfulTrackerService; @@ -44,23 +46,43 @@ public void onReceive(Context context, Intent intent) { // Fetch bedtime settings to check if the bedtime schedule is on boolean isBedtimeScheduleOn = SharedPrefsHelper.fetchBedtimeSettings(context).isScheduleOn; + // Start needful services + startServices(context); + + // Reschedule bedtime workers if the bedtime schedule is on + if (isBedtimeScheduleOn) AlarmTasksSchedulingHelper.scheduleBedtimeStartTask(context); + + // Reschedule midnight reset worker + AlarmTasksSchedulingHelper.scheduleMidnightResetTask(context, false); + } + } + + private void startServices(@NonNull Context context) { + try { // Start the MindfulTrackerService if needed if (!SharedPrefsHelper.fetchAppTimers(context).isEmpty()) { - SafeServiceConnection mTrackerServiceConn = new SafeServiceConnection<>(MindfulTrackerService.class, context); - mTrackerServiceConn.startOnly(MindfulTrackerService.ACTION_START_SERVICE_TIMER_MODE); + Intent serviceIntent = new Intent(context, MindfulTrackerService.class); + serviceIntent.setAction(MindfulTrackerService.ACTION_START_SERVICE_TIMER_MODE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(serviceIntent); + } else { + context.startService(serviceIntent); + } } // Start the MindfulVpnService if needed if (!SharedPrefsHelper.fetchBlockedApps(context).isEmpty() && MindfulVpnService.prepare(context) == null) { - SafeServiceConnection mVpnServiceConn = new SafeServiceConnection<>(MindfulVpnService.class, context); - mVpnServiceConn.startOnly(MindfulVpnService.ACTION_START_SERVICE_VPN); - } + Intent serviceIntent = new Intent(context, MindfulVpnService.class); + serviceIntent.setAction(MindfulVpnService.ACTION_START_SERVICE_VPN); - // Reschedule bedtime workers if the bedtime schedule is on - if (isBedtimeScheduleOn) AlarmTasksSchedulingHelper.scheduleBedtimeStartTask(context); - - // Reschedule midnight reset worker - AlarmTasksSchedulingHelper.scheduleMidnightResetTask(context, false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(serviceIntent); + } else { + context.startService(serviceIntent); + } + } + } catch (Exception ignored) { } } } diff --git a/android/app/src/main/java/com/mindful/android/receivers/alarm/MidnightResetReceiver.java b/android/app/src/main/java/com/mindful/android/receivers/alarm/MidnightResetReceiver.java index 7de3edb..ac53cb4 100644 --- a/android/app/src/main/java/com/mindful/android/receivers/alarm/MidnightResetReceiver.java +++ b/android/app/src/main/java/com/mindful/android/receivers/alarm/MidnightResetReceiver.java @@ -12,6 +12,8 @@ package com.mindful.android.receivers.alarm; +import static com.mindful.android.services.EmergencyPauseService.DEFAULT_EMERGENCY_PASSES_COUNT; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -23,7 +25,6 @@ import com.mindful.android.helpers.SharedPrefsHelper; import com.mindful.android.services.MindfulAccessibilityService; import com.mindful.android.services.MindfulTrackerService; -import com.mindful.android.utils.AppConstants; import com.mindful.android.utils.Utils; public class MidnightResetReceiver extends BroadcastReceiver { @@ -36,29 +37,35 @@ public class MidnightResetReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, @NonNull Intent intent) { if (ACTION_START_MIDNIGHT_RESET.equals(intent.getAction())) { + try { + onMidnightReset(context); + Log.d(TAG, "onReceive: Midnight reset task completed successfully"); + } catch (Exception ignored) { + } - // Reset emergency passes count to default value - SharedPrefsHelper.storeEmergencyPassesCount(context, AppConstants.DEFAULT_EMERGENCY_PASSES_COUNT); + // Schedule task for next day + AlarmTasksSchedulingHelper.scheduleMidnightResetTask(context, false); + } + } - // Let tracking service know about midnight reset - if (Utils.isServiceRunning(context, MindfulTrackerService.class.getName())) { - Intent serviceIntent = new Intent(context, MindfulTrackerService.class).setAction(ACTION_MIDNIGHT_SERVICE_RESET); - context.startService(serviceIntent); - } - // Let accessibility service know about midnight reset - if (Utils.isServiceRunning(context, MindfulAccessibilityService.class.getName())) { - Intent serviceIntent = new Intent(context, MindfulAccessibilityService.class).setAction(ACTION_MIDNIGHT_SERVICE_RESET); - context.startService(serviceIntent); - } else { - // Else at least reset short content screen time - SharedPrefsHelper.storeShortsScreenTimeMs(context, 0); - } + private void onMidnightReset(@NonNull Context context) { + // Reset emergency passes count to default value + SharedPrefsHelper.storeEmergencyPassesCount(context, DEFAULT_EMERGENCY_PASSES_COUNT); - Log.d(TAG, "onReceive: Midnight reset task completed successfully"); + // Let tracking service know about midnight reset + if (Utils.isServiceRunning(context, MindfulTrackerService.class.getName())) { + Intent serviceIntent = new Intent(context, MindfulTrackerService.class).setAction(ACTION_MIDNIGHT_SERVICE_RESET); + context.startService(serviceIntent); + } - // Schedule task for next day - AlarmTasksSchedulingHelper.scheduleMidnightResetTask(context, false); + // Let accessibility service know about midnight reset + if (Utils.isServiceRunning(context, MindfulAccessibilityService.class.getName())) { + Intent serviceIntent = new Intent(context, MindfulAccessibilityService.class).setAction(ACTION_MIDNIGHT_SERVICE_RESET); + context.startService(serviceIntent); + } else { + // Else at least reset short content screen time + SharedPrefsHelper.storeShortsScreenTimeMs(context, 0); } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/mindful/android/services/EmergencyPauseService.java b/android/app/src/main/java/com/mindful/android/services/EmergencyPauseService.java index ddf17a3..ac6e760 100644 --- a/android/app/src/main/java/com/mindful/android/services/EmergencyPauseService.java +++ b/android/app/src/main/java/com/mindful/android/services/EmergencyPauseService.java @@ -12,7 +12,6 @@ package com.mindful.android.services; -import static com.mindful.android.utils.AppConstants.DEFAULT_EMERGENCY_PASS_PERIOD_MS; import static com.mindful.android.utils.AppConstants.EMERGENCY_PAUSE_SERVICE_NOTIFICATION_ID; import android.app.Notification; @@ -36,6 +35,8 @@ import com.mindful.android.utils.Utils; public class EmergencyPauseService extends Service { + public static final int DEFAULT_EMERGENCY_PASSES_COUNT = 3; + private static final int DEFAULT_EMERGENCY_PASS_PERIOD_MS = 5 * 60 * 1000; private static final String TAG = "Mindful.EmergencyPauseService"; public static final String ACTION_START_SERVICE_EMERGENCY = "com.mindful.android.EmergencyPauseService.START_SERVICE_EMERGENCY"; diff --git a/android/app/src/main/java/com/mindful/android/services/MindfulAccessibilityService.java b/android/app/src/main/java/com/mindful/android/services/MindfulAccessibilityService.java index d2caddd..3ad6c67 100644 --- a/android/app/src/main/java/com/mindful/android/services/MindfulAccessibilityService.java +++ b/android/app/src/main/java/com/mindful/android/services/MindfulAccessibilityService.java @@ -96,7 +96,7 @@ public class MindfulAccessibilityService extends AccessibilityService implements ":id/bro_omnibar_address_title_text" )); - private final AppInstallUninstallReceiver mAppInstallUninstallReceiver = new AppInstallUninstallReceiver(); + private AppInstallUninstallReceiver mAppInstallUninstallReceiver; private WellBeingSettings mWellBeingSettings = new WellBeingSettings(); private Map mNsfwWebsites = new HashMap<>(); private String mLastRedirectedUrl = ""; @@ -129,11 +129,14 @@ protected void onServiceConnected() { mTotalShortsScreenTimeMs = SharedPrefsHelper.fetchShortsScreenTimeMs(this); // Register listener for install and uninstall events - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_PACKAGE_ADDED); - filter.addAction(Intent.ACTION_PACKAGE_REMOVED); - filter.addDataScheme("package"); - registerReceiver(mAppInstallUninstallReceiver, filter); + if (mAppInstallUninstallReceiver == null) { + mAppInstallUninstallReceiver = new AppInstallUninstallReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + registerReceiver(mAppInstallUninstallReceiver, filter); + } // Start timer to reset shortsScreen time everyday midnight Calendar calendar = Calendar.getInstance(); @@ -433,7 +436,10 @@ public void onInterrupt() { public void onDestroy() { super.onDestroy(); // Unregister prefs listener and receiver - unregisterReceiver(mAppInstallUninstallReceiver); + if (mAppInstallUninstallReceiver != null) { + unregisterReceiver(mAppInstallUninstallReceiver); + mAppInstallUninstallReceiver = null; + } SharedPrefsHelper.unregisterListener(this, this); Log.d(TAG, "onDestroy: Accessibility service destroyed"); } diff --git a/android/app/src/main/java/com/mindful/android/utils/AppConstants.java b/android/app/src/main/java/com/mindful/android/utils/AppConstants.java index 33a540e..e2679db 100644 --- a/android/app/src/main/java/com/mindful/android/utils/AppConstants.java +++ b/android/app/src/main/java/com/mindful/android/utils/AppConstants.java @@ -15,11 +15,6 @@ public class AppConstants { public static final String FLUTTER_METHOD_CHANNEL = "com.mindful.android.methodchannel"; - public static final int DEFAULT_EMERGENCY_PASSES_COUNT = 3; - public static final int DEFAULT_EMERGENCY_PASS_PERIOD_MS = 5 * 60 * 1000; - public static final long WIDGET_MANUAL_REFRESH_INTERVAL = 60 * 1000; // 1 minute - - /// Notification IDs public static final int TRACKER_SERVICE_NOTIFICATION_ID = 101; public static final int VPN_SERVICE_NOTIFICATION_ID = 102; diff --git a/android/app/src/main/java/com/mindful/android/widgets/DeviceUsageWidget.java b/android/app/src/main/java/com/mindful/android/widgets/DeviceUsageWidget.java index 64cda93..b4e3acc 100644 --- a/android/app/src/main/java/com/mindful/android/widgets/DeviceUsageWidget.java +++ b/android/app/src/main/java/com/mindful/android/widgets/DeviceUsageWidget.java @@ -23,6 +23,8 @@ import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.widget.RemoteViews; @@ -30,12 +32,15 @@ import com.mindful.android.MainActivity; import com.mindful.android.R; +import com.mindful.android.generics.SuccessCallback; import com.mindful.android.helpers.NetworkUsageHelper; import com.mindful.android.helpers.ScreenUsageHelper; import com.mindful.android.helpers.SharedPrefsHelper; -import com.mindful.android.utils.AppConstants; +import com.mindful.android.models.AggregatedUsage; import com.mindful.android.utils.Utils; +import org.jetbrains.annotations.Contract; + import java.util.Calendar; import java.util.HashMap; import java.util.List; @@ -48,24 +53,22 @@ * Clicking the widget opens the Mindful app. */ public class DeviceUsageWidget extends AppWidgetProvider { + private static final long WIDGET_MANUAL_REFRESH_INTERVAL = 60 * 1000; // 1 minute private static long lastRefreshedTime = 0L; private static final String TAG = "Mindful.DeviceUsageWidget"; private static final String WIDGET_ACTION_REFRESH = "com.mindful.android.ACTION_REFRESH"; private static final String WIDGET_ACTION_LAUNCH_APP = "com.mindful.android.ACTION_LAUNCH_APP"; - private int totalScreenUsageMins = 0; - private int totalMobileUsageMBs = 0; - private int totalWifiUsageMBs = 0; - @Override - public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); - Log.d(TAG, "onReceive: Received event with action: " + intent.getAction()); - if (WIDGET_ACTION_REFRESH.equals(intent.getAction())) { + String action = Utils.getActionFromIntent(intent); + Log.d(TAG, "onReceive: Received event with action: " + action); + + if (WIDGET_ACTION_REFRESH.equals(action)) { // Return if interval between last refreshed time is less than the specified interval long currentTime = System.currentTimeMillis(); - if ((currentTime - lastRefreshedTime) <= AppConstants.WIDGET_MANUAL_REFRESH_INTERVAL) { + if ((currentTime - lastRefreshedTime) <= WIDGET_MANUAL_REFRESH_INTERVAL) { return; } @@ -73,14 +76,14 @@ public void onReceive(Context context, Intent intent) { AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); ComponentName widgetComponent = new ComponentName(context, DeviceUsageWidget.class); int[] appWidgetIds = appWidgetManager.getAppWidgetIds(widgetComponent); - updateWidget(context, appWidgetManager, appWidgetIds, false); + updateWidgetAsync(context, appWidgetManager, appWidgetIds, false); } } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, @NonNull int[] appWidgetIds) { - updateWidget(context, appWidgetManager, appWidgetIds, true); + updateWidgetAsync(context, appWidgetManager, appWidgetIds, true); } /** @@ -91,47 +94,71 @@ public void onUpdate(Context context, AppWidgetManager appWidgetManager, @NonNul * @param appWidgetIds The list of widget IDs to update. * @param isAutomatic Indicates if the update is triggered automatically (by system) or manually. */ - private void updateWidget(@NonNull Context context, @NonNull AppWidgetManager appWidgetManager, @NonNull int[] appWidgetIds, boolean isAutomatic) { - updateUsageData(context); - - RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.device_usage_widget_layout); - views.setTextViewText(R.id.widgetScreenUsage, Utils.formatScreenTime(totalScreenUsageMins)); - views.setTextViewText(R.id.widgetMobileUsage, Utils.formatDataMBs(totalMobileUsageMBs)); - views.setTextViewText(R.id.widgetWifiUsage, Utils.formatDataMBs(totalWifiUsageMBs)); - - // Called by system. It may be first time so we need to attach onClick listeners - if (isAutomatic) { - Intent refreshIntent = new Intent(context, DeviceUsageWidget.class); - refreshIntent.setAction(WIDGET_ACTION_REFRESH); - PendingIntent refreshPendingIntent = PendingIntent.getBroadcast(context, 0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - - Intent launchIntent = new Intent(context, MainActivity.class); - launchIntent.setAction(WIDGET_ACTION_LAUNCH_APP); - PendingIntent launchPendingIntent = PendingIntent.getActivity(context, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - - views.setOnClickPendingIntent(R.id.widgetRefreshIcon, refreshPendingIntent); - views.setOnClickPendingIntent(R.id.widgetRoot, launchPendingIntent); - } + private void updateWidgetAsync(@NonNull Context context, @NonNull AppWidgetManager appWidgetManager, @NonNull int[] appWidgetIds, boolean isAutomatic) { + // Async callback to run when usages are fetched successfully + SuccessCallback callback = new SuccessCallback() { + @Override + public void onSuccess(@NonNull AggregatedUsage result) { + // Ensure UI updates run on the main thread + new Handler(Looper.getMainLooper()).post(() -> { + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.device_usage_widget_layout); + views.setTextViewText(R.id.widgetScreenUsage, Utils.formatScreenTime(result.totalScreenUsageMins)); + views.setTextViewText(R.id.widgetMobileUsage, Utils.formatDataMBs(result.totalMobileUsageMBs)); + views.setTextViewText(R.id.widgetWifiUsage, Utils.formatDataMBs(result.totalWifiUsageMBs)); + + // Called by system. It may be first time so we need to attach onClick listeners + if (isAutomatic) { + setUpClickListener(context, views); + } + + for (int appWidgetId : appWidgetIds) { + if (isAutomatic) { + appWidgetManager.updateAppWidget(appWidgetId, views); + } else { + appWidgetManager.partiallyUpdateAppWidget(appWidgetId, views); + } + } + + Log.d(TAG, "updateWidgetAsync: Usage widget updated successfully"); + }); + } + }; - for (int appWidgetId : appWidgetIds) { - if (isAutomatic) { - appWidgetManager.updateAppWidget(appWidgetId, views); - } else { - appWidgetManager.partiallyUpdateAppWidget(appWidgetId, views); + + // Fetch usages on background thread and use callback to update widget data + new Thread(new Runnable() { + @Override + public void run() { + AggregatedUsage usage = fetchAggregatedUsage(context); + callback.onSuccess(usage); } - } + }).start(); + } + + private void setUpClickListener(Context context, @NonNull RemoteViews views) { + Intent refreshIntent = new Intent(context, DeviceUsageWidget.class); + refreshIntent.setAction(WIDGET_ACTION_REFRESH); + PendingIntent refreshPendingIntent = PendingIntent.getBroadcast(context, 0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + Intent launchIntent = new Intent(context, MainActivity.class); + launchIntent.setAction(WIDGET_ACTION_LAUNCH_APP); + PendingIntent launchPendingIntent = PendingIntent.getActivity(context, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - Log.d(TAG, "updateManually: Usage widget updated successfully"); + views.setOnClickPendingIntent(R.id.widgetRefreshIcon, refreshPendingIntent); + views.setOnClickPendingIntent(R.id.widgetRoot, launchPendingIntent); } /** * Fetches device usage data for the current day only from launchable apps, including screen usage, mobile data usage, and wifi data usage. - * Calculates total usage for each category and stores them in class variables. + * Calculates total usage for each category and stores them in AggregatedUsage model and returns it. * * @param context The context of the application. + * @return AggregatedUsage model with latest usage. */ - private void updateUsageData(@NonNull Context context) { + @NonNull + @Contract("_ -> new") + private AggregatedUsage fetchAggregatedUsage(@NonNull Context context) { Calendar screenUsageCal = Calendar.getInstance(); screenUsageCal.set(Calendar.HOUR_OF_DAY, 0); screenUsageCal.set(Calendar.MINUTE, 0); @@ -153,7 +180,7 @@ private void updateUsageData(@NonNull Context context) { UsageStatsManager usageStatsManager = (UsageStatsManager) context.getApplicationContext().getSystemService(Context.USAGE_STATS_SERVICE); NetworkStatsManager networkStatsManager = (NetworkStatsManager) context.getApplicationContext().getSystemService(Context.NETWORK_STATS_SERVICE); - HashMap screenUsageOneDay = ScreenUsageHelper.fetchUsageForInterval(usageStatsManager, screenUsageStart, screenUsageStart + ms24Hours); + HashMap screenUsageOneDay = ScreenUsageHelper.fetchUsageForInterval(usageStatsManager, screenUsageStart, screenUsageStart + ms24Hours, null); HashMap mobileUsageOneDay = NetworkUsageHelper.fetchMobileUsageForInterval(networkStatsManager, dataUsageStart, dataUsageStart + ms24Hours); HashMap wifiUsageOneDay = NetworkUsageHelper.fetchWifiUsageForInterval(networkStatsManager, dataUsageStart, dataUsageStart + ms24Hours); @@ -184,8 +211,10 @@ private void updateUsageData(@NonNull Context context) { wifiUsageKbs += wifiUsageOneDay.getOrDefault(NetworkStats.Bucket.UID_REMOVED, 0L); mobileUsageKbs += mobileUsageOneDay.getOrDefault(NetworkStats.Bucket.UID_REMOVED, 0L); - totalMobileUsageMBs = Math.toIntExact(mobileUsageKbs / 1024); - totalWifiUsageMBs = Math.toIntExact(wifiUsageKbs / 1024); - totalScreenUsageMins = Math.toIntExact(screenTimeSec / 60); + return new AggregatedUsage( + Math.toIntExact(screenTimeSec / 60), + Math.toIntExact(mobileUsageKbs / 1024), + Math.toIntExact(wifiUsageKbs / 1024) + ); } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index efa43b2..2cf15e6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,18 +19,16 @@ import 'package:mindful/mindful_app.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - /// Initialize method channel - await MethodChannelService.instance.init(); - - /// Initialize isar database service - await IsarDbService.instance.init(); - /// Initialize local crashlytics FlutterError.onError = (errorDetails) { CrashLogService.instance.recordCrashError( errorDetails.exception.toString(), errorDetails.stack.toString(), ); + + if (kDebugMode) { + FlutterError.presentError(errorDetails); + } }; PlatformDispatcher.instance.onError = (error, stack) { @@ -41,6 +39,12 @@ Future main() async { return true; }; + /// Initialize method channel + await MethodChannelService.instance.init(); + + /// Initialize isar database service + await IsarDbService.instance.init(); + /// run main app runApp( const ProviderScope( diff --git a/pubspec.lock b/pubspec.lock index e867746..1bcda4c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: crypto - sha256: "1dceb0cf05cb63a7852c11560060e53ec2f182079a16ced6f4395c5b0875baf8" + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.5" dart_style: dependency: transitive description: @@ -237,10 +237,10 @@ packages: dependency: "direct main" description: name: fluentui_system_icons - sha256: af92e0abc8a4060ffdcae2ad31a050cd242bf9eff121769b9cfb11fe05d08d6c + sha256: "3a84a29fb45d5e38996889a1f826f98f4bbd6cc6446bc367eb782acbf85d6c55" url: "https://pub.dev" source: hosted - version: "1.1.252" + version: "1.1.259" flutter: dependency: "direct main" description: flutter @@ -295,10 +295,10 @@ packages: dependency: transitive description: name: flutter_shaders - sha256: "02750b545c01ff4d8e9bbe8f27a7731aa3778402506c67daa1de7f5fc3f4befe" + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.1.3" flutter_test: dependency: "direct dev" description: flutter @@ -468,10 +468,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" package_config: dependency: transitive description: @@ -753,10 +753,10 @@ packages: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" web_socket: dependency: transitive description: @@ -777,18 +777,18 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xxh3: dependency: transitive description: name: xxh3 - sha256: a92b30944a9aeb4e3d4f3c3d4ddb3c7816ca73475cd603682c4f8149690f56d7 + sha256: cbeb0e1d10f4c6bf67b650f395eac0cc689425b5efc2ba0cc3d3e069a0beaeec url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" yaml: dependency: transitive description: