diff --git a/.gitignore b/.gitignore index 6033bd1..c26288b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,10 @@ app/build/ build/ local.properties +keystore.properties *.hprof *.apk *.aab *.iml +*.jks +*.keystore diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f424071..8ad5f25 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,8 +1,19 @@ +import java.util.Properties + plugins { id("com.android.application") id("org.jetbrains.kotlin.plugin.compose") } +val keystorePropertiesFile = rootProject.file("keystore.properties") +val keystoreProperties = Properties().apply { + if (keystorePropertiesFile.exists()) { + keystorePropertiesFile.inputStream().use(::load) + } +} +val hasReleaseSigning = listOf("storeFile", "storePassword", "keyAlias", "keyPassword") + .all { keystoreProperties.getProperty(it).isNullOrBlank().not() } + android { namespace = "com.example.instanoreels" compileSdk = 36 @@ -17,9 +28,23 @@ android { buildConfigField("String", "INSTAGRAM_PACKAGE", "\"com.instagram.android\"") } + signingConfigs { + if (hasReleaseSigning) { + create("release") { + storeFile = rootProject.file(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("storePassword") + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + } + } + } + buildTypes { release { isMinifyEnabled = false + if (hasReleaseSigning) { + signingConfig = signingConfigs.getByName("release") + } proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" diff --git a/app/src/main/java/com/example/instanoreels/accessibility/InstagramBlockerAccessibilityService.kt b/app/src/main/java/com/example/instanoreels/accessibility/InstagramBlockerAccessibilityService.kt index 8d37656..8e2bc08 100644 --- a/app/src/main/java/com/example/instanoreels/accessibility/InstagramBlockerAccessibilityService.kt +++ b/app/src/main/java/com/example/instanoreels/accessibility/InstagramBlockerAccessibilityService.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import java.text.Normalizer class InstagramBlockerAccessibilityService : AccessibilityService() { private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) @@ -192,16 +193,25 @@ class InstagramBlockerAccessibilityService : AccessibilityService() { } private fun shouldBackOutOfDmViewer(area: InstagramArea, snapshot: InstagramScreenSnapshot): Boolean { - val text = snapshot.searchableText - val looksLikeDmReelViewer = text.contains("reply to") || - text.contains("reply to yourself") || - text.contains("send to") || - text.contains("see more") || - text.contains("antworten") || - text.contains("senden an") + val text = snapshot.searchableText.foldForMatching() + val looksLikeDmReelViewer = listOf( + "reply to", + "reply to yourself", + "send to", + "see more", + "antworten", + "senden an", + "an chat senden", + "an chats senden", + "teilen mit", + "weiterleiten", + ).any { text.contains(it.foldForMatching()) } + // Any blocked Reel surface is safer to leave with Back. Instagram uses the + // middle-bottom area for different controls inside Reel viewers, so a + // coordinate tap there can hit "Send to" instead of the DM tab. return area == InstagramArea.DM_THREAD || - area == InstagramArea.REELS && inReelViewerFromDm || + area == InstagramArea.REELS || looksLikeDmReelViewer } @@ -217,10 +227,15 @@ class InstagramBlockerAccessibilityService : AccessibilityService() { get() = applicationContext.packageName } +private fun String.foldForMatching(): String { + return Normalizer.normalize(lowercase(), Normalizer.Form.NFD) + .replace(Regex("\\p{Mn}+"), "") + .replace("ß", "ss") +} + private fun InstagramArea.isRedirectableBlockedArea(): Boolean { return this in setOf( InstagramArea.HOME_FEED, - InstagramArea.REELS, InstagramArea.EXPLORE, InstagramArea.SEARCH, ) diff --git a/app/src/main/java/com/example/instanoreels/accessibility/InstagramScreenClassifier.kt b/app/src/main/java/com/example/instanoreels/accessibility/InstagramScreenClassifier.kt index 07495e7..0531ebe 100644 --- a/app/src/main/java/com/example/instanoreels/accessibility/InstagramScreenClassifier.kt +++ b/app/src/main/java/com/example/instanoreels/accessibility/InstagramScreenClassifier.kt @@ -3,6 +3,7 @@ package com.example.instanoreels.accessibility import com.example.instanoreels.data.BlockerSettings import com.example.instanoreels.model.BlockDecision import com.example.instanoreels.model.InstagramArea +import java.text.Normalizer class InstagramScreenClassifier { fun classify(snapshot: InstagramScreenSnapshot): InstagramArea { @@ -111,10 +112,14 @@ class InstagramScreenClassifier { "suggested for you", "for you", "vorgeschlagen", + "vorschläge", + "vorschlaege", + "vorschlage", + "weitere reels", "für dich", "fur dich", ) - val reelRecommendationKeywords = suggestedKeywords + listOf("more reels", "mehr reels") + val reelRecommendationKeywords = suggestedKeywords + listOf("more reels", "mehr reels", "weitere reels") val homeKeywords = listOf("home", "startseite", "feed") val dmInboxKeywords = listOf("messages", "direct", "inbox", "chats", "nachrichten", "postfach") val dmInboxPreviewKeywords = listOf( @@ -142,7 +147,17 @@ class InstagramScreenClassifier { } private fun String.containsAny(keywords: List): Boolean { - return keywords.any { contains(it.lowercase()) } + val foldedText = foldForMatching() + return keywords.any { keyword -> + val foldedKeyword = keyword.foldForMatching() + contains(keyword.lowercase()) || foldedText.contains(foldedKeyword) + } +} + +private fun String.foldForMatching(): String { + return Normalizer.normalize(lowercase(), Normalizer.Form.NFD) + .replace(Regex("\\p{Mn}+"), "") + .replace("ß", "ss") } private fun String.looksLikeAuthScreen(): Boolean {