package me.kr328.nevo.decorators.smscaptcha; import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.os.UserManager; import android.util.Log; import android.widget.Toast; import com.oasisfeng.nevo.sdk.MutableNotification; import com.oasisfeng.nevo.sdk.MutableStatusBarNotification; import net.grandcentrix.tray.AppPreferences; import net.grandcentrix.tray.core.TrayItem; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Objects; import java.util.TreeSet; import java.util.regex.Pattern; import me.kr328.nevo.decorators.smscaptcha.utils.CaptchaUtils; import me.kr328.nevo.decorators.smscaptcha.utils.MessageUtils; import me.kr328.nevo.decorators.smscaptcha.utils.NotificationUtils; import me.kr328.nevo.decorators.smscaptcha.utils.PackageUtils; import me.kr328.nevo.decorators.smscaptcha.utils.PatternUtils; public class CaptchaDecoratorService extends BaseSmsDecoratorService { public static final String TAG = CaptchaDecoratorService.class.getSimpleName(); public static final String[] TARGET_PACKAGES = new String[]{"com.android.messaging", "com.google.android.apps.messaging", "com.android.mms" ,"com.sonyericsson.conversations" ,"com.moez.QKSMS"}; public static final String NOTIFICATION_CHANNEL_CAPTCHA_NORMAL = "notification_channel_captcha_normal"; public static final String NOTIFICATION_CHANNEL_CAPTCHA_SILENT = "notification_channel_captcha_silent"; public static final String NOTIFICATION_EXTRA_RECAST = Global.PREFIX_NOTIFICATION_EXTRA + ".captcha.notification.recast"; private Settings mSettings; private CaptchaUtils mCaptchaUtils; private TreeSet<String> mAppliedKeys = new TreeSet<>(); private BroadcastReceiver mKeyguardReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Bundle fillBundle = new Bundle(); fillBundle.putBoolean(NOTIFICATION_EXTRA_RECAST, true); recastAllNotifications(fillBundle); } }; @Override protected void apply(MutableStatusBarNotification evolving) { MutableNotification notification = evolving.getNotification(); Bundle extras = notification.extras; boolean recast = extras.getBoolean(NOTIFICATION_EXTRA_RECAST, false); NotificationUtils.Messages messages = NotificationUtils.parseMessages(notification); String[] captchas = mCaptchaUtils.findSmsCaptchas(messages.texts); if (captchas.length == 0) return; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) notification.setChannelId(recast ? NOTIFICATION_CHANNEL_CAPTCHA_SILENT : NOTIFICATION_CHANNEL_CAPTCHA_NORMAL); else notification.priority = recast ? Notification.PRIORITY_LOW : Notification.PRIORITY_HIGH; KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); if (mSettings.isCaptchaHideOnLocked() && keyguardManager != null && keyguardManager.isKeyguardLocked()) applyKeyguardLocked(notification, evolving.getKey(), messages, captchas); else applyKeyguardUnlocked(notification, evolving.getKey(), messages, captchas); notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE; notification.visibility = Notification.VISIBILITY_PUBLIC; extras.putBoolean(Global.NOTIFICATION_EXTRA_APPLIED, true); mAppliedKeys.add(evolving.getKey()); Log.i(TAG, "Applied " + evolving.getKey()); } private void applyKeyguardLocked(Notification notification, String key, NotificationUtils.Messages messages , String[] captchas) { Notification.Action[] actions = new Notification.Action[] { createNonIconAction(key ,getString(R.string.captcha_service_notification_locked_action_copy_code) ,new CaptchaMessage(messages ,captchas[0])) }; NotificationUtils.replaceMessages(notification ,text -> CaptchaUtils.replaceCaptchaWithChar(text ,captchas ,'*')); if ( mSettings.isCaptchaOverrideDefaultAction() ) replaceActions(notification ,key ,actions); else appendActions(notification ,key ,actions); } private void applyKeyguardUnlocked(Notification notification, String key, NotificationUtils.Messages messages, String[] captchas) { Notification.Action[] actions = Arrays.stream(captchas). map(captcha -> createNonIconAction(key ,getString(R.string.captcha_service_notification_unlocked_action_copy_code_format ,captcha) ,new CaptchaMessage(messages ,captcha))). toArray(Notification.Action[]::new); NotificationUtils.rebuildMessageStyle(notification); if ( mSettings.isCaptchaOverrideDefaultAction() ) replaceActions(notification ,key ,actions); else appendActions(notification ,key ,actions); } private void loadSettings() { if (Objects.requireNonNull(getSystemService(UserManager.class)).isUserUnlocked()) { AppPreferences mAppPreferences = new AppPreferences(this); mSettings = Settings.defaultValueFromContext(this).readFromTrayPreference(mAppPreferences); mAppPreferences.registerOnTrayPreferenceChangeListener(this::onSettingsChanged); } else { mSettings = Settings.defaultValueFromContext(createDeviceProtectedStorageContext()); } } private void createNotificationChannels() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channelNormal = new NotificationChannel(NOTIFICATION_CHANNEL_CAPTCHA_NORMAL, getString(R.string.captcha_service_notification_channel_name), NotificationManager.IMPORTANCE_HIGH); NotificationChannel channelSilent = new NotificationChannel(NOTIFICATION_CHANNEL_CAPTCHA_SILENT, getString(R.string.captcha_service_notification_channel_name), NotificationManager.IMPORTANCE_LOW); ArrayList<NotificationChannel> notificationChannels = new ArrayList<>(); notificationChannels.add(channelNormal); notificationChannels.add(channelSilent); for (String packageName : TARGET_PACKAGES) if (PackageUtils.hasPackageInstalled(this ,packageName)) createNotificationChannels(packageName, notificationChannels); } } private void initCaptchaUtils() { mCaptchaUtils = new CaptchaUtils(mSettings.isCaptchaUseDefaultPattern() , PatternUtils.compilePattern(mSettings.getCaptchaIdentifyPattern(), "" ,Pattern.CASE_INSENSITIVE), PatternUtils.compilePattern(mSettings.getCaptchaParsePattern(), "" ,Pattern.CASE_INSENSITIVE)); Log.d(TAG ,"CaptchaUtils " + mSettings.isCaptchaUseDefaultPattern() + " " + mSettings.getCaptchaIdentifyPattern() + " " + mSettings.getCaptchaParsePattern()); } private void recastAllNotifications(Bundle fillInExtras) { for (String key : mAppliedKeys) recastNotification(key, fillInExtras); mAppliedKeys.clear(); } private void copyCaptcha(String captcha , NotificationUtils.Messages messages) { ((ClipboardManager) Objects.requireNonNull(getSystemService(Context.CLIPBOARD_SERVICE))). setPrimaryClip(ClipData.newPlainText("SmsCaptcha", captcha)); switch (mSettings.getCaptchaPostCopyAction()) { case Settings.POST_ACTION_DELETE : Arrays.stream(messages.texts).forEach(t -> MessageUtils.delete(this , t)); break; case Settings.POST_ACTION_MARK_AS_READ : Arrays.stream(messages.texts).forEach(t -> MessageUtils.markAsRead(this , t)); break; } Toast.makeText(this, getString(R.string.captcha_service_toast_copied_format, captcha), Toast.LENGTH_LONG).show(); } @Override public void onUserUnlocked() { this.loadSettings(); } @Override public void onActionClicked(String key ,Parcelable cookies) { CaptchaMessage captchaMessage = (CaptchaMessage) cookies; if ( captchaMessage == null ) return; copyCaptcha(captchaMessage.captcha ,captchaMessage.messages); } private void registerReceivers() { registerReceiver(mKeyguardReceiver, new IntentFilter() {{ addAction(Intent.ACTION_USER_PRESENT); addAction(Intent.ACTION_SCREEN_OFF); }}); } private void unregisterReceivers() { unregisterReceiver(mKeyguardReceiver); } @Override protected void onNotificationRemoved(String key, int reason) { super.onNotificationRemoved(key ,reason); mAppliedKeys.remove(key); } @Override protected void onConnected() { super.onConnected(); loadSettings(); createNotificationChannels(); initCaptchaUtils(); registerReceivers(); } private void onSettingsChanged(Collection<TrayItem> trayItems) { for (TrayItem item : trayItems) { switch (item.key()) { case Settings.SETTING_CAPTCHA_IDENTIFY_PATTERN: mSettings.setCaptchaIdentifyPattern(item.value()); initCaptchaUtils(); break; case Settings.SETTING_CAPTCHA_PARSE_PATTERN: mSettings.setCaptchaParsePattern(item.value()); initCaptchaUtils(); break; case Settings.SETTING_CAPTCHA_USE_DEFAULT_PATTERN : mSettings.setCaptchaUseDefaultPattern(Boolean.parseBoolean(item.value())); initCaptchaUtils(); break; case Settings.SETTING_CAPTCHA_HIDE_ON_LOCKED : mSettings.setCaptchaHideOnLocked(Boolean.parseBoolean(item.value())); break; case Settings.SETTING_CAPTCHA_OVERRIDE_DEFAULT_ACTION : mSettings.setCaptchaOverrideDefaultAction(Boolean.parseBoolean(item.value())); break; case Settings.SETTING_CAPTCHA_POST_COPY_ACTION : mSettings.setCaptchaPostCopyAction(Integer.parseInt(Objects.requireNonNull(item.value()))); break; } Log.i(TAG ,"Settings Updated " + item.key() + "=" + item.value()); } } @Override public void onDestroy() { super.onDestroy(); unregisterReceivers(); } static class CaptchaMessage implements Parcelable { NotificationUtils.Messages messages; String captcha; CaptchaMessage(NotificationUtils.Messages messages, String captcha) { this.messages = messages; this.captcha = captcha; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(messages.texts.length); for (CharSequence s : messages.texts) dest.writeString(s.toString()); dest.writeString(captcha); } public static final Creator<CaptchaMessage> CREATOR = new Creator<CaptchaMessage>() { @Override public CaptchaMessage createFromParcel(Parcel source) { NotificationUtils.Messages messages = new NotificationUtils.Messages(); messages.texts = new String[source.readInt()]; for ( int i = 0 ; i < messages.texts.length ; i++ ) messages.texts[i] = source.readString(); return new CaptchaMessage(messages ,source.readString()); } @Override public CaptchaMessage[] newArray(int size) { return new CaptchaMessage[size]; } }; } }