안드로이드 기기에서는 시스템 및 애플리케이션에 의해 다양한 다이얼로그가 표시됩니다. 터치 기반 인터페이스에서 이러한 팝업창들은 사용자 경험을 저해할 수 있으며, 특히 마우스가 없는 환경에서는 수동으로 닫기가 번거롭습니다. 본문에서는 화이트리스트 기반의 다이얼로그 차단 시스템을 구현하여, 데이터베이스에 등록된 조건에 따라 특정 다이얼로그의 표시를 동적으로 제어하는 방법을 설명합니다.
구현 개요
이 솔루션은 ContentProvider와 SQLite 데이터베이스를 활용하여 다이얼로그 차단 규칙을 관리합니다. AlertDialog 및 Dialog 클래스의 show() 메서드에서 데이터베이스를 조회하여 현재 다이얼로그의 패키지명, 메시지 내용, 버튼 텍스트 등이 화이트리스트와 일치하는지 확인하고, 일치할 경우 해당 다이얼로그를 자동으로 차단하거나 특정 버튼을 클릭하여 닫습니다.
Dialog 클래스 수정
Dialog.java 파일에 차단 로직을 추가합니다. 데이터베이스 조회 및 버튼 자동 클릭 기능을 포함합니다.
// frameworks/base/core/java/android/app/Dialog.java
import android.database.Cursor;
import android.net.Uri;
import android.widget.Button;
import android.widget.TextView;
public class Dialog implements DialogInterface, Window.Callback {
// 다이얼로그 제어용 URI 및 프로젝션 정의
private static final String DIALOG_AUTHORITY = "content://com.android.providers.settings.DialogControlProvider/dialog";
private Uri dialogControlUri = Uri.parse(DIALOG_AUTHORITY);
private String[] queryProjection = {"_id", "package_name", "message_content", "title_text", "button_label"};
private boolean dialogInterceptEnabled = false;
private String cachedDialogTitle = "";
private static final String LOG_TAG = "DialogControl";
// 차단 플래그 설정 메서드
public void setDialogInterceptEnabled(boolean enabled) {
dialogInterceptEnabled = enabled;
Slog.d(LOG_TAG, "Dialog intercept status: " + dialogInterceptEnabled);
}
// 뷰 계층에서 텍스트 수집
private static String extractViewText(View view) {
StringBuilder builder = new StringBuilder();
if (view instanceof TextView) {
builder.append(((TextView) view).getText().toString()).append(" ");
} else if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int idx = 0; idx < viewGroup.getChildCount(); idx++) {
builder.append(extractViewText(viewGroup.getChildAt(idx)));
}
}
return builder.toString().trim();
}
// 특정 텍스트가 포함된 버튼 검색
private Button locateButtonByText(View rootView, String targetText) {
if (rootView instanceof Button) {
Button btn = (Button) rootView;
String btnLabel = btn.getText().toString();
if (targetText != null && !targetText.isEmpty() && btnLabel != null && !btnLabel.isEmpty()) {
if (btnLabel.contains(targetText)) {
return btn;
}
}
} else if (rootView instanceof ViewGroup) {
ViewGroup container = (ViewGroup) rootView;
for (int i = 0; i < container.getChildCount(); i++) {
Button foundBtn = locateButtonByText(container.getChildAt(i), targetText);
if (foundBtn != null) {
return foundBtn;
}
}
}
return null;
}
// 차단 조건 확인
private boolean checkInterceptCondition() {
String currentPackage = mContext.getPackageName();
String collectedText = "";
Cursor cursor = mContext.getContentResolver().query(dialogControlUri, queryProjection, null, null, null);
if (cursor != null) {
try {
while (cursor.moveToNext()) {
int recordId = cursor.getInt(cursor.getColumnIndexOrThrow("_id"));
String pkgName = cursor.getString(cursor.getColumnIndexOrThrow("package_name"));
String msgContent = cursor.getString(cursor.getColumnIndexOrThrow("message_content"));
String titleText = cursor.getString(cursor.getColumnIndexOrThrow("title_text"));
String btnLabel = cursor.getString(cursor.getColumnIndexOrThrow("button_label"));
if (currentPackage != null && !currentPackage.isEmpty() && currentPackage.equals(pkgName)) {
// 버튼 자동 클릭 처리
if (btnLabel != null && !btnLabel.isEmpty()) {
Button targetButton = locateButtonByText(mDecor, btnLabel);
if (targetButton != null) {
if ((msgContent != null && !msgContent.isEmpty()) || (titleText != null && !titleText.isEmpty())) {
if (collectedText.isEmpty()) {
collectedText = extractViewText(mDecor);
}
if (!collectedText.isEmpty()) {
if (msgContent != null && collectedText.contains(msgContent)) {
targetButton.performClick();
return true;
}
if (titleText != null && collectedText.contains(titleText)) {
targetButton.performClick();
return true;
}
}
} else {
targetButton.performClick();
return true;
}
}
}
// 메시지 또는 제목 기반 차단
if ((msgContent != null && !msgContent.isEmpty()) || (titleText != null && !titleText.isEmpty())) {
if (collectedText.isEmpty()) {
collectedText = extractViewText(mDecor);
}
if (!collectedText.isEmpty()) {
if (msgContent != null && collectedText.contains(msgContent)) {
return true;
}
if (titleText != null && collectedText.contains(titleText)) {
return true;
}
}
}
}
}
} finally {
cursor.close();
}
}
return false;
}
public void show() {
dialogInterceptEnabled = false;
if (mShowing) {
if (mDecor != null) {
if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
}
}
return;
}
// ... 기존 코드 ...
if (dialogInterceptEnabled) {
Log.d(LOG_TAG, "Dialog blocked by intercept flag");
return;
}
if (checkInterceptCondition()) {
Log.d(LOG_TAG, "Dialog blocked by whitelist rule");
return;
}
mWindowManager.addView(mDecor, l);
mShowing = true;
}
public void setTitle(@Nullable CharSequence title) {
if (title != null) {
cachedDialogTitle = title.toString();
}
mWindow.setTitle(title);
mWindow.getAttributes().setTitle(title);
}
}
AlertDialog 클래스 수정
AlertDialog.java에서 onCreate() 메서드에 차단 로직을 추가합니다.
// frameworks/base/core/java/android/app/AlertDialog.java
import android.database.Cursor;
import android.net.Uri;
public class AlertDialog extends Dialog implements DialogInterface {
private Uri controlUri = Uri.parse("content://com.android.providers.settings.DialogControlProvider/dialog");
private String[] projection = {"_id", "package_name", "message_content", "title_text", "button_label"};
private boolean shouldBlockDialog = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAlert.installContent();
shouldBlockDialog = evaluateBlockCondition();
setDialogInterceptEnabled(shouldBlockDialog);
}
private boolean evaluateBlockCondition() {
String packageName = mContext.getPackageName();
CharSequence titleSequence = mAlert.getTitle();
CharSequence messageSequence = mAlert.getMessage();
String title = (titleSequence != null && titleSequence.length() > 0) ? titleSequence.toString() : "";
String message = (messageSequence != null && messageSequence.length() > 0) ? messageSequence.toString() : "";
Cursor cursor = mContext.getContentResolver().query(controlUri, projection, null, null, null);
boolean hasValidTitle = !title.isEmpty();
boolean hasValidMessage = !message.isEmpty();
if (cursor != null) {
try {
while (cursor.moveToNext()) {
String pkg = cursor.getString(cursor.getColumnIndexOrThrow("package_name"));
String targetMsg = cursor.getString(cursor.getColumnIndexOrThrow("message_content"));
String targetTitle = cursor.getString(cursor.getColumnIndexOrThrow("title_text"));
if (packageName != null && !packageName.isEmpty() && packageName.equals(pkg)) {
if (hasValidMessage && targetMsg != null && message.contains(targetMsg)) {
return true;
}
if (hasValidTitle && targetTitle != null && title.contains(targetTitle)) {
return true;
}
}
}
} finally {
cursor.close();
}
}
return false;
}
}
ContentProvider 구현
화이트리스트 데이터를 제공하는 ContentProvider를 생성합니다.
// frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/DialogControlProvider.java
package com.android.providers.settings;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Binder;
public class DialogControlProvider extends ContentProvider {
private static final String AUTHORITY = "com.android.providers.settings.DialogControlProvider";
private static final String TABLE_PATH = "dialog";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + TABLE_PATH);
private static final int MATCH_ALL = 1;
private static final int MATCH_ID = 2;
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI(AUTHORITY, TABLE_PATH, MATCH_ALL);
uriMatcher.addURI(AUTHORITY, TABLE_PATH + "/#", MATCH_ID);
}
private DialogDatabaseHelper dbHelper;
@Override
public boolean onCreate() {
dbHelper = new DialogDatabaseHelper(getContext());
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor;
switch (uriMatcher.match(uri)) {
case MATCH_ALL:
cursor = db.query(DialogDatabaseHelper.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
break;
case MATCH_ID:
selection = DialogDatabaseHelper.COLUMN_ID + "=?";
selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
cursor = db.query(DialogDatabaseHelper.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
final long token = Binder.clearCallingIdentity();
try {
cursor.setNotificationUri(getContext().getContentResolver(), uri);
} finally {
Binder.restoreCallingIdentity(token);
}
return cursor;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
if (uriMatcher.match(uri) != MATCH_ALL) {
throw new IllegalArgumentException("Invalid URI for insert: " + uri);
}
long rowId = db.insert(DialogDatabaseHelper.TABLE_NAME, null, values);
if (rowId > 0) {
Uri newUri = ContentUris.withAppendedId(CONTENT_URI, rowId);
final long token = Binder.clearCallingIdentity();
try {
getContext().getContentResolver().notifyChange(newUri, null);
} finally {
Binder.restoreCallingIdentity(token);
}
return newUri;
}
throw new SQLException("Failed to insert record into " + uri);
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int count;
switch (uriMatcher.match(uri)) {
case MATCH_ALL:
count = db.delete(DialogDatabaseHelper.TABLE_NAME, selection, selectionArgs);
break;
case MATCH_ID:
selection = DialogDatabaseHelper.COLUMN_ID + "=?";
selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
count = db.delete(DialogDatabaseHelper.TABLE_NAME, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
final long token = Binder.clearCallingIdentity();
try {
getContext().getContentResolver().notifyChange(uri, null);
} finally {
Binder.restoreCallingIdentity(token);
}
return count;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int count;
switch (uriMatcher.match(uri)) {
case MATCH_ALL:
count = db.update(DialogDatabaseHelper.TABLE_NAME, values, selection, selectionArgs);
break;
case MATCH_ID:
selection = DialogDatabaseHelper.COLUMN_ID + "=?";
selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
count = db.update(DialogDatabaseHelper.TABLE_NAME, values, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
final long token = Binder.clearCallingIdentity();
try {
getContext().getContentResolver().notifyChange(uri, null);
} finally {
Binder.restoreCallingIdentity(token);
}
return count;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case MATCH_ALL:
return "vnd.android.cursor.dir/" + AUTHORITY + "." + TABLE_PATH;
case MATCH_ID:
return "vnd.android.cursor.item/" + AUTHORITY + "." + TABLE_PATH;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
}
데이터베이스 헬퍼 구현
화이트리스트 저장을 위한 SQLite 데이터베이스 헬퍼 클래스입니다.
// frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/DialogDatabaseHelper.java
package com.android.providers.settings;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class DialogDatabaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "dialog_control.db";
private static final int DATABASE_VERSION = 1;
public static final String TABLE_NAME = "dialog";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_PACKAGE = "package_name";
public static final String COLUMN_MESSAGE = "message_content";
public static final String COLUMN_TITLE = "title_text";
public static final String COLUMN_BUTTON = "button_label";
private static final String CREATE_TABLE_SQL =
"CREATE TABLE " + TABLE_NAME + " (" +
COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
COLUMN_PACKAGE + " TEXT, " +
COLUMN_MESSAGE + " TEXT, " +
COLUMN_TITLE + " TEXT, " +
COLUMN_BUTTON + " TEXT);";
public DialogDatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_SQL);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
}
AndroidManifest.xml 수정
ContentProvider를 시스템에 등록합니다.
<!-- frameworks/base/packages/SettingsProvider/AndroidManifest.xml -->
<application>
<provider
android:name=".DialogControlProvider"
android:authorities="com.android.providers.settings.DialogControlProvider"
android:multiprocess="false"
android:exported="true"
android:singleUser="true"
android:initOrder="100" />
</application>
AlertController 수정
제목과 메시지에 접근할 수 있도록 getter 메서드를 추가합니다.
// frameworks/base/core/java/com/android/internal/app/AlertController.java
public class AlertController {
// 기존 코드...
public CharSequence getTitle() {
return mTitle;
}
public CharSequence getMessage() {
return mMessage;
}
}