diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..c264799 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 82b4fc5..8277719 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.android.application' + id 'org.jetbrains.kotlin.android' } android { @@ -23,12 +24,14 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } } dependencies { + //implementation 'androidx.core:core-ktx:+' + // implementation 'androidx.appcompat:appcompat:1.6.1' // implementation 'com.google.android.material:material:1.8.0' diff --git a/app/src/main/java/org/fischman/noexplore/NoExploreService.java b/app/src/main/java/org/fischman/noexplore/NoExploreService.java deleted file mode 100644 index 7d304fd..0000000 --- a/app/src/main/java/org/fischman/noexplore/NoExploreService.java +++ /dev/null @@ -1,224 +0,0 @@ -package org.fischman.noexplore; - -import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; - -import android.accessibilityservice.AccessibilityService; -import android.accessibilityservice.AccessibilityServiceInfo; -import android.accessibilityservice.GestureDescription; -import android.graphics.Color; -import android.graphics.Path; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.hardware.display.DisplayManager; -import android.util.Log; -import android.view.Gravity; -import android.view.View; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.LinearLayout; -import android.widget.TextView; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class NoExploreService extends AccessibilityService { - private final boolean DEBUG = false; - - private void emit(String msg) { if (DEBUG) Log.e("AMI", msg); } - - // Time (epoch millis) of the last event that was acted on for each type. - // Used to throttle actions, in preference to a longer android:notificationTimeout value because - // that also slows down the first action instead of only subsequent ones. - private Map lastEventTime = new HashMap(); - - private TextView obscuringView; - - public NoExploreService() {} - - @Override - public void onServiceConnected() { - super.onServiceConnected(); - AccessibilityServiceInfo info = getServiceInfo(); - if (info == null) { - Log.e("AMI", "getServiceInfo() returned null!"); - return; - } - info.flags |= AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS; - info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; - setServiceInfo(info); - - TextView tv = new TextView(this); - tv.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT, 1F)); - tv.setGravity(Gravity.CENTER); - tv.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); - tv.setText("\"Suggested posts\" being obscured by " + getString(R.string.app_name)); - tv.setTextColor(Color.YELLOW); - tv.setBackgroundColor(Color.BLACK); - tv.setTextSize(35F); - obscuringView = tv; - } - - private static String emptyIfNull(CharSequence s) { return s != null ? s.toString() : ""; } - - // Handy for debugging. - private void dumpRecursively(AccessibilityNodeInfo node) { - dumpRecursively("", node); - } - private void dumpRecursively(String prefix, AccessibilityNodeInfo node) { - if (!DEBUG || node == null) return; - emit(prefix + node.getText() + " - " + node.getHintText() + " - " + node.getTooltipText() + " - " + node.getViewIdResourceName()); - int count = node.getChildCount(); - for (int i = 0; i < count; ++i) { - dumpRecursively(prefix + " ", node.getChild(i)); - } - } - - @Override - public void onAccessibilityEvent(AccessibilityEvent event) { - if (event.getPackageName() == null) { - if (event.getEventType() == AccessibilityEvent.TYPE_WINDOWS_CHANGED && - (event.getWindowChanges() & AccessibilityEvent.WINDOWS_CHANGE_ACTIVE) != 0) { - emit("hideObscuringOverlay because TYPE_WINDOWS_CHANGED from null package"); - hideObscuringOverlay(); - } - return; - } - - long eventTime = event.getEventTime(); - if (eventTime - lastEventTime.getOrDefault(event.getEventType(), 0L) < 500) return; - if (DEBUG) emit("event is " + event); - - switch (event.getPackageName().toString()) { - case "com.instagram.android": - onInstagramEvent(event); - return; - case "com.google.android.apps.maps": - onMapsEvent(event); - return; - default: - return; - } - } - - private AccessibilityNodeInfo firstDescendant(AccessibilityNodeInfo source, String viewID) { - if (source == null) return null; - List candidates = source.findAccessibilityNodeInfosByViewId(viewID); - if (candidates.isEmpty()) return null; - if (candidates.size() > 1 && DEBUG) - emit("Multiple descendants with view ID " + viewID + ": " + candidates); - AccessibilityNodeInfo found = candidates.iterator().next(); - if (!found.isVisibleToUser()) return null; - return found; - } - - private void onMapsEvent(AccessibilityEvent event) { - long eventTime = event.getEventTime(); - int eventType = event.getEventType(); - if (eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) return; - AccessibilityNodeInfo found = firstDescendant(event.getSource(), "com.google.android.apps.maps:id/explore_tab_home_title_card"); - if (found == null) return; - Rect r = new Rect(); - found.getBoundsInWindow(r); - // Don't process the view until it's popped up enough for drag-down to be meaningful. - if (r.top > 2000) return; - // Don't process the view if its bounds don't make sense (e.g. overlapping notification bar). - if (r.top < 10) return; - - lastEventTime.put(eventType, eventTime); - Path path = new Path(); - path.moveTo((r.left+r.right)/2, r.top+5); - path.lineTo((r.left+r.right)/2, r.top+200); - GestureDescription gesture = new GestureDescription.Builder() - .addStroke(new GestureDescription.StrokeDescription(path, 0, 1)) - .build(); - Boolean dispatched = dispatchGesture(gesture, new GestureResultCallback() { - @Override - public void onCompleted(GestureDescription gestureDescription) { emit("Gesture completed"); } - @Override - public void onCancelled(GestureDescription gestureDescription) { emit("Gesture cancelled"); } - }, null); - emit("Dispatch gesture for maps: " + dispatched); - if (DEBUG) { - emit("Gesture because saw maps \"Latest in\" in source, which follows after source: " + event.getSource()); - dumpRecursively(event.getSource()); - } - } - - private int profileTabTop = -1; - private int profileTabTop(AccessibilityNodeInfo source) { - if (profileTabTop > 0) { return profileTabTop; } - AccessibilityNodeInfo profileTab = firstDescendant(source, "com.instagram.android:id/profile_tab"); - if (profileTab == null) return 0; - Rect rect = new Rect(); - profileTab.getBoundsInWindow(rect); - emit("profileTab: " + rect); - profileTabTop = rect.top; - return profileTabTop; - } - - - private void onInstagramEvent(AccessibilityEvent event) { - long eventTime = event.getEventTime(); - int eventType = event.getEventType(); - - // TYPE_VIEW_SELECTED is delivered a bit faster than TYPE_VIEW_CLICK so use that, but also - // is delivered multiple times, hence the eventTime-based throttling. - if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) { - AccessibilityNodeInfo search = firstDescendant(event.getSource(), "com.instagram.android:id/search_tab"); - AccessibilityNodeInfo reels = firstDescendant(event.getSource(), "com.instagram.android:id/clips_tab"); - if (search != null || reels != null) { - lastEventTime.put(eventType, eventTime); - if (DEBUG) emit("BACK because search or reels is not null: " + search + ", " + reels); - performGlobalAction(GLOBAL_ACTION_BACK); - } - return; - } - - if (eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) return; - AccessibilityNodeInfo found = firstDescendant(event.getSource(), "com.instagram.android:id/end_of_feed_demarcator_container"); - if (found == null) { hideObscuringOverlay(); return; } - Rect r = new Rect(); - found.getBoundsInScreen(r); - lastEventTime.put(eventType, eventTime); - emit("Obscuring view because end_of_feed_demarcator is at " + r); - showObscuringOverlay(r.bottom, profileTabTop(event.getSource())); - } - - private WindowManager getWindowManager() { return (WindowManager) getSystemService(WINDOW_SERVICE); } - private DisplayManager getDisplayManager() { return (DisplayManager) getSystemService(DISPLAY_SERVICE); } - private void hideObscuringOverlay() { - emit("hideObscuringOverlay"); - if (obscuringView.getParent() == null) { - emit("hideObscuringOverlay no-op'd"); - return; - } - getWindowManager().removeView(obscuringView); - } - - private void showObscuringOverlay(int top, int bottom) { - emit("showObscuringOverlay: " + top +" - " + bottom); - // Force always being able to see a strip at the top for scrolling stories back into view. - if (top < 400) top = 400; - WindowManager wm = getWindowManager(); - WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); - lp.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; - lp.format = PixelFormat.OPAQUE; - lp.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - lp.width = MATCH_PARENT; - lp.height = bottom - top; // wm.getMaximumWindowMetrics().getBounds().height() - top - bottom; - lp.gravity = Gravity.BOTTOM; - lp.y = wm.getMaximumWindowMetrics().getBounds().height() - bottom; - if (obscuringView.getParent() == null) { - emit(" showObscuringOverlay - addView"); - wm.addView(obscuringView, lp); - } else { - emit(" showObscuringOverlay - updateView"); - wm.updateViewLayout(obscuringView, lp); - } - } - - @Override - public void onInterrupt() {} -} diff --git a/app/src/main/java/org/fischman/noexplore/NoExploreService.kt b/app/src/main/java/org/fischman/noexplore/NoExploreService.kt new file mode 100644 index 0000000..72cb398 --- /dev/null +++ b/app/src/main/java/org/fischman/noexplore/NoExploreService.kt @@ -0,0 +1,230 @@ +package org.fischman.noexplore + +import android.accessibilityservice.AccessibilityService +import android.accessibilityservice.AccessibilityServiceInfo +import android.accessibilityservice.GestureDescription +import android.accessibilityservice.GestureDescription.StrokeDescription +import android.annotation.SuppressLint +import android.graphics.Color +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.Rect +import android.util.Log +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import android.widget.LinearLayout +import android.widget.TextView + +class NoExploreService : AccessibilityService() { + private val debug = true + private fun emit(msg: String) { + if (debug) Log.e("AMI", msg) + } + + // Time (epoch millis) of the last event that was acted on for each type. + // Used to throttle actions, in preference to a longer android:notificationTimeout value because + // that also slows down the first action instead of only subsequent ones. + private val lastEventTime: MutableMap = HashMap() + private var obscuringView: TextView? = null + @SuppressLint("SetTextI18n") + public override fun onServiceConnected() { + super.onServiceConnected() + val info = serviceInfo + if (info == null) { + Log.e("AMI", "getServiceInfo() returned null!") + return + } + info.flags = info.flags or AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS + info.flags = info.flags or AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS + serviceInfo = info + val tv = TextView(this) + tv.layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + 1f + ) + tv.gravity = Gravity.CENTER + tv.textAlignment = View.TEXT_ALIGNMENT_CENTER + tv.text = "\"Suggested posts\" being obscured by " + getString(R.string.app_name) + tv.setTextColor(Color.YELLOW) + tv.setBackgroundColor(Color.BLACK) + tv.textSize = 35f + obscuringView = tv + } + + // Handy for debugging. + private fun dumpRecursively(node: AccessibilityNodeInfo?) { + dumpRecursively("", node) + } + + private fun dumpRecursively(prefix: String, node: AccessibilityNodeInfo?) { + if (!debug || node == null) return + emit(prefix + node.text + " - " + node.hintText + " - " + node.tooltipText + " - " + node.viewIdResourceName) + val count = node.childCount + for (i in 0 until count) { + dumpRecursively("$prefix ", node.getChild(i)) + } + } + + override fun onAccessibilityEvent(event: AccessibilityEvent) { + if (event.packageName == null) { + if (event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED && + event.windowChanges and AccessibilityEvent.WINDOWS_CHANGE_ACTIVE != 0 + ) { + emit("hideObscuringOverlay because TYPE_WINDOWS_CHANGED from null package") + hideObscuringOverlay() + } + return + } + val eventTime = event.eventTime + if (eventTime - lastEventTime.getOrDefault(event.eventType, 0L) < 500) return + if (debug) emit("event is $event") + when (event.packageName.toString()) { + "com.instagram.android" -> { + onInstagramEvent(event) + return + } + + "com.google.android.apps.maps" -> { + onMapsEvent(event) + return + } + + else -> return + } + } + + private fun firstDescendant( + source: AccessibilityNodeInfo?, + viewID: String + ): AccessibilityNodeInfo? { + if (source == null) return null + val candidates = source.findAccessibilityNodeInfosByViewId(viewID) + if (candidates.isEmpty()) return null + if (candidates.size > 1 && debug) emit("Multiple descendants with view ID $viewID: $candidates") + val found = candidates.iterator().next() + return if (!found.isVisibleToUser) null else found + } + + private fun onMapsEvent(event: AccessibilityEvent) { + val eventTime = event.eventTime + val eventType = event.eventType + if (eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) return + val found = firstDescendant( + event.source, + "com.google.android.apps.maps:id/explore_tab_home_title_card" + ) + ?: return + val r = Rect() + found.getBoundsInWindow(r) + // Don't process the view until it's popped up enough for drag-down to be meaningful. + if (r.top > 2000) return + // Don't process the view if its bounds don't make sense (e.g. overlapping notification bar). + if (r.top < 10) return + lastEventTime[eventType] = eventTime + val path = Path() + path.moveTo(((r.left + r.right) / 2).toFloat(), (r.top + 5).toFloat()) + path.lineTo(((r.left + r.right) / 2).toFloat(), (r.top + 200).toFloat()) + val gesture = GestureDescription.Builder() + .addStroke(StrokeDescription(path, 0, 1)) + .build() + val dispatched = dispatchGesture(gesture, object : GestureResultCallback() { + override fun onCompleted(gestureDescription: GestureDescription) { + emit("Gesture completed") + } + + override fun onCancelled(gestureDescription: GestureDescription) { + emit("Gesture cancelled") + } + }, null) + emit("Dispatch gesture for maps: $dispatched") + if (debug) { + emit("Gesture because saw maps \"Latest in\" in source, which follows after source: " + event.source) + dumpRecursively(event.source) + } + } + + private var profileTabTop = -1 + private fun profileTabTop(source: AccessibilityNodeInfo?): Int { + if (profileTabTop > 0) { + return profileTabTop + } + val profileTab = firstDescendant(source, "com.instagram.android:id/profile_tab") ?: return 0 + val rect = Rect() + profileTab.getBoundsInWindow(rect) + emit("profileTab: $rect") + profileTabTop = rect.top + return profileTabTop + } + + private val demarcator = "com.instagram.android:id/end_of_feed_demarcator_container" + + private fun onInstagramEvent(event: AccessibilityEvent) { + val eventTime = event.eventTime + val eventType = event.eventType + + // TYPE_VIEW_SELECTED is delivered a bit faster than TYPE_VIEW_CLICK so use that, but also + // is delivered multiple times, hence the eventTime-based throttling. + if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) { + val search = firstDescendant(event.source, "com.instagram.android:id/search_tab") + val reels = firstDescendant(event.source, "com.instagram.android:id/clips_tab") + if (search != null || reels != null) { + lastEventTime[eventType] = eventTime + if (debug) emit("BACK because search or reels is not null: $search, $reels") + performGlobalAction(GLOBAL_ACTION_BACK) + } + return + } + if (eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) return + val found = firstDescendant(event.source, demarcator) + if (found == null) { + hideObscuringOverlay() + return + } + val r = Rect() + found.getBoundsInScreen(r) + lastEventTime[eventType] = eventTime + emit("Obscuring view because $demarcator is at $r") + showObscuringOverlay(r.bottom, profileTabTop(event.source)) + } + + private val windowManager: WindowManager get() = getSystemService(WINDOW_SERVICE) as WindowManager + + private fun hideObscuringOverlay() { + emit("hideObscuringOverlay") + if (obscuringView!!.parent == null) { + emit("hideObscuringOverlay suppressed") + return + } + windowManager.removeView(obscuringView) + } + + private fun showObscuringOverlay(top: Int, bottom: Int) { + @Suppress("NAME_SHADOWING") var top = top + emit("showObscuringOverlay: $top - $bottom") + // Force always being able to see a strip at the top for scrolling stories back into view. + if (top < 400) top = 400 + val wm = windowManager + val lp = WindowManager.LayoutParams() + lp.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY + lp.format = PixelFormat.OPAQUE + lp.flags = lp.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + lp.width = ViewGroup.LayoutParams.MATCH_PARENT + lp.height = bottom - top + lp.gravity = Gravity.BOTTOM + lp.y = wm.maximumWindowMetrics.bounds.height() - bottom + if (obscuringView!!.parent == null) { + emit(" showObscuringOverlay - addView") + wm.addView(obscuringView, lp) + } else { + emit(" showObscuringOverlay - updateView") + wm.updateViewLayout(obscuringView, lp) + } + } + + override fun onInterrupt() {} +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3daed1d..cd1e7a4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,5 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id 'com.android.application' version '8.1.1' apply false + id 'org.jetbrains.kotlin.android' version '2.0.0-Beta1' apply false } \ No newline at end of file