diff --git a/app/build.gradle b/app/build.gradle index cadb5a5..46c9486 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ android { defaultConfig { applicationId "net.headlezz.notificationlogger" - minSdkVersion 18 + minSdkVersion 19 targetSdkVersion 23 versionCode 1 versionName "1.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 292c1b9..527db7a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ package="net.headlezz.notificationlogger"> - + - + + + + + + + + diff --git a/app/src/main/java/net/headlezz/notificationlogger/Application.java b/app/src/main/java/net/headlezz/notificationlogger/Application.java new file mode 100644 index 0000000..50f51f6 --- /dev/null +++ b/app/src/main/java/net/headlezz/notificationlogger/Application.java @@ -0,0 +1,12 @@ +package net.headlezz.notificationlogger; + +import timber.log.Timber; + +public class Application extends android.app.Application { + + @Override + public void onCreate() { + super.onCreate(); + Timber.plant(new Timber.DebugTree()); + } +} diff --git a/app/src/main/java/net/headlezz/notificationlogger/MainActivity.java b/app/src/main/java/net/headlezz/notificationlogger/MainActivity.java index e3f7d4b..0e0748c 100644 --- a/app/src/main/java/net/headlezz/notificationlogger/MainActivity.java +++ b/app/src/main/java/net/headlezz/notificationlogger/MainActivity.java @@ -1,21 +1,26 @@ package net.headlezz.notificationlogger; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; +import android.support.v7.preference.PreferenceManager; import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; import android.view.View; import net.headlezz.notificationlogger.createnotification.NotificationCreationFragment; +import net.headlezz.notificationlogger.logger.LoggerUtils; +import net.headlezz.notificationlogger.presenter.LoggerWarningPresenter; import butterknife.Bind; import butterknife.ButterKnife; -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener, LoggerWarningPresenter.LoggerWarningView { @Bind(R.id.toolbar) Toolbar mToolbar; @@ -23,14 +28,21 @@ public class MainActivity extends AppCompatActivity { @Bind(R.id.main_menu_fab_add_notification) FloatingActionButton mFAB; + SharedPreferences mSharedPrefs; + + LoggerWarningPresenter mLoggerWarningPresenter; + Snackbar mLoggerWarningView; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); ButterKnife.bind(this); - setSupportActionBar(mToolbar); + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + mLoggerWarningPresenter = new LoggerWarningPresenter(this); + mFAB.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -38,7 +50,7 @@ public class MainActivity extends AppCompatActivity { } }); - if(getSupportFragmentManager().findFragmentById(R.id.main_menu_fragment_container) == null) { + if (getSupportFragmentManager().findFragmentById(R.id.main_menu_fragment_container) == null) { getSupportFragmentManager().beginTransaction() .add(R.id.main_menu_fragment_container, new NotificationListFragment()) .commit(); @@ -48,10 +60,25 @@ public class MainActivity extends AppCompatActivity { @Override protected void onResume() { super.onResume(); + if(!LoggerUtils.isLoggerRunning()) + LoggerUtils.buildNotificationAccessDialog(this).show(); if(getSupportFragmentManager().getBackStackEntryCount() > 0) mFAB.setVisibility(View.GONE); } + @Override + protected void onStart() { + super.onStart(); + mSharedPrefs.registerOnSharedPreferenceChangeListener(this); + mLoggerWarningPresenter.setCurrentLoggerPreference(mSharedPrefs.getBoolean("pref_logging_enabled", true)); + } + + @Override + protected void onStop() { + super.onStop(); + mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this); + } + @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main_activity, menu); @@ -60,11 +87,11 @@ public class MainActivity extends AppCompatActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { - if(item.getItemId() == R.id.menu_settings) { + if (item.getItemId() == R.id.menu_settings) { Intent intent = new Intent(this, PreferenceActivity.class); startActivity(intent); return true; - } else if(item.getItemId() == android.R.id.home) { + } else if (item.getItemId() == android.R.id.home) { onBackPressed(); } return super.onOptionsItemSelected(item); @@ -86,11 +113,48 @@ public class MainActivity extends AppCompatActivity { @Override public void onBackPressed() { // make fab visible and change title if we come back to first fragment - if(getSupportFragmentManager().getBackStackEntryCount() == 1) { + if (getSupportFragmentManager().getBackStackEntryCount() == 1) { mFAB.setVisibility(View.VISIBLE); getSupportActionBar().setTitle(getString(R.string.app_name)); getSupportActionBar().setDisplayHomeAsUpEnabled(false); } super.onBackPressed(); } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.equals("pref_logging_enabled")) + mLoggerWarningPresenter.setCurrentLoggerPreference(sharedPreferences.getBoolean("pref_logging_enabled", true)); + } + + @Override + public void showLoggerWarning() { + if (mLoggerWarningView == null) { + mLoggerWarningView = Snackbar.make(findViewById(R.id.viewholder), R.string.main_snack_logging_disabled_warning, Snackbar.LENGTH_INDEFINITE); + mLoggerWarningView.setAction(R.string.main_snack_logging_disabled_warning_action, new View.OnClickListener() { + @Override + public void onClick(View v) { + mSharedPrefs.edit().putBoolean("pref_logging_enabled", true).apply(); + } + }); + // fix for FAB not moving down after dismiss + mLoggerWarningView.getView().addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + } + + @Override + public void onViewDetachedFromWindow(View v) { + mFAB.setTranslationY(0); + } + }); + } + mLoggerWarningView.show(); + } + + @Override + public void hideLoggerWarning() { + if (mLoggerWarningView != null && mLoggerWarningView.isShownOrQueued()) + mLoggerWarningView.dismiss(); + } } diff --git a/app/src/main/java/net/headlezz/notificationlogger/PackageUtils.java b/app/src/main/java/net/headlezz/notificationlogger/PackageUtils.java new file mode 100644 index 0000000..0d32c08 --- /dev/null +++ b/app/src/main/java/net/headlezz/notificationlogger/PackageUtils.java @@ -0,0 +1,16 @@ +package net.headlezz.notificationlogger; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +public class PackageUtils { + + public static String getAppName(Context context, String packageName) throws PackageManager.NameNotFoundException { + PackageManager pm = context.getPackageManager(); + ApplicationInfo appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); + return (String) pm.getApplicationLabel(appInfo); + } + + +} diff --git a/app/src/main/java/net/headlezz/notificationlogger/PreferenceFragment.java b/app/src/main/java/net/headlezz/notificationlogger/PreferenceFragment.java index c5a7036..625bcd0 100644 --- a/app/src/main/java/net/headlezz/notificationlogger/PreferenceFragment.java +++ b/app/src/main/java/net/headlezz/notificationlogger/PreferenceFragment.java @@ -1,17 +1,23 @@ package net.headlezz.notificationlogger; import android.app.Activity; +import android.content.ContentResolver; +import android.content.DialogInterface; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import android.support.design.widget.Snackbar; +import android.support.v7.app.AlertDialog; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceFragmentCompat; +import net.headlezz.notificationlogger.logger.Logged_notificationTable; + public class PreferenceFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceClickListener { private PreferenceCallbacks mCallbacks; final String PREF_ABOUT = "pref_about"; final String PREF_BLACKLIST = "pref_blacklist"; + final String PREF_CLEAR_DATABASE = "pref_clear_database"; interface PreferenceCallbacks { @@ -24,6 +30,7 @@ public class PreferenceFragment extends PreferenceFragmentCompat implements Pref addPreferencesFromResource(R.xml.preferences); findPreference(PREF_ABOUT).setOnPreferenceClickListener(this); findPreference(PREF_BLACKLIST).setOnPreferenceClickListener(this); + findPreference(PREF_CLEAR_DATABASE).setOnPreferenceClickListener(this); } @Override @@ -35,10 +42,29 @@ public class PreferenceFragment extends PreferenceFragmentCompat implements Pref } else if(key.equals(PREF_BLACKLIST)) { mCallbacks.showBlacklistScreen(); return true; - } + } else if(key.equals(PREF_CLEAR_DATABASE)) + askClearDatabase(); return false; } + private void askClearDatabase() { + new AlertDialog.Builder(getContext()) + .setTitle(R.string.pref_clear_database_dialog_title) + .setMessage(R.string.pref_clear_database_dialog_message) + .setNegativeButton(R.string.pref_clear_database_dialog_negative, null) + .setPositiveButton(R.string.pref_clear_database_dialog_positive, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ContentResolver resolver = getContext().getContentResolver(); + // there are no notifications with negative ids, so we use this to select all rows + String whereClause = Logged_notificationTable.FIELD_NOTIFICATION_ID + " != -1"; + resolver.delete(Logged_notificationTable.CONTENT_URI, whereClause, new String[0]); + Snackbar.make(getActivity().findViewById(android.R.id.content), R.string.pref_clear_database_snackbar_cleared, Snackbar.LENGTH_SHORT).show(); + } + }) + .create().show(); + } + @SuppressWarnings("ConstantConditions") @Override public void onAttach(Activity activity) { diff --git a/app/src/main/java/net/headlezz/notificationlogger/logger/LoggedNotification.java b/app/src/main/java/net/headlezz/notificationlogger/logger/LoggedNotification.java new file mode 100644 index 0000000..ac2d56c --- /dev/null +++ b/app/src/main/java/net/headlezz/notificationlogger/logger/LoggedNotification.java @@ -0,0 +1,36 @@ +package net.headlezz.notificationlogger.logger; + +import java.util.Date; + +import ckm.simple.sql_provider.annotation.SimpleSQLColumn; +import ckm.simple.sql_provider.annotation.SimpleSQLTable; + +@SimpleSQLTable(table="logged_notification", provider="NotificationProvider") +public class LoggedNotification { + + + @SimpleSQLColumn("title") + public String title; + + @SimpleSQLColumn("message") + public String message; + + @SimpleSQLColumn("date") + public Date date; + + @SimpleSQLColumn("app_name") + public String appName; + + @SimpleSQLColumn("package_name") + public String packageName; + + @SimpleSQLColumn("notification_id") + public int notificationId; + + @SimpleSQLColumn("user_id") + public int userId; + + @SimpleSQLColumn("small_icon_id") + public int smallIconId; + +} diff --git a/app/src/main/java/net/headlezz/notificationlogger/logger/LoggerUtils.java b/app/src/main/java/net/headlezz/notificationlogger/logger/LoggerUtils.java new file mode 100644 index 0000000..1f64635 --- /dev/null +++ b/app/src/main/java/net/headlezz/notificationlogger/logger/LoggerUtils.java @@ -0,0 +1,36 @@ +package net.headlezz.notificationlogger.logger; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.support.v7.app.AlertDialog; + +import net.headlezz.notificationlogger.R; + +public class LoggerUtils { + + public static void openNotificationSystemSettings(Context context) { + Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + context.startActivity(intent); + } + + public static boolean isLoggerRunning() { + return NotificationLoggerService.isRunning; + } + + public static AlertDialog buildNotificationAccessDialog(final Context context) { + return new AlertDialog.Builder(context) + .setTitle(context.getString(R.string.main_dialog_enable_notification_access_title)) + .setMessage(context.getString(R.string.main_dialog_enable_notification_access_message)) + .setPositiveButton(R.string.main_dialog_enable_notification_access_positive, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + openNotificationSystemSettings(context); + } + }) + .setNegativeButton(R.string.main_dialog_enable_notification_access_negative, null) + .create(); + } + +} diff --git a/app/src/main/java/net/headlezz/notificationlogger/logger/NotificationLoggerService.java b/app/src/main/java/net/headlezz/notificationlogger/logger/NotificationLoggerService.java new file mode 100644 index 0000000..73d5238 --- /dev/null +++ b/app/src/main/java/net/headlezz/notificationlogger/logger/NotificationLoggerService.java @@ -0,0 +1,114 @@ +package net.headlezz.notificationlogger.logger; + +import android.app.Notification; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.service.notification.NotificationListenerService; +import android.service.notification.StatusBarNotification; + +import net.headlezz.notificationlogger.PackageUtils; + +import java.util.Date; + +import timber.log.Timber; + +public class NotificationLoggerService extends NotificationListenerService implements SharedPreferences.OnSharedPreferenceChangeListener { + + /** + * true if logger is enabled in system settings + */ + public static boolean isRunning = false; + + /** + * true if logger is enabled in app settings + */ + public static boolean isEnabled = true; + + SharedPreferences mSharedPrefs; + + @Override + public IBinder onBind(Intent intent) { + isRunning = true; + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + isEnabled = mSharedPrefs.getBoolean("pref_logging_enabled", true); + mSharedPrefs.registerOnSharedPreferenceChangeListener(this); + Timber.d(String.format("Logger started (running: %s, enabled: %s)", isRunning, isEnabled)); + return super.onBind(intent); + } + + @Override + public boolean onUnbind(Intent intent) { + mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this); + Timber.d("Logger stopped"); + return super.onUnbind(intent); + } + + @Override + public void onNotificationPosted(StatusBarNotification sbn) { + super.onNotificationPosted(sbn); + + if(!isEnabled) { + Timber.d("Notification posted but logger is disabled"); + return; + } + + + LoggedNotification ln = buildLoggedNotification(sbn); + getApplicationContext().getContentResolver().insert( + Logged_notificationTable.CONTENT_URI, + Logged_notificationTable.getContentValues(ln, false) + ); + Timber.d(String.format("Notificaton logged (id: %d)", ln.notificationId)); + } + + private LoggedNotification buildLoggedNotification(StatusBarNotification sbn) { + int notificationId = sbn.getId(); + + int userId = sbn.getUserId(); + Date date = new Date(sbn.getPostTime()); + String packageName = sbn.getPackageName(); + int iconId = sbn.getNotification().icon; + + String appName; + try { + appName = PackageUtils.getAppName(getApplicationContext(), packageName); + } catch (PackageManager.NameNotFoundException e) { + Timber.w("App name not found for " + packageName); + appName = ""; + } + + Notification notification = sbn.getNotification(); + Bundle extras = notification.extras; + + String title = extras.getString("android.title", ""); + String message = extras.getCharSequence("android.text", "").toString(); + + if(message.isEmpty() && notification.tickerText != null) + message = notification.tickerText.toString(); + if(message.isEmpty() && extras.containsKey("android.infoText")) + message = extras.get("android.infoText").toString(); + + LoggedNotification ln = new LoggedNotification(); + ln.title = title; + ln.message = message; + ln.date = date; + ln.appName = appName; + ln.packageName = packageName; + ln.notificationId = notificationId; + ln.userId = userId; + ln.smallIconId = iconId; + return ln; + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if(key.equals("pref_logging_enabled")) { + isEnabled = sharedPreferences.getBoolean("pref_logging_enabled", true); + Timber.d("Logging is now " + isEnabled); + } + } +} diff --git a/app/src/main/java/net/headlezz/notificationlogger/logger/NotificationProviderConfig.java b/app/src/main/java/net/headlezz/notificationlogger/logger/NotificationProviderConfig.java new file mode 100644 index 0000000..ffd6084 --- /dev/null +++ b/app/src/main/java/net/headlezz/notificationlogger/logger/NotificationProviderConfig.java @@ -0,0 +1,18 @@ +package net.headlezz.notificationlogger.logger; + +import ckm.simple.sql_provider.UpgradeScript; +import ckm.simple.sql_provider.annotation.ProviderConfig; +import ckm.simple.sql_provider.annotation.SimpleSQLConfig; + +@SimpleSQLConfig( + name="NotificationProvider", + authority = "net.headlezz.notificationlogger.logger.authority", + database = "notifications.db", + version = 1 +) +public class NotificationProviderConfig implements ProviderConfig { + @Override + public UpgradeScript[] getUpdateScripts() { + return new UpgradeScript[0]; + } +} diff --git a/app/src/main/java/net/headlezz/notificationlogger/presenter/LoggerWarningPresenter.java b/app/src/main/java/net/headlezz/notificationlogger/presenter/LoggerWarningPresenter.java new file mode 100644 index 0000000..3cfdd17 --- /dev/null +++ b/app/src/main/java/net/headlezz/notificationlogger/presenter/LoggerWarningPresenter.java @@ -0,0 +1,23 @@ +package net.headlezz.notificationlogger.presenter; + +public class LoggerWarningPresenter { + + public interface LoggerWarningView { + void showLoggerWarning(); + void hideLoggerWarning(); + } + + LoggerWarningView mView; + + public LoggerWarningPresenter(LoggerWarningView lwv) { + mView = lwv; + } + + public void setCurrentLoggerPreference(boolean loggingEnabled) { + if(loggingEnabled) + mView.hideLoggerWarning(); + else + mView.showLoggerWarning(); + } + +} diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index 12ae9a6..82e5d94 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -1,5 +1,6 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1c84b09..501025c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,6 +3,13 @@ Settings Settings + Logging is disabled. + Enable + Enable Notification Access + In order to log notifcations, this app needs access to your notification system. + Open settings + Cancel + Notification Settings Support Notification Logging @@ -13,6 +20,12 @@ Remove all logged notifications Export to sdcard About + Clear database + Delete all logged notifications? + Cancel + Yes + Database cleared. + FAB: Create notification Create notification Blacklist