- CardNotifListenerService: 결제 알림(금액+승인/결제/카드) 가로채 백엔드로 전송 (토큰은 Capacitor Preferences에서, 결제성 알림만 전송) - CardNotifPlugin: 알림 접근 권한 확인/설정 열기 - AndroidManifest 서비스 등록, MainActivity 플러그인 등록 - 가계부 화면: 네이티브 미허용 시 '알림 접근 권한' 안내 배너 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package kr.sblog.slimbudget;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.NotificationListenerService;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 카드사 결제 푸시 알림을 가로채 백엔드로 전달한다.
|
||||
* - 결제로 보이는 알림(금액+승인/결제/카드)만 전송 (개인정보 최소화)
|
||||
* - 인증 토큰은 Capacitor Preferences(SharedPreferences "CapacitorStorage")의 token 사용
|
||||
* - 백엔드가 파싱·중복제거·미확인 내역 생성
|
||||
*/
|
||||
public class CardNotifListenerService extends NotificationListenerService {
|
||||
|
||||
private static final String API_URL = "https://app.sblog.kr/api/account/entries/notification";
|
||||
|
||||
@Override
|
||||
public void onNotificationPosted(StatusBarNotification sbn) {
|
||||
try {
|
||||
if (sbn == null || sbn.getNotification() == null) return;
|
||||
Bundle ex = sbn.getNotification().extras;
|
||||
if (ex == null) return;
|
||||
|
||||
CharSequence titleCs = ex.getCharSequence(Notification.EXTRA_TITLE);
|
||||
CharSequence textCs = ex.getCharSequence(Notification.EXTRA_TEXT);
|
||||
CharSequence bigCs = ex.getCharSequence(Notification.EXTRA_BIG_TEXT);
|
||||
|
||||
String title = titleCs == null ? "" : titleCs.toString();
|
||||
String text = (bigCs != null ? bigCs : (textCs == null ? "" : textCs)).toString();
|
||||
|
||||
if (!looksLikePayment(title + " " + text)) return;
|
||||
|
||||
String token = getSharedPreferences("CapacitorStorage", MODE_PRIVATE)
|
||||
.getString("token", null);
|
||||
if (token == null || token.isEmpty()) return;
|
||||
|
||||
final String pkg = sbn.getPackageName();
|
||||
new Thread(() -> post(pkg, title, text, token)).start();
|
||||
} catch (Exception ignore) {
|
||||
// 알림 처리 실패는 무시 (앱 동작에 영향 없도록)
|
||||
}
|
||||
}
|
||||
|
||||
/** 결제 알림 후보 판별: 금액(####원) + 승인/결제/카드 키워드 */
|
||||
private boolean looksLikePayment(String s) {
|
||||
boolean money = s.matches("(?s).*\\d[\\d,]{2,}\\s*원.*");
|
||||
boolean kw = s.contains("승인") || s.contains("결제") || s.contains("카드");
|
||||
return money && kw;
|
||||
}
|
||||
|
||||
private void post(String pkg, String title, String text, String token) {
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
URL url = new URL(API_URL);
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("Authorization", "Bearer " + token);
|
||||
conn.setConnectTimeout(8000);
|
||||
conn.setReadTimeout(8000);
|
||||
conn.setDoOutput(true);
|
||||
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("app", pkg);
|
||||
body.put("title", title);
|
||||
body.put("text", text);
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(body.toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
conn.getResponseCode(); // 요청 전송 (응답은 무시)
|
||||
} catch (Exception ignore) {
|
||||
// 네트워크 실패는 무시
|
||||
} finally {
|
||||
if (conn != null) conn.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package kr.sblog.slimbudget;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.provider.Settings;
|
||||
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
|
||||
/**
|
||||
* 카드 결제 알림 자동인식 플러그인.
|
||||
* - isEnabled: 알림 접근 권한(NotificationListener) 허용 여부
|
||||
* - openSettings: 알림 접근 설정 화면 열기
|
||||
* 실제 알림 수신/전송은 CardNotifListenerService 가 담당한다.
|
||||
*/
|
||||
@CapacitorPlugin(name = "CardNotif")
|
||||
public class CardNotifPlugin extends Plugin {
|
||||
|
||||
@PluginMethod
|
||||
public void isEnabled(PluginCall call) {
|
||||
String pkg = getContext().getPackageName();
|
||||
String flat = Settings.Secure.getString(
|
||||
getContext().getContentResolver(), "enabled_notification_listeners");
|
||||
boolean enabled = flat != null && flat.contains(pkg);
|
||||
JSObject ret = new JSObject();
|
||||
ret.put("enabled", enabled);
|
||||
call.resolve(ret);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void openSettings(PluginCall call) {
|
||||
Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
getContext().startActivity(intent);
|
||||
call.resolve();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
package kr.sblog.slimbudget;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {}
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
registerPlugin(CardNotifPlugin.class);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user