test
This commit is contained in:
53
app/src/main/AndroidManifest.xml
Normal file
53
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.NotificationMonitor">
|
||||
|
||||
<!-- Main Activity -->
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- App Selector Activity -->
|
||||
<activity
|
||||
android:name=".ui.AppSelectorActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/select_apps" />
|
||||
|
||||
<!-- Notification Listener Service — requires special permission granted via Settings -->
|
||||
<service
|
||||
android:name=".service.NotificationMonitorService"
|
||||
android:exported="true"
|
||||
android:label="@string/service_label"
|
||||
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.notification.NotificationListenerService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- Boot receiver to restart monitoring after reboot -->
|
||||
<receiver
|
||||
android:name=".service.BootReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
26
app/src/main/java/com/notificationmonitor/data/AppInfo.java
Normal file
26
app/src/main/java/com/notificationmonitor/data/AppInfo.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.notificationmonitor.data;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
/**
|
||||
* Holds metadata about an installed app for display in the selector list.
|
||||
*/
|
||||
public class AppInfo {
|
||||
private final String appName;
|
||||
private final String packageName;
|
||||
private final Drawable icon;
|
||||
private boolean selected;
|
||||
|
||||
public AppInfo(String appName, String packageName, Drawable icon, boolean selected) {
|
||||
this.appName = appName;
|
||||
this.packageName = packageName;
|
||||
this.icon = icon;
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
public String getAppName() { return appName; }
|
||||
public String getPackageName() { return packageName; }
|
||||
public Drawable getIcon() { return icon; }
|
||||
public boolean isSelected() { return selected; }
|
||||
public void setSelected(boolean selected) { this.selected = selected; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.notificationmonitor.data;
|
||||
|
||||
/**
|
||||
* Represents a captured notification entry shown in the log.
|
||||
*/
|
||||
public class NotificationEntry {
|
||||
private final String appName;
|
||||
private final String packageName;
|
||||
private final String title;
|
||||
private final String text;
|
||||
private final long timestamp;
|
||||
|
||||
public NotificationEntry(String appName, String packageName,
|
||||
String title, String text, long timestamp) {
|
||||
this.appName = appName;
|
||||
this.packageName = packageName;
|
||||
this.title = title;
|
||||
this.text = text;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public String getAppName() { return appName; }
|
||||
public String getPackageName() { return packageName; }
|
||||
public String getTitle() { return title != null ? title : ""; }
|
||||
public String getText() { return text != null ? text : ""; }
|
||||
public long getTimestamp() { return timestamp; }
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.notificationmonitor.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Centralised persistence layer using SharedPreferences.
|
||||
* Stores the set of monitored package names and a rolling log of
|
||||
* captured notifications (newest-first, capped at MAX_LOG_SIZE).
|
||||
*/
|
||||
public class PreferencesManager {
|
||||
|
||||
private static final String PREFS_NAME = "nm_prefs";
|
||||
private static final String KEY_PACKAGES = "monitored_packages";
|
||||
private static final String KEY_LOG = "notification_log";
|
||||
private static final int MAX_LOG_SIZE = 200;
|
||||
|
||||
private final SharedPreferences prefs;
|
||||
|
||||
public PreferencesManager(Context context) {
|
||||
prefs = context.getApplicationContext()
|
||||
.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
// ── Monitored packages ────────────────────────────────────────────────
|
||||
|
||||
public Set<String> getMonitoredPackages() {
|
||||
return prefs.getStringSet(KEY_PACKAGES, new HashSet<>());
|
||||
}
|
||||
|
||||
public void saveMonitoredPackages(Set<String> packages) {
|
||||
prefs.edit().putStringSet(KEY_PACKAGES, packages).apply();
|
||||
}
|
||||
|
||||
public boolean isMonitored(String packageName) {
|
||||
return getMonitoredPackages().contains(packageName);
|
||||
}
|
||||
|
||||
// ── Notification log ──────────────────────────────────────────────────
|
||||
|
||||
public List<NotificationEntry> getNotificationLog() {
|
||||
List<NotificationEntry> entries = new ArrayList<>();
|
||||
String json = prefs.getString(KEY_LOG, "[]");
|
||||
try {
|
||||
JSONArray arr = new JSONArray(json);
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
JSONObject o = arr.getJSONObject(i);
|
||||
entries.add(new NotificationEntry(
|
||||
o.optString("appName"),
|
||||
o.optString("packageName"),
|
||||
o.optString("title"),
|
||||
o.optString("text"),
|
||||
o.optLong("timestamp")
|
||||
));
|
||||
}
|
||||
} catch (JSONException ignored) {}
|
||||
return entries;
|
||||
}
|
||||
|
||||
public void addNotificationEntry(NotificationEntry entry) {
|
||||
List<NotificationEntry> entries = getNotificationLog();
|
||||
entries.add(0, entry); // newest first
|
||||
if (entries.size() > MAX_LOG_SIZE) {
|
||||
entries = entries.subList(0, MAX_LOG_SIZE);
|
||||
}
|
||||
saveLog(entries);
|
||||
}
|
||||
|
||||
public void clearLog() {
|
||||
prefs.edit().remove(KEY_LOG).apply();
|
||||
}
|
||||
|
||||
private void saveLog(List<NotificationEntry> entries) {
|
||||
JSONArray arr = new JSONArray();
|
||||
for (NotificationEntry e : entries) {
|
||||
JSONObject o = new JSONObject();
|
||||
try {
|
||||
o.put("appName", e.getAppName());
|
||||
o.put("packageName", e.getPackageName());
|
||||
o.put("title", e.getTitle());
|
||||
o.put("text", e.getText());
|
||||
o.put("timestamp", e.getTimestamp());
|
||||
} catch (JSONException ignored) {}
|
||||
arr.put(o);
|
||||
}
|
||||
prefs.edit().putString(KEY_LOG, arr.toString()).apply();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.notificationmonitor.service;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* Re-enables notification monitoring after device reboot.
|
||||
* The NotificationListenerService itself is managed by the OS, but
|
||||
* this receiver gives us a hook to do any app-level init on boot.
|
||||
*/
|
||||
public class BootReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
|
||||
// The OS automatically re-binds NotificationListenerService
|
||||
// if the user has granted access. Nothing extra needed here,
|
||||
// but this is a good place for future initialisation work.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.notificationmonitor.service;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.NotificationListenerService;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import com.notificationmonitor.data.NotificationEntry;
|
||||
import com.notificationmonitor.data.PreferencesManager;
|
||||
|
||||
/**
|
||||
* Listens for notifications system-wide.
|
||||
* Only processes packages the user has explicitly selected.
|
||||
*
|
||||
* NOTE: This service is enabled/disabled by the system once the user
|
||||
* grants "Notification access" in Settings → Apps → Special app access.
|
||||
*/
|
||||
public class NotificationMonitorService extends NotificationListenerService {
|
||||
|
||||
public static final String ACTION_NOTIFICATION_RECEIVED =
|
||||
"com.notificationmonitor.NOTIFICATION_RECEIVED";
|
||||
public static final String EXTRA_ENTRY = "entry_json";
|
||||
|
||||
// Broadcast keys for the UI
|
||||
public static final String EXTRA_APP_NAME = "app_name";
|
||||
public static final String EXTRA_PKG = "pkg";
|
||||
public static final String EXTRA_TITLE = "title";
|
||||
public static final String EXTRA_TEXT = "text";
|
||||
public static final String EXTRA_TIMESTAMP = "timestamp";
|
||||
|
||||
private PreferencesManager prefsManager;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
prefsManager = new PreferencesManager(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationPosted(StatusBarNotification sbn) {
|
||||
if (sbn == null) return;
|
||||
|
||||
String pkg = sbn.getPackageName();
|
||||
|
||||
// Skip our own notifications and only process selected packages
|
||||
if (pkg.equals(getPackageName())) return;
|
||||
if (!prefsManager.isMonitored(pkg)) return;
|
||||
|
||||
Notification notification = sbn.getNotification();
|
||||
if (notification == null) return;
|
||||
|
||||
Bundle extras = notification.extras;
|
||||
String title = extras != null ? charSeqToString(extras.getCharSequence(Notification.EXTRA_TITLE)) : "";
|
||||
String text = extras != null ? charSeqToString(extras.getCharSequence(Notification.EXTRA_TEXT)) : "";
|
||||
if (text.isEmpty() && extras != null) {
|
||||
// Fall back to big text
|
||||
text = charSeqToString(extras.getCharSequence(Notification.EXTRA_BIG_TEXT));
|
||||
}
|
||||
|
||||
String appName = getAppName(pkg);
|
||||
long ts = System.currentTimeMillis();
|
||||
|
||||
// Persist to log
|
||||
NotificationEntry entry = new NotificationEntry(appName, pkg, title, text, ts);
|
||||
prefsManager.addNotificationEntry(entry);
|
||||
|
||||
// Broadcast to any open Activity
|
||||
Intent broadcast = new Intent(ACTION_NOTIFICATION_RECEIVED);
|
||||
broadcast.putExtra(EXTRA_APP_NAME, appName);
|
||||
broadcast.putExtra(EXTRA_PKG, pkg);
|
||||
broadcast.putExtra(EXTRA_TITLE, title);
|
||||
broadcast.putExtra(EXTRA_TEXT, text);
|
||||
broadcast.putExtra(EXTRA_TIMESTAMP, ts);
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationRemoved(StatusBarNotification sbn) {
|
||||
// No action needed for monitoring purposes
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
private String getAppName(String packageName) {
|
||||
try {
|
||||
PackageManager pm = getPackageManager();
|
||||
ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
|
||||
return pm.getApplicationLabel(info).toString();
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return packageName;
|
||||
}
|
||||
}
|
||||
|
||||
private String charSeqToString(CharSequence cs) {
|
||||
return cs != null ? cs.toString() : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.notificationmonitor.ui;
|
||||
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.notificationmonitor.data.AppInfo;
|
||||
import com.notificationmonitor.data.PreferencesManager;
|
||||
import com.notificationmonitor.databinding.ActivityAppSelectorBinding;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class AppSelectorActivity extends AppCompatActivity
|
||||
implements AppSelectorAdapter.OnAppToggleListener {
|
||||
|
||||
private ActivityAppSelectorBinding binding;
|
||||
private AppSelectorAdapter adapter;
|
||||
private PreferencesManager prefsManager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ActivityAppSelectorBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
setSupportActionBar(binding.toolbar);
|
||||
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
prefsManager = new PreferencesManager(this);
|
||||
adapter = new AppSelectorAdapter(this);
|
||||
binding.recyclerApps.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.recyclerApps.setAdapter(adapter);
|
||||
|
||||
// Search filter
|
||||
binding.etSearch.addTextChangedListener(new TextWatcher() {
|
||||
@Override public void beforeTextChanged(CharSequence s, int st, int c, int a) {}
|
||||
@Override public void onTextChanged(CharSequence s, int st, int b, int c) {
|
||||
adapter.filter(s.toString());
|
||||
}
|
||||
@Override public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
|
||||
loadApps();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppToggled(String packageName, boolean selected) {
|
||||
Set<String> monitored = prefsManager.getMonitoredPackages();
|
||||
if (selected) {
|
||||
monitored.add(packageName);
|
||||
} else {
|
||||
monitored.remove(packageName);
|
||||
}
|
||||
prefsManager.saveMonitoredPackages(monitored);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void loadApps() {
|
||||
binding.progressBar.setVisibility(android.view.View.VISIBLE);
|
||||
|
||||
AsyncTask.execute(() -> {
|
||||
PackageManager pm = getPackageManager();
|
||||
Set<String> monitored = prefsManager.getMonitoredPackages();
|
||||
|
||||
// Use MATCH_ALL to get every installed package regardless of visibility filters
|
||||
List<ApplicationInfo> installedApps =
|
||||
pm.getInstalledApplications(PackageManager.GET_META_DATA | PackageManager.MATCH_ALL);
|
||||
|
||||
List<AppInfo> appList = new ArrayList<>();
|
||||
for (ApplicationInfo info : installedApps) {
|
||||
// Skip our own app
|
||||
if (info.packageName.equals(getPackageName())) continue;
|
||||
|
||||
// Include user-installed apps AND system apps that have a UI
|
||||
boolean isUserApp = (info.flags & ApplicationInfo.FLAG_SYSTEM) == 0;
|
||||
boolean isSystemWithUI = ((info.flags & ApplicationInfo.FLAG_SYSTEM) != 0)
|
||||
&& pm.getLaunchIntentForPackage(info.packageName) != null;
|
||||
|
||||
if (isUserApp || isSystemWithUI) {
|
||||
String name = pm.getApplicationLabel(info).toString();
|
||||
boolean checked = monitored.contains(info.packageName);
|
||||
appList.add(new AppInfo(name, info.packageName,
|
||||
pm.getApplicationIcon(info), checked));
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(appList,
|
||||
(a, b) -> a.getAppName().compareToIgnoreCase(b.getAppName()));
|
||||
|
||||
runOnUiThread(() -> {
|
||||
binding.progressBar.setVisibility(android.view.View.GONE);
|
||||
adapter.setApps(appList);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.notificationmonitor.ui;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.notificationmonitor.R;
|
||||
import com.notificationmonitor.data.AppInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AppSelectorAdapter
|
||||
extends RecyclerView.Adapter<AppSelectorAdapter.ViewHolder> {
|
||||
|
||||
public interface OnAppToggleListener {
|
||||
void onAppToggled(String packageName, boolean selected);
|
||||
}
|
||||
|
||||
private List<AppInfo> allApps = new ArrayList<>();
|
||||
private List<AppInfo> filteredApps = new ArrayList<>();
|
||||
private final OnAppToggleListener listener;
|
||||
|
||||
public AppSelectorAdapter(OnAppToggleListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setApps(List<AppInfo> apps) {
|
||||
allApps = new ArrayList<>(apps);
|
||||
filteredApps = new ArrayList<>(apps);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void filter(String query) {
|
||||
filteredApps.clear();
|
||||
if (query.isEmpty()) {
|
||||
filteredApps.addAll(allApps);
|
||||
} else {
|
||||
String lq = query.toLowerCase();
|
||||
for (AppInfo a : allApps) {
|
||||
if (a.getAppName().toLowerCase().contains(lq) ||
|
||||
a.getPackageName().toLowerCase().contains(lq)) {
|
||||
filteredApps.add(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_app, parent, false);
|
||||
return new ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
AppInfo app = filteredApps.get(position);
|
||||
holder.ivIcon.setImageDrawable(app.getIcon());
|
||||
holder.tvName.setText(app.getAppName());
|
||||
holder.tvPkg.setText(app.getPackageName());
|
||||
|
||||
// Prevent recycled checkbox from triggering listener
|
||||
holder.checkBox.setOnCheckedChangeListener(null);
|
||||
holder.checkBox.setChecked(app.isSelected());
|
||||
holder.checkBox.setOnCheckedChangeListener((btn, checked) -> {
|
||||
app.setSelected(checked);
|
||||
listener.onAppToggled(app.getPackageName(), checked);
|
||||
});
|
||||
|
||||
holder.itemView.setOnClickListener(v -> holder.checkBox.toggle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() { return filteredApps.size(); }
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
ImageView ivIcon;
|
||||
TextView tvName, tvPkg;
|
||||
CheckBox checkBox;
|
||||
ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
ivIcon = itemView.findViewById(R.id.iv_app_icon);
|
||||
tvName = itemView.findViewById(R.id.tv_app_name);
|
||||
tvPkg = itemView.findViewById(R.id.tv_package_name);
|
||||
checkBox = itemView.findViewById(R.id.checkbox_monitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
148
app/src/main/java/com/notificationmonitor/ui/MainActivity.java
Normal file
148
app/src/main/java/com/notificationmonitor/ui/MainActivity.java
Normal file
@@ -0,0 +1,148 @@
|
||||
package com.notificationmonitor.ui;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.notificationmonitor.R;
|
||||
import com.notificationmonitor.data.NotificationEntry;
|
||||
import com.notificationmonitor.data.PreferencesManager;
|
||||
import com.notificationmonitor.databinding.ActivityMainBinding;
|
||||
import com.notificationmonitor.service.NotificationMonitorService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityMainBinding binding;
|
||||
private NotificationLogAdapter adapter;
|
||||
private PreferencesManager prefsManager;
|
||||
|
||||
// Receives live notification events from the service
|
||||
private final BroadcastReceiver notificationReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String appName = intent.getStringExtra(NotificationMonitorService.EXTRA_APP_NAME);
|
||||
String pkg = intent.getStringExtra(NotificationMonitorService.EXTRA_PKG);
|
||||
String title = intent.getStringExtra(NotificationMonitorService.EXTRA_TITLE);
|
||||
String text = intent.getStringExtra(NotificationMonitorService.EXTRA_TEXT);
|
||||
long timestamp = intent.getLongExtra(NotificationMonitorService.EXTRA_TIMESTAMP, 0);
|
||||
|
||||
NotificationEntry entry = new NotificationEntry(appName, pkg, title, text, timestamp);
|
||||
adapter.addEntry(entry);
|
||||
binding.recyclerNotifications.scrollToPosition(0);
|
||||
updateEmptyState();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
setSupportActionBar(binding.toolbar);
|
||||
|
||||
prefsManager = new PreferencesManager(this);
|
||||
|
||||
// Set up RecyclerView
|
||||
adapter = new NotificationLogAdapter();
|
||||
binding.recyclerNotifications.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.recyclerNotifications.setAdapter(adapter);
|
||||
|
||||
// Load persisted log
|
||||
List<NotificationEntry> log = prefsManager.getNotificationLog();
|
||||
adapter.setEntries(log);
|
||||
updateEmptyState();
|
||||
|
||||
// Prompt for notification access if not yet granted
|
||||
if (!isNotificationAccessGranted()) {
|
||||
showPermissionDialog();
|
||||
}
|
||||
|
||||
// Select apps FAB
|
||||
binding.fabSelectApps.setOnClickListener(v ->
|
||||
startActivity(new Intent(this, AppSelectorActivity.class)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(
|
||||
notificationReceiver,
|
||||
new IntentFilter(NotificationMonitorService.ACTION_NOTIFICATION_RECEIVED));
|
||||
|
||||
// Refresh banner in case user just granted access
|
||||
updatePermissionBanner();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(notificationReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_clear_log) {
|
||||
prefsManager.clearLog();
|
||||
adapter.clearEntries();
|
||||
updateEmptyState();
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.action_settings) {
|
||||
startActivity(new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS));
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
private boolean isNotificationAccessGranted() {
|
||||
String flat = Settings.Secure.getString(
|
||||
getContentResolver(), "enabled_notification_listeners");
|
||||
return flat != null && flat.contains(getPackageName());
|
||||
}
|
||||
|
||||
private void updatePermissionBanner() {
|
||||
if (isNotificationAccessGranted()) {
|
||||
binding.bannerPermission.setVisibility(android.view.View.GONE);
|
||||
} else {
|
||||
binding.bannerPermission.setVisibility(android.view.View.VISIBLE);
|
||||
binding.bannerPermission.setOnClickListener(v ->
|
||||
startActivity(new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)));
|
||||
}
|
||||
}
|
||||
|
||||
private void showPermissionDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.permission_dialog_title)
|
||||
.setMessage(R.string.permission_dialog_message)
|
||||
.setPositiveButton(R.string.go_to_settings, (d, w) ->
|
||||
startActivity(new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void updateEmptyState() {
|
||||
boolean empty = adapter.getItemCount() == 0;
|
||||
binding.emptyState.setVisibility(empty ? android.view.View.VISIBLE : android.view.View.GONE);
|
||||
binding.recyclerNotifications.setVisibility(empty ? android.view.View.GONE : android.view.View.VISIBLE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.notificationmonitor.ui;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.notificationmonitor.R;
|
||||
import com.notificationmonitor.data.NotificationEntry;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class NotificationLogAdapter
|
||||
extends RecyclerView.Adapter<NotificationLogAdapter.ViewHolder> {
|
||||
|
||||
private final List<NotificationEntry> entries = new ArrayList<>();
|
||||
private final SimpleDateFormat sdf =
|
||||
new SimpleDateFormat("dd MMM HH:mm:ss", Locale.getDefault());
|
||||
|
||||
public void setEntries(List<NotificationEntry> list) {
|
||||
entries.clear();
|
||||
entries.addAll(list);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addEntry(NotificationEntry entry) {
|
||||
entries.add(0, entry);
|
||||
notifyItemInserted(0);
|
||||
}
|
||||
|
||||
public void clearEntries() {
|
||||
entries.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_notification, parent, false);
|
||||
return new ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
NotificationEntry entry = entries.get(position);
|
||||
holder.tvAppName.setText(entry.getAppName());
|
||||
holder.tvTitle.setText(entry.getTitle().isEmpty() ? "(no title)" : entry.getTitle());
|
||||
holder.tvText.setText(entry.getText().isEmpty() ? "(no content)" : entry.getText());
|
||||
holder.tvTime.setText(sdf.format(new Date(entry.getTimestamp())));
|
||||
|
||||
holder.tvTitle.setVisibility(entry.getTitle().isEmpty() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() { return entries.size(); }
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvAppName, tvTitle, tvText, tvTime;
|
||||
ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvAppName = itemView.findViewById(R.id.tv_app_name);
|
||||
tvTitle = itemView.findViewById(R.id.tv_title);
|
||||
tvText = itemView.findViewById(R.id.tv_text);
|
||||
tvTime = itemView.findViewById(R.id.tv_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
app/src/main/res/drawable/ic_notification_bell.xml
Normal file
10
app/src/main/res/drawable/ic_notification_bell.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" />
|
||||
</vector>
|
||||
53
app/src/main/res/layout/activity_app_selector.xml
Normal file
53
app/src/main/res/layout/activity_app_selector.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@color/surface"
|
||||
app:title="@string/select_apps"
|
||||
app:titleTextColor="@color/on_surface" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<!-- Search box -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="12dp"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/search_apps"
|
||||
android:imeOptions="actionSearch"
|
||||
android:inputType="text"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_apps"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
</LinearLayout>
|
||||
106
app/src/main/res/layout/activity_main.xml
Normal file
106
app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="4dp">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@color/surface"
|
||||
app:title="@string/app_name"
|
||||
app:titleTextColor="@color/on_surface" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<!-- Permission warning banner -->
|
||||
<TextView
|
||||
android:id="@+id/banner_permission"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/warning"
|
||||
android:drawablePadding="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="12dp"
|
||||
android:text="@string/permission_banner"
|
||||
android:textColor="@color/on_warning"
|
||||
android:textSize="13sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Notification log -->
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_notifications"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="80dp"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
<!-- Empty state -->
|
||||
<LinearLayout
|
||||
android:id="@+id/empty_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🔔"
|
||||
android:textSize="64sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/empty_state_title"
|
||||
android:textColor="@color/on_surface"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/empty_state_subtitle"
|
||||
android:textColor="@color/on_surface_secondary"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- FAB to select apps -->
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/fab_select_apps"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/select_apps"
|
||||
app:icon="@android:drawable/ic_menu_manage"
|
||||
app:backgroundTint="@color/primary"
|
||||
app:iconTint="@color/on_primary" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
50
app/src/main/res/layout/item_app.xml
Normal file
50
app/src/main/res/layout/item_app.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="64dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="10dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_app_icon"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:contentDescription="@string/app_icon_desc"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_app_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/on_surface"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_package_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/on_surface_secondary"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_monitor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
|
||||
</LinearLayout>
|
||||
64
app/src/main/res/layout/item_notification.xml
Normal file
64
app/src/main/res/layout/item_notification.xml
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:cardBackgroundColor="@color/surface"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="14dp">
|
||||
|
||||
<!-- Header row: app name + time -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_app_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textColor="@color/primary"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/on_surface_secondary"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Notification title -->
|
||||
<TextView
|
||||
android:id="@+id/tv_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="@color/on_surface"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<!-- Notification body -->
|
||||
<TextView
|
||||
android:id="@+id/tv_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:maxLines="3"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/on_surface_secondary"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
15
app/src/main/res/menu/menu_main.xml
Normal file
15
app/src/main/res/menu/menu_main.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_clear_log"
|
||||
android:title="Clear Log"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_settings"
|
||||
android:title="Notification Access Settings"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
10
app/src/main/res/mipmap-hdpi/ic_launcher.xml
Normal file
10
app/src/main/res/mipmap-hdpi/ic_launcher.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/primary" />
|
||||
<foreground>
|
||||
<inset
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:drawable="@drawable/ic_notification_bell"
|
||||
android:inset="20%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
10
app/src/main/res/mipmap-hdpi/ic_launcher_round.xml
Normal file
10
app/src/main/res/mipmap-hdpi/ic_launcher_round.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/primary" />
|
||||
<foreground>
|
||||
<inset
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:drawable="@drawable/ic_notification_bell"
|
||||
android:inset="20%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
26
app/src/main/res/values/colors.xml
Normal file
26
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Brand -->
|
||||
<color name="primary">#1976D2</color>
|
||||
<color name="primary_dark">#1565C0</color>
|
||||
<color name="on_primary">#FFFFFF</color>
|
||||
|
||||
<!-- Surfaces -->
|
||||
<color name="background">#F0F4F8</color>
|
||||
<color name="surface">#FFFFFF</color>
|
||||
<color name="on_surface">#1A1A2E</color>
|
||||
<color name="on_surface_secondary">#6B7280</color>
|
||||
|
||||
<!-- Warning banner -->
|
||||
<color name="warning">#FFF3CD</color>
|
||||
<color name="on_warning">#856404</color>
|
||||
|
||||
<!-- Material defaults -->
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
14
app/src/main/res/values/strings.xml
Normal file
14
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Notification Monitor</string>
|
||||
<string name="service_label">Notification Monitor Service</string>
|
||||
<string name="select_apps">Select Apps</string>
|
||||
<string name="search_apps">Search apps…</string>
|
||||
<string name="app_icon_desc">App icon</string>
|
||||
<string name="permission_banner">⚠️ Notification access not granted — tap to enable</string>
|
||||
<string name="permission_dialog_title">Notification Access Required</string>
|
||||
<string name="permission_dialog_message">This app needs notification access to monitor selected apps. Tap "Go to Settings", find "Notification Monitor", and enable it.</string>
|
||||
<string name="go_to_settings">Go to Settings</string>
|
||||
<string name="empty_state_title">No notifications yet</string>
|
||||
<string name="empty_state_subtitle">Select apps to monitor using the button below,\nthen notifications will appear here.</string>
|
||||
</resources>
|
||||
9
app/src/main/res/values/themes.xml
Normal file
9
app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.NotificationMonitor" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorPrimaryVariant">@color/primary_dark</item>
|
||||
<item name="colorOnPrimary">@color/on_primary</item>
|
||||
<item name="android:windowBackground">@color/background</item>
|
||||
</style>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user