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:
1 parent
feb18adacc
commit
25df1a89e6
6 files changed
+110
-1
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
+3
-1
@@ -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()) }
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user