diff options
-rw-r--r-- | core/java/com/android/internal/util/cm/SpamFilter.java | 58 | ||||
-rw-r--r-- | packages/SystemUI/AndroidManifest.xml | 6 | ||||
-rw-r--r-- | packages/SystemUI/res/layout/notification_guts.xml | 10 | ||||
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/cm/SpamMessageProvider.java | 196 | ||||
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/cm/SpamOpenHelper.java | 46 | ||||
-rwxr-xr-x[-rw-r--r--] | packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java | 28 | ||||
-rwxr-xr-x[-rw-r--r--] | services/core/java/com/android/server/notification/NotificationManagerService.java | 74 |
7 files changed, 418 insertions, 0 deletions
diff --git a/core/java/com/android/internal/util/cm/SpamFilter.java b/core/java/com/android/internal/util/cm/SpamFilter.java new file mode 100644 index 0000000..cc0e716 --- /dev/null +++ b/core/java/com/android/internal/util/cm/SpamFilter.java @@ -0,0 +1,58 @@ +package com.android.internal.util.cm; + +import android.app.Notification; +import android.content.ContentResolver; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; + +public class SpamFilter { + + public static final String AUTHORITY = "com.cyanogenmod.spam"; + public static final Uri NOTIFICATION_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY) + .build(); + + public static final class SpamContract { + + public static final class PackageTable { + public static final String TABLE_NAME = "packages"; + public static final String ID = "_id"; + public static final String PACKAGE_NAME = "package_name"; + } + + public static final class NotificationTable { + public static final String TABLE_NAME = "notifications"; + public static final String ID = "_id"; + public static final String PACKAGE_ID = "package_id"; + public static final String MESSAGE_TEXT = "message_text"; + public static final String COUNT = "count"; + public static final String LAST_BLOCKED = "last_blocked"; + public static final String NORMALIZED_TEXT = "normalized_text"; + } + + } + + public static String getNormalizedContent(String msg) { + return msg.toLowerCase().replaceAll("[^\\p{L}\\p{Nd}]+", ""); + } + + public static String getNotificationContent(Notification notification) { + Bundle extras = notification.extras; + String titleExtra = extras.containsKey(Notification.EXTRA_TITLE_BIG) + ? Notification.EXTRA_TITLE_BIG : Notification.EXTRA_TITLE; + CharSequence notificationTitle = extras.getCharSequence(titleExtra); + CharSequence notificationMessage = extras.getCharSequence(Notification.EXTRA_TEXT); + + if (TextUtils.isEmpty(notificationMessage)) { + CharSequence[] inboxLines = extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES); + if (inboxLines == null || inboxLines.length == 0) { + notificationMessage = ""; + } else { + notificationMessage = TextUtils.join("\n", inboxLines); + } + } + return notificationTitle + "\n" + notificationMessage; + } +} diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 8375197..772067b 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -411,5 +411,11 @@ <action android:name="com.android.systemui.action.CLEAR_TUNER" /> </intent-filter> </receiver> + + <provider android:name=".cm.SpamMessageProvider" + android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" + android:exported="true" + android:authorities="com.cyanogenmod.spam" /> + </application> </manifest> diff --git a/packages/SystemUI/res/layout/notification_guts.xml b/packages/SystemUI/res/layout/notification_guts.xml index d52c274..92cb601 100644 --- a/packages/SystemUI/res/layout/notification_guts.xml +++ b/packages/SystemUI/res/layout/notification_guts.xml @@ -87,6 +87,16 @@ /> <ImageButton style="@android:style/Widget.Material.Light.Button.Borderless.Small" + android:id="@+id/notification_inspect_filter_notification" + android:layout_width="52dp" + android:layout_height="match_parent" + android:layout_weight="0" + android:gravity="center" + android:src="@drawable/ic_volume_ringer_mute" + android:visibility="gone" + /> + + <ImageButton style="@android:style/Widget.Material.Light.Button.Borderless.Small" android:id="@+id/notification_inspect_item" android:layout_width="52dp" android:layout_height="match_parent" diff --git a/packages/SystemUI/src/com/android/systemui/cm/SpamMessageProvider.java b/packages/SystemUI/src/com/android/systemui/cm/SpamMessageProvider.java new file mode 100644 index 0000000..a43f263 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cm/SpamMessageProvider.java @@ -0,0 +1,196 @@ +package com.android.systemui.cm; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.text.TextUtils; + +import com.android.internal.util.cm.SpamFilter; +import com.android.internal.util.cm.SpamFilter.SpamContract.PackageTable; +import com.android.internal.util.cm.SpamFilter.SpamContract.NotificationTable; + +public class SpamMessageProvider extends ContentProvider { + public static final String AUTHORITY = SpamFilter.AUTHORITY; + + private static final String UPDATE_COUNT_QUERY = + "UPDATE " + NotificationTable.TABLE_NAME + + " SET " + NotificationTable.LAST_BLOCKED + "=%d," + + NotificationTable.COUNT + "=" + NotificationTable.COUNT + "+1 " + + " WHERE " + NotificationTable.ID + "='%s'"; + + private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); + private static final int PACKAGES = 0; + private static final int MESSAGE = 1; + private static final int PACKAGE_ID = 2; + private static final int MESSAGE_UPDATE_COUNT = 3; + private static final int MESSAGE_FOR_ID = 4; + static { + sURIMatcher.addURI(AUTHORITY, "packages", PACKAGES); + sURIMatcher.addURI(AUTHORITY, "package/id/*", PACKAGE_ID); + sURIMatcher.addURI(AUTHORITY, "message", MESSAGE); + sURIMatcher.addURI(AUTHORITY, "message/#", MESSAGE_FOR_ID); + sURIMatcher.addURI(AUTHORITY, "message/inc_count/#", MESSAGE_UPDATE_COUNT); + } + + private SpamOpenHelper mDbHelper; + + @Override + public boolean onCreate() { + mDbHelper = new SpamOpenHelper(getContext()); + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + int match = sURIMatcher.match(uri); + switch (match) { + case PACKAGE_ID: + Cursor idCursor = mDbHelper.getReadableDatabase().query(PackageTable.TABLE_NAME, + new String[]{NotificationTable.ID}, PackageTable.PACKAGE_NAME + "=?", + new String[]{uri.getLastPathSegment()}, null, null, null); + return idCursor; + case PACKAGES: + Cursor pkgCursor = mDbHelper.getReadableDatabase().query(PackageTable.TABLE_NAME, + null, null, null, null, null, null); + return pkgCursor; + case MESSAGE: + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(PackageTable.TABLE_NAME + "," + NotificationTable.TABLE_NAME); + String pkgId = PackageTable.TABLE_NAME + "." + PackageTable.ID; + String notificationPkgId = NotificationTable.TABLE_NAME + "." + + NotificationTable.PACKAGE_ID; + qb.appendWhere(pkgId + "=" + notificationPkgId); + SQLiteDatabase db = mDbHelper.getReadableDatabase(); + Cursor ret = qb.query(db, new String[]{NotificationTable.TABLE_NAME + ".*"}, + selection, selectionArgs, null, null, null); + ret.moveToFirst(); + return ret; + case MESSAGE_FOR_ID: + qb = new SQLiteQueryBuilder(); + qb.setTables(NotificationTable.TABLE_NAME); + qb.appendWhere(NotificationTable.PACKAGE_ID + "=" + uri.getLastPathSegment()); + db = mDbHelper.getReadableDatabase(); + ret = qb.query(db, null, null, null, null, null, null); + return ret; + default: + return null; + } + } + + private long getPackageId(String pkg) { + long rowId = -1; + Cursor idCursor = mDbHelper.getReadableDatabase().query(PackageTable.TABLE_NAME, + new String[]{NotificationTable.ID}, PackageTable.PACKAGE_NAME + "=?", + new String[]{pkg}, null, null, null); + if (idCursor != null) { + if (idCursor.moveToFirst()) { + rowId = idCursor.getLong(0); + } + idCursor.close(); + } + return rowId; + } + + @Override + public String getType(Uri uri) { + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + if (values == null) { + return null; + } + int match = sURIMatcher.match(uri); + switch (match) { + case MESSAGE: + String msgText = values.getAsString(NotificationTable.MESSAGE_TEXT); + String packageName = values.getAsString(PackageTable.PACKAGE_NAME); + if (TextUtils.isEmpty(msgText) || TextUtils.isEmpty(packageName)) { + return null; + } + values.clear(); + values.put(PackageTable.PACKAGE_NAME, packageName); + long packageId = getPackageId(packageName); + if (packageId == -1) { + packageId = mDbHelper.getWritableDatabase().insert( + PackageTable.TABLE_NAME, null, values); + } + if (packageId != -1) { + values.clear(); + values.put(NotificationTable.MESSAGE_TEXT, msgText); + values.put(NotificationTable.NORMALIZED_TEXT, + SpamFilter.getNormalizedContent(msgText)); + values.put(NotificationTable.PACKAGE_ID, packageId); + values.put(NotificationTable.LAST_BLOCKED, System.currentTimeMillis()); + mDbHelper.getReadableDatabase().insert(NotificationTable.TABLE_NAME, + null, values); + notifyChange(); + } + return null; + default: + return null; + } + } + + private void notifyChange() { + getContext().getContentResolver().notifyChange(SpamFilter.NOTIFICATION_URI, null); + } + + private void removePackageIfNecessary(int packageId) { + long numEntries = DatabaseUtils.queryNumEntries(mDbHelper.getReadableDatabase(), + NotificationTable.TABLE_NAME, NotificationTable.PACKAGE_ID + "=?", + new String[]{String.valueOf(packageId)}); + if (numEntries == 0) { + mDbHelper.getWritableDatabase().delete(PackageTable.TABLE_NAME, PackageTable.ID + "=?", + new String[]{String.valueOf(packageId)}); + } + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int match = sURIMatcher.match(uri); + switch (match) { + case MESSAGE_FOR_ID: + int packageId = -1; + Cursor idCursor = mDbHelper.getReadableDatabase().query(NotificationTable.TABLE_NAME, + new String[]{NotificationTable.PACKAGE_ID}, NotificationTable.ID + "=?", + new String[]{uri.getLastPathSegment()}, null, null, null); + if (idCursor != null) { + if (idCursor.moveToFirst()) { + packageId = idCursor.getInt(0); + } + idCursor.close(); + } + int result = mDbHelper.getWritableDatabase().delete(NotificationTable.TABLE_NAME, + NotificationTable.ID + "=?", new String[]{uri.getLastPathSegment()}); + removePackageIfNecessary(packageId); + notifyChange(); + return result; + default: + return 0; + } + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int match = sURIMatcher.match(uri); + switch (match) { + case MESSAGE_UPDATE_COUNT: + String formattedQuery = String.format(UPDATE_COUNT_QUERY, + System.currentTimeMillis(), uri.getLastPathSegment()); + mDbHelper.getWritableDatabase().execSQL(formattedQuery); + notifyChange(); + return 0; + default: + return 0; + } + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/cm/SpamOpenHelper.java b/packages/SystemUI/src/com/android/systemui/cm/SpamOpenHelper.java new file mode 100644 index 0000000..45dc91c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cm/SpamOpenHelper.java @@ -0,0 +1,46 @@ +package com.android.systemui.cm; + +import com.android.internal.util.cm.SpamFilter.SpamContract.NotificationTable; +import com.android.internal.util.cm.SpamFilter.SpamContract.PackageTable; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class SpamOpenHelper extends SQLiteOpenHelper { + + private static final String DATABASE_NAME = "spam.db"; + private static final int VERSION = 4; + private static final String CREATE_PACKAGES_TABLE = + "create table " + PackageTable.TABLE_NAME + "(" + + PackageTable.ID + " INTEGER PRIMARY KEY," + + PackageTable.PACKAGE_NAME + " TEXT UNIQUE);"; + private static final String CREATE_NOTIFICATIONS_TABLE = + "create table " + NotificationTable.TABLE_NAME + "(" + + NotificationTable.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + NotificationTable.PACKAGE_ID + " INTEGER," + + NotificationTable.MESSAGE_TEXT + " STRING," + + NotificationTable.LAST_BLOCKED + " INTEGER," + + NotificationTable.NORMALIZED_TEXT + " STRING," + + NotificationTable.COUNT + " INTEGER DEFAULT 0);"; + + private Context mContext; + + public SpamOpenHelper(Context context) { + super(context, DATABASE_NAME, null, VERSION); + mContext = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_PACKAGES_TABLE); + db.execSQL(CREATE_NOTIFICATIONS_TABLE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + mContext.deleteDatabase(DATABASE_NAME); + onCreate(db); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index 79174c9..f8fd25e 100644..100755 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -29,6 +29,8 @@ import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; +import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; @@ -42,6 +44,7 @@ import android.database.ContentObserver; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; @@ -87,6 +90,9 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.statusbar.StatusBarIconList; +import com.android.internal.util.cm.SpamFilter; +import com.android.internal.util.cm.SpamFilter.SpamContract.NotificationTable; +import com.android.internal.util.cm.SpamFilter.SpamContract.PackageTable; import com.android.internal.util.NotificationColorUtil; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; @@ -96,6 +102,7 @@ import com.android.systemui.SwipeHelper; import com.android.systemui.SystemUI; import com.android.systemui.assist.AssistManager; import com.android.systemui.recents.Recents; +import com.android.systemui.cm.SpamMessageProvider; import com.android.systemui.statusbar.NotificationData.Entry; import com.android.systemui.statusbar.phone.NavigationBarView; import com.android.systemui.statusbar.phone.NotificationGroupManager; @@ -145,6 +152,12 @@ public abstract class BaseStatusBar extends SystemUI implements private static final String BANNER_ACTION_SETUP = "com.android.systemui.statusbar.banner_action_setup"; + private static final Uri SPAM_MESSAGE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SpamMessageProvider.AUTHORITY) + .appendPath("message") + .build(); + protected CommandQueue mCommandQueue; protected IStatusBarService mBarService; protected H mHandler = createHandler(); @@ -920,6 +933,7 @@ public abstract class BaseStatusBar extends SystemUI implements final View settingsButton = guts.findViewById(R.id.notification_inspect_item); final View appSettingsButton = guts.findViewById(R.id.notification_inspect_app_provided_settings); + final View filterButton = guts.findViewById(R.id.notification_inspect_filter_notification); if (appUid >= 0) { final int appUidF = appUid; settingsButton.setOnClickListener(new View.OnClickListener() { @@ -929,6 +943,19 @@ public abstract class BaseStatusBar extends SystemUI implements } }); + filterButton.setVisibility(View.VISIBLE); + filterButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + ContentValues values = new ContentValues(); + String message = SpamFilter.getNotificationContent( + sbn.getNotification()); + values.put(NotificationTable.MESSAGE_TEXT, message); + values.put(PackageTable.PACKAGE_NAME, pkg); + mContext.getContentResolver().insert(SPAM_MESSAGE_URI, values); + removeNotification(sbn.getKey(), null); + } + }); + final Intent appSettingsQueryIntent = new Intent(Intent.ACTION_MAIN) .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES) @@ -958,6 +985,7 @@ public abstract class BaseStatusBar extends SystemUI implements } else { settingsButton.setVisibility(View.GONE); appSettingsButton.setVisibility(View.GONE); + filterButton.setVisibility(View.GONE); } } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index da9e883..747fae7 100644..100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -42,7 +42,9 @@ import android.app.usage.UsageStatsManagerInternal; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; +import android.database.Cursor; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; @@ -93,6 +95,7 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.AtomicFile; import android.util.Log; +import android.util.LruCache; import android.util.Slog; import android.util.Xml; import android.view.accessibility.AccessibilityEvent; @@ -102,6 +105,9 @@ import android.widget.Toast; import com.android.internal.R; import com.android.internal.statusbar.NotificationVisibility; import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.cm.SpamFilter; +import com.android.internal.util.cm.SpamFilter.SpamContract.NotificationTable; +import com.android.internal.util.cm.SpamFilter.SpamContract.PackageTable; import com.android.server.EventLogTags; import com.android.server.LocalServices; import com.android.server.SystemService; @@ -141,6 +147,8 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; /** {@hide} */ public class NotificationManagerService extends SystemService { @@ -173,6 +181,8 @@ public class NotificationManagerService extends SystemService { static final int JUNK_SCORE = -1000; static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10; static final int SCORE_DISPLAY_THRESHOLD = Notification.PRIORITY_MIN * NOTIFICATION_PRIORITY_MULTIPLIER; + private static final String IS_FILTERED_QUERY = NotificationTable.NORMALIZED_TEXT + "=? AND " + + PackageTable.PACKAGE_NAME + "=?"; // Notifications with scores below this will not interrupt the user, either via LED or // sound or vibration @@ -224,6 +234,19 @@ public class NotificationManagerService extends SystemService { private boolean mUseAttentionLight; boolean mSystemReady; + private final LruCache<Integer, FilterCacheInfo> mSpamCache; + private ExecutorService mSpamExecutor = Executors.newSingleThreadExecutor(); + + private static final Uri FILTER_MSG_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SpamFilter.AUTHORITY) + .appendPath("message") + .build(); + + private static final Uri UPDATE_MSG_URI = FILTER_MSG_URI.buildUpon() + .appendEncodedPath("inc_count") + .build(); + private boolean mDisableNotificationEffects; private int mCallState; private String mSoundNotificationKey; @@ -910,6 +933,7 @@ public class NotificationManagerService extends SystemService { public NotificationManagerService(Context context) { super(context); + mSpamCache = new LruCache<Integer, FilterCacheInfo>(100); } @Override @@ -2239,6 +2263,11 @@ public class NotificationManagerService extends SystemService { return; } + if (isNotificationSpam(notification, pkg)) { + mArchive.record(r.sbn); + return; + } + int index = indexOfNotificationLocked(n.getKey()); if (index < 0) { mNotificationList.add(r); @@ -2816,6 +2845,51 @@ public class NotificationManagerService extends SystemService { return (x < low) ? low : ((x > high) ? high : x); } + private int getNotificationHash(Notification notification, String packageName) { + CharSequence message = SpamFilter.getNotificationContent(notification); + return (message + ":" + packageName).hashCode(); + } + + private static final class FilterCacheInfo { + String packageName; + int notificationId; + } + + private boolean isNotificationSpam(Notification notification, String basePkg) { + Integer notificationHash = getNotificationHash(notification, basePkg); + boolean isSpam = false; + if (mSpamCache.get(notificationHash) != null) { + isSpam = true; + } else { + String msg = SpamFilter.getNotificationContent(notification); + Cursor c = getContext().getContentResolver().query(FILTER_MSG_URI, null, IS_FILTERED_QUERY, + new String[]{SpamFilter.getNormalizedContent(msg), basePkg}, null); + if (c != null) { + if (c.moveToFirst()) { + FilterCacheInfo info = new FilterCacheInfo(); + info.packageName = basePkg; + int notifId = c.getInt(c.getColumnIndex(NotificationTable.ID)); + info.notificationId = notifId; + mSpamCache.put(notificationHash, info); + isSpam = true; + } + c.close(); + } + } + if (isSpam) { + final int notifId = mSpamCache.get(notificationHash).notificationId; + mSpamExecutor.submit(new Runnable() { + @Override + public void run() { + Uri updateUri = Uri.withAppendedPath(UPDATE_MSG_URI, String.valueOf(notifId)); + getContext().getContentResolver().update(updateUri, new ContentValues(), + null, null); + } + }); + } + return isSpam; + } + void sendAccessibilityEvent(Notification notification, CharSequence packageName) { AccessibilityManager manager = AccessibilityManager.getInstance(getContext()); if (!manager.isEnabled()) { |