Skip to content

Commit

Permalink
Merge pull request #24 from akaMrNagar/dev
Browse files Browse the repository at this point in the history
Feat: Notification grouping and minor improvements.
  • Loading branch information
akaMrNagar authored Jan 3, 2025
2 parents d018e86 + 9c5cee9 commit 124eb7e
Show file tree
Hide file tree
Showing 41 changed files with 629 additions and 298 deletions.
32 changes: 17 additions & 15 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,34 +45,36 @@ android {
}


signingConfigs {
release {
if (System.getenv("KEYSTORE_FILE") != null) {
storeFile file(System.getenv("KEYSTORE_FILE"))
storePassword System.getenv("STORE_PASSWORD")
keyAlias System.getenv("KEY_ALIAS")
keyPassword System.getenv("KEY_PASSWORD")
}
}
}

buildTypes {
release {
ndk {
debugSymbolLevel 'full'
}
if (System.getenv("KEYSTORE_FILE") != null) {
signingConfigs {
register("release") {
storeFile file(System.getenv("KEYSTORE_FILE"))
storePassword System.getenv("STORE_PASSWORD")
keyAlias System.getenv("KEY_ALIAS")
keyPassword System.getenv("KEY_PASSWORD")
}
}
signingConfig = signingConfigs.release
} else {
signingConfig = signingConfigs.debug
}
resValue "string", "app_name", "Mindful"
signingConfig = signingConfigs.release
}

debug {
applicationIdSuffix ".debug"
signingConfig signingConfigs.debug
resValue "string", "app_name", "Mindful Debug"
signingConfig = signingConfigs.release ?: signingConfigs.debug
}

profile {
applicationIdSuffix ".profile"
signingConfig signingConfigs.debug
resValue "string", "app_name", "Mindful Profile"
signingConfig = signingConfigs.release ?: signingConfigs.debug
}
}
buildFeatures {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
Intent currentIntent = getIntent();
Map<String, Object> intentData = new HashMap<>();
intentData.put("route", currentIntent.getStringExtra(INTENT_EXTRA_INITIAL_ROUTE));
intentData.put("targetedPackage", currentIntent.getStringExtra(INTENT_EXTRA_PACKAGE_NAME));
intentData.put("isSelfRestart", currentIntent.getBooleanExtra(INTENT_EXTRA_IS_SELF_RESTART, false));
intentData.put("extraPackageName", currentIntent.getStringExtra(INTENT_EXTRA_PACKAGE_NAME));
intentData.put("extraIsSelfStart", currentIntent.getBooleanExtra(INTENT_EXTRA_IS_SELF_RESTART, false));

// Update intent data on flutter side
mMethodChannel.invokeMethod("updateIntentData", intentData);
Expand Down Expand Up @@ -160,7 +160,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
break;
}
case "getUpComingNotifications": {
result.success(SharedPrefsHelper.getUpComingNotificationsArrayString(this));
result.success(SharedPrefsHelper.getSerializedNotificationsJson(this));
break;
}
case "getShortsScreenTimeMs": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@
import org.json.JSONArray;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* Helper class to manage SharedPreferences operations.
Expand Down Expand Up @@ -352,6 +355,23 @@ public static HashSet<Integer> getSetNotificationBatchSchedules(@NonNull Context
}
}

/**
* Fetches the hashset of notification batched apps if jsonBatchedApps is null else store it's json.
*
* @param context The application context.
* @param jsonBatchedApps The JSON string of notification batched apps.
*/
@NonNull
public static HashSet<String> getSetNotificationBatchedApps(@NonNull Context context, @Nullable String jsonBatchedApps) {
checkAndInitializeNotificationBatchPrefs(context);
if (jsonBatchedApps == null) {
return JsonDeserializer.jsonStrToStringHashSet(mNotificationBatchPrefs.getString(PREF_KEY_BATCHED_APPS, ""));
} else {
mNotificationBatchPrefs.edit().putString(PREF_KEY_BATCHED_APPS, jsonBatchedApps).apply();
return JsonDeserializer.jsonStrToStringHashSet(jsonBatchedApps);
}
}

/**
* Creates and Inserts a new notification into SharedPreferences based on the passed object.
*
Expand All @@ -361,7 +381,7 @@ public static HashSet<Integer> getSetNotificationBatchSchedules(@NonNull Context
public static void insertNotificationToPrefs(@NonNull Context context, @NonNull UpcomingNotification notification) {
checkAndInitializeNotificationBatchPrefs(context);

// Create new notification object
// Create new json object
JSONObject currentNotification = new JSONObject(notification.toMap());

// Get existing notifications
Expand All @@ -379,7 +399,6 @@ public static void insertNotificationToPrefs(@NonNull Context context, @NonNull
notificationsArray.remove(i);
}
}

} catch (Exception e1) {
notificationsArray = new JSONArray();
}
Expand All @@ -397,26 +416,9 @@ public static void insertNotificationToPrefs(@NonNull Context context, @NonNull
* @return A JSON string representing the stored notifications array.
*/
@NonNull
public static String getUpComingNotificationsArrayString(@NonNull Context context) {
public static String getSerializedNotificationsJson(@NonNull Context context) {
checkAndInitializeNotificationBatchPrefs(context);
return mNotificationBatchPrefs.getString(PREF_KEY_UPCOMING_NOTIFICATIONS, "[]");
}

/**
* Fetches the hashset of notification batched apps if jsonBatchedApps is null else store it's json.
*
* @param context The application context.
* @param jsonBatchedApps The JSON string of notification batched apps.
*/
@NonNull
public static HashSet<String> getSetNotificationBatchedApps(@NonNull Context context, @Nullable String jsonBatchedApps) {
checkAndInitializeNotificationBatchPrefs(context);
if (jsonBatchedApps == null) {
return JsonDeserializer.jsonStrToStringHashSet(mNotificationBatchPrefs.getString(PREF_KEY_BATCHED_APPS, ""));
} else {
mNotificationBatchPrefs.edit().putString(PREF_KEY_BATCHED_APPS, jsonBatchedApps).apply();
return JsonDeserializer.jsonStrToStringHashSet(jsonBatchedApps);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ public class UpcomingNotification {
public final String contentText;
public final long timeStamp;

// Constructor that initializes the object using StatusBarNotification
/**
* Constructor that initializes the object using StatusBarNotification
*
* @param sbn Status Bar Notification object
*/
public UpcomingNotification(@NonNull StatusBarNotification sbn) {
Bundle extras = sbn.getNotification().extras;
this.packageName = sbn.getPackageName();
this.titleText = extras.getString(Notification.EXTRA_TITLE, "Null").trim();
this.contentText = extras.getString(Notification.EXTRA_TEXT, "Null").trim();
this.timeStamp = sbn.getPostTime();
this.titleText = extras.getCharSequence(Notification.EXTRA_TITLE, "").toString().trim();
this.contentText = extras.getCharSequence(Notification.EXTRA_TEXT, "").toString().trim();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import com.mindful.android.R;
import com.mindful.android.helpers.AlarmTasksSchedulingHelper;
import com.mindful.android.helpers.SharedPrefsHelper;
import com.mindful.android.utils.AppConstants;
import com.mindful.android.utils.Utils;

import org.json.JSONArray;
Expand Down Expand Up @@ -56,7 +55,7 @@ public NotificationBatchWorker(@NonNull Context context, @NonNull WorkerParamete
public Result doWork() {
try {
// Return if no available notifications
String jsonStr = SharedPrefsHelper.getUpComingNotificationsArrayString(mContext);
String jsonStr = SharedPrefsHelper.getSerializedNotificationsJson(mContext);
int notificationsCount = new JSONArray(jsonStr).length();
if (notificationsCount == 0) return Result.success();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,15 @@ public class MindfulAccessibilityService extends AccessibilityService implements
* These are used to retrieve/extract url from the browsers.
*/
private final HashSet<String> mUrlBarNodeIds = new HashSet<>(Set.of(
":id/url_bar",
":id/mozac_browser_toolbar_url_view",
":id/url_bar", // Chrome
":id/mozac_browser_toolbar_url_view", // Firefox
":id/url",
":id/search",
":id/url_field",
":id/location_bar_edit_text",
":id/addressbarEdit",
":id/bro_omnibar_address_title_text"
":id/bro_omnibar_address_title_text",
":id/cbn_tv_title" // Quetta Browser
));

// Fixed thread pool for parallel event processing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,39 +21,41 @@
import android.service.notification.StatusBarNotification;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.mindful.android.generics.ServiceBinder;
import com.mindful.android.helpers.SharedPrefsHelper;
import com.mindful.android.models.UpcomingNotification;
import com.mindful.android.utils.Utils;

import org.jetbrains.annotations.Contract;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;


public class MindfulNotificationListenerService extends NotificationListenerService {
private final String TAG = "Mindful.MindfulNotificationService";
private final ServiceBinder<MindfulNotificationListenerService> mBinder = new ServiceBinder<>(MindfulNotificationListenerService.this);
private final Set<String> mSocialMediaPackages = Set.of("com.whatsapp", "com.instagram.android", "com.snapchat.android");

private HashSet<String> mDistractingApps = new HashSet<>(0);
private boolean mIsListenerActive = false;

@Override
public void onListenerConnected() {
super.onListenerConnected();
mDistractingApps = SharedPrefsHelper.getSetNotificationBatchedApps(this, null);

//
SharedPrefsHelper.insertCrashLogToPrefs(this, new Throwable("MindfulNotificationListenerService is CONNECTED by system"));
mIsListenerActive = true;
}

@Override
public void onListenerDisconnected() {
super.onListenerDisconnected();
mIsListenerActive = false;

//
SharedPrefsHelper.insertCrashLogToPrefs(this, new Throwable("MindfulNotificationListenerService is DIS-CONNECTED by system"));
mIsListenerActive = false;
}

@Override
Expand All @@ -62,45 +64,51 @@ public void onNotificationPosted(StatusBarNotification sbn) {
if (!mIsListenerActive) return;
String packageName = sbn.getPackageName();
try {

// Return if the posting app is not marked as distracting
if (!mDistractingApps.contains(packageName) || !sbn.isClearable()) return;


// Dismiss notification
cancelNotification(sbn.getKey());
Log.d(TAG, "onNotificationPosted: Notification dismissed");

// Check if we need to store it or not
if (shouldStoreNotification(packageName, sbn.getTag())) {
UpcomingNotification notification = new UpcomingNotification(sbn);
SharedPrefsHelper.insertNotificationToPrefs(this, notification);
Log.d(TAG, "onNotificationPosted: Notification stored from package: " + packageName);
// Return if it is from social media but does not have tag
if (sbn.getTag() == null && mSocialMediaPackages.contains(packageName)) return;

// Check if we can store it or not
UpcomingNotification notification = new UpcomingNotification(sbn);
if (notification.titleText.isEmpty() || notification.contentText.isEmpty()) {
Log.d(TAG, "onNotificationPosted: Notification is not valid, so skipping it from storing.");
SharedPrefsHelper.insertCrashLogToPrefs(
this,
new Exception("Invalid notification from " + packageName + " with title: " + notification.titleText + " and content: " + notification.contentText)
);
return;
}

SharedPrefsHelper.insertNotificationToPrefs(this, notification);
Log.d(TAG, "onNotificationPosted: Notification stored from package: " + packageName);
} catch (Exception e) {
SharedPrefsHelper.insertCrashLogToPrefs(this, e);
Log.e(TAG, "onNotificationPosted: Something went wrong for package: " + packageName, e);
}
}


@Contract(pure = true)
private boolean shouldStoreNotification(@NonNull String packageName, @Nullable String tag) {
// For whatsapp
if (packageName.equals("com.whatsapp") && tag == null) return false;
return true;
}


public void updateDistractingApps(HashSet<String> distractingApps) {
mDistractingApps = distractingApps;
Log.d(TAG, "updateDistractingApps: Distracting apps updated successfully");
}


@Override
public IBinder onBind(Intent intent) {
String action = Utils.getActionFromIntent(intent);
return action.equals(ACTION_BIND_TO_MINDFUL) ? mBinder : super.onBind(intent);
}

@Override
public void onDestroy() {
super.onDestroy();
SharedPrefsHelper.insertCrashLogToPrefs(this, new Throwable("MindfulNotificationListenerService is DESTROYED"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public class AppConstants {
public static final String FLUTTER_METHOD_CHANNEL = "com.mindful.android.methodchannel";

// Extra intent data
public static final String INTENT_EXTRA_IS_SELF_RESTART = "com.mindful.android.isSelfRestart";
public static final String INTENT_EXTRA_INITIAL_ROUTE = "com.mindful.android.initialRoute";
public static final String INTENT_EXTRA_IS_SELF_RESTART = "com.mindful.android.isSelfRestart";
public static final String INTENT_EXTRA_PACKAGE_NAME = "com.mindful.android.launchedAppPackageName";
public static final String INTENT_EXTRA_DIALOG_INFO = "com.mindful.android.dialogInformation";
public static final String INTENT_EXTRA_MAX_PROGRESS = "com.mindful.android.maxProgress";
Expand Down
1 change: 0 additions & 1 deletion android/app/src/main/res/values-ja/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
~ */
-->
<resources>
<string name="app_name" comment="app_name app values">Mindful</string>
<string name="accessibility_description">Mindful アプリは、ユーザー補助を使用して、ウェブサイトやアプリのショート動画をブロックします。ブロックリストに登録されたURLやコンテンツへのアクセスを制限することで、集中力を維持するのに役立ちます。 \n\n⚠️注意:Mindful はプライバシーを最優先に考えています。100%安全でオフラインで動作します。個人データの収集や保存は一切行いません。 </string>
<string name="admin_description">Mindful は、ユーザーのデータを収集、保存、または送信することはありません。無料でオープンソースのソフトウェア(FOSS)なので、ソースコードを自由に確認・変更できます。管理者権限は、アプリの正常な動作に必要なシステム操作にのみ使用され、プライバシーは完全に保護されます。</string>
<string name="toast_enable_notification">通知を許可する</string>
Expand Down
1 change: 0 additions & 1 deletion android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

<resources>
<!-- app values -->
<string name="app_name" translatable="false">Mindful</string>
<string name="accessibility_description">The Mindful app uses accessibility services to block
websites and short-form content on apps. It checks URLs and content against your blocklist
and prevents access to help you stay focused. \n\n⚠️Note: Your privacy is our priority.
Expand Down
6 changes: 4 additions & 2 deletions lib/core/database/daos/dynamic_records_dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,9 @@ class DynamicRecordsDao extends DatabaseAccessor<AppDatabase>
.go();

/// Loads list of all [CrashLog] objects from the database,
Future<List<CrashLog>> fetchCrashLogs() async => select(crashLogsTable).get();
Future<List<CrashLog>> fetchCrashLogs() async => ((select(crashLogsTable))
..orderBy([(e) => OrderingTerm.desc(e.timeStamp)]))
.get();

/// Clear all [CrashLogs] objects from the database,
Future<int> clearCrashLogs() async => delete(crashLogsTable).go();
Expand Down Expand Up @@ -254,7 +256,7 @@ class DynamicRecordsDao extends DatabaseAccessor<AppDatabase>
..where(
(e) => e.startDateTime.isBetweenValues(start, end),
)
..orderBy([(tbl) => OrderingTerm.desc(tbl.startDateTime)]))
..orderBy([(e) => OrderingTerm.desc(e.startDateTime)]))
.get();

/// Loads the total duration in seconds for all the [FocusSession] in the database for the provided interval
Expand Down
6 changes: 6 additions & 0 deletions lib/core/extensions/ext_date_time.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ extension ExtDateTime on DateTime {
String timeString(BuildContext context) =>
DateFormat.jm(Localizations.localeOf(context).languageCode).format(this);

/// Returns date and time string in a localized format (e.g., 1 Jan 2025, 7:15 PM).
String dateTimeString(BuildContext context) => DateFormat(
'd MMM yyyy, h:mm a',
Localizations.localeOf(context).languageCode,
).format(this);

/// Returns TRUE if the [DateTime] lies between [start] and [end] else false.
bool isBetween(DateTime start, DateTime end) {
return isAfter(start) && isBefore(end);
Expand Down
Loading

0 comments on commit 124eb7e

Please sign in to comment.