Remove the INTERNET permission and guard the network call sites

PRD SI-5: nothing leaves the device.

- flexera/AndroidManifest.xml removes android.permission.INTERNET; it is attached to the
  debug and release build types so it outranks flavor and library manifests
- FlexeraNetwork (new, flexera/src): isAvailable() plus a crash guard, because OkHttp
  rethrows Android's SecurityException on its dispatcher thread and that killed the launcher
- Drawer search skips web suggestions when the permission is missing
- LawnchairApp installs the guard; the bug reporter ignores the same exception

Tested on the Android 17 emulator: typing in the drawer search and opening the About
screen no longer crash; the guard logged and absorbed six refused requests.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
TMCandClaude Fable 5.2 committed 2026-11-11 20:48:43 +01:00
1 parent feb18adacc
commit 25df1a89e6
6 files changed
+110 -1

No files matched your search

+12
View File
@@ -329,6 +329,18 @@ android {
// Flexera Launcher: branding and default overrides. This variant-level source set
// outranks 'lawn' and 'main', so nothing upstream has to be edited.
res.srcDirs = ['flexera/res']
java.srcDirs = ['flexera/src']
kotlin.directories.add('flexera/src')
}
// Flexera Launcher: manifest overrides (permission removals). Build-type manifests
// outrank flavor and library manifests, so the removals always win.
debug {
manifest.srcFile "flexera/AndroidManifest.xml"
}
release {
manifest.srcFile "flexera/AndroidManifest.xml"
}
lawnWithQuickstepNightly {
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Flexera Launcher manifest overrides. Attached to the debug and release build types in build.gradle,
which outrank Lawnchair's flavor manifests and every library manifest during the merge.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- PRD SI-5: nothing leaves the device, so the app must not be able to open network connections. -->
<uses-permission
android:name="android.permission.INTERNET"
tools:node="remove" />
</manifest>
+75
View File
@@ -0,0 +1,75 @@
package app.flexera
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Looper
import android.util.Log
/**
* Flexera Launcher ships without the INTERNET permission (PRD SI-5: nothing leaves the device).
*
* Lawnchair has a few features that talk to the network (web search suggestions, the About
* screen's contributor list, announcements, bug-report upload). Without the permission, Android
* answers their requests with a [SecurityException]. OkHttp rethrows that on its own dispatcher
* thread after it has already told the caller the request failed, and an exception that reaches
* the top of any thread kills the whole launcher.
*
* This object provides the two pieces that keep that from happening:
* - [isAvailable], so a feature can skip the request altogether, and
* - [installCrashGuard], a safety net for requests that are attempted anyway.
*/
object FlexeraNetwork {
private const val TAG = "FlexeraNetwork"
private const val MAX_CAUSE_DEPTH = 8
@Volatile
private var guardInstalled = false
/** True when the app is allowed to open network connections. */
@JvmStatic
fun isAvailable(context: Context): Boolean =
context.checkSelfPermission(Manifest.permission.INTERNET) == PackageManager.PERMISSION_GRANTED
/**
* Wraps the process-wide uncaught-exception handler so that a network request refused for
* lack of the INTERNET permission ends only the background thread it ran on. Every other
* exception goes to the handler that was installed before, unchanged.
*/
@JvmStatic
fun installCrashGuard() {
if (guardInstalled) return
guardInstalled = true
val previous = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
if (isBlockedNetworkCall(thread, throwable)) {
Log.w(TAG, "Ignored a network request blocked by the missing INTERNET permission on ${thread.name}")
} else {
previous?.uncaughtException(thread, throwable)
}
}
}
/**
* True when [throwable] is Android refusing a network request because the app lacks the
* INTERNET permission, raised on a background thread. The main thread is never excused:
* letting it continue after an uncaught exception would leave the UI in an unknown state.
*/
@JvmStatic
fun isBlockedNetworkCall(thread: Thread, throwable: Throwable): Boolean {
if (thread === Looper.getMainLooper().thread) return false
var current: Throwable? = throwable
var depth = 0
while (current != null && depth < MAX_CAUSE_DEPTH) {
if (current is SecurityException && current.message?.contains("INTERNET") == true) {
return true
}
current = current.cause
depth++
}
return false
}
}
@@ -72,6 +72,8 @@ class LawnchairApp : LauncherApplication() {
override fun onCreate() {
super.onCreate()
instance = this
// Flexera Launcher: a network request refused for lack of INTERNET must not kill the launcher.
app.flexera.FlexeraNetwork.installCrashGuard()
QuickStepContract.sRecentsDisabled = !recentsEnabled
Flowerpot.Manager.getInstance(this)
registerActivityLifecycleCallbacks(activityHandler)
@@ -39,6 +39,10 @@ class LawnchairBugReporter(private val context: Context) {
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
// Flexera Launcher: not a crash, so no bug report (see FlexeraNetwork).
if (app.flexera.FlexeraNetwork.isBlockedNetworkCall(thread, throwable)) {
return@setDefaultUncaughtExceptionHandler
}
try {
sendNotification(throwable)
} finally {
@@ -2,6 +2,7 @@ package app.lawnchair.search.algorithms.engine.provider.web
import android.content.Context
import android.util.Log
import app.flexera.FlexeraNetwork
import app.lawnchair.preferences.PreferenceManager
import app.lawnchair.preferences2.PreferenceManager2
import app.lawnchair.preferences2.firstCached
@@ -25,7 +26,8 @@ object WebSuggestionProvider : SearchProvider {
val prefs = PreferenceManager.getInstance(context)
val prefs2 = PreferenceManager2.getInstance(context)
if (query.isBlank() || !prefs.searchResultStartPageSuggestion.get()) {
// Flexera Launcher: no INTERNET permission, so there is nothing to ask the web for.
if (query.isBlank() || !prefs.searchResultStartPageSuggestion.get() || !FlexeraNetwork.isAvailable(context)) {
return flow { emit(emptyList()) }
}