diff --git a/docs/provider.md b/docs/provider.md
index a5b4841..c6a0a93 100644
--- a/docs/provider.md
+++ b/docs/provider.md
@@ -40,6 +40,25 @@ You can skip this step, and the provider will create a folder named after the pr
 
 To specify a folder, create it on your machine and provide it at startup using the `--provider-folder` flag.
 
+## Manual Android APK Installation
+
+If the phone does not permit APK installation through USB, install the bundled
+`gads-settings.apk` on the phone using its file manager, then start the provider
+with `--android-manual-apk-install`. This mode checks for `com.gads.settings`
+and preserves manually installed helper packages. It never falls back to ADB
+installation when the helper is missing. Keep the device disabled until the
+manual installation is complete.
+
+Use `--android-preserve-ime` to keep the phone's current input method. This skips
+GADS keyboard setup and forwarding, so desktop text input through the GADS IME
+is unavailable. The phone's own keyboard remains usable.
+
+For a USB-connected phone, `--android-usb-only` also skips enabling ADB TCP mode
+and forwarding the remote ADB tunnel. GADS screen and control connections still
+use USB port forwarding. All these options default to false, so existing providers
+retain their setup behavior. Android permissions and manufacturer restrictions
+still apply; these flags do not grant permissions.
+
 ## Provider Setup
 
 ### macOS
diff --git a/main.go b/main.go
index effd84b..7514166 100644
--- a/main.go
+++ b/main.go
@@ -65,6 +65,9 @@ func main() {
 	providerCmd.Flags().String("hub", "", "The address of the GADS hub instance")
 	providerCmd.Flags().String("turn-username-suffix", "gads", "Suffix to append to TURN usernames (format: timestamp:suffix)")
 	providerCmd.Flags().Bool("use-ios-pair-cache", false, "Cache iOS pair records on disk to skip Trust dialog on reconnect (for unsupervised devices)")
+	providerCmd.Flags().Bool("android-manual-apk-install", false, "Use manually installed GADS Settings; do not install or uninstall Android helper APKs through ADB")
+	providerCmd.Flags().Bool("android-usb-only", false, "Keep Android debugging on USB; do not enable ADB TCP mode or the remote ADB tunnel")
+	providerCmd.Flags().Bool("android-preserve-ime", false, "Keep the phone's current input method; disable the GADS remote keyboard setup")
 	rootCmd.AddCommand(providerCmd)
 
 	// ADB Tunnel Command
diff --git a/provider/config/config.go b/provider/config/config.go
index a9c195b..89a5331 100644
--- a/provider/config/config.go
+++ b/provider/config/config.go
@@ -19,6 +19,11 @@ import (
 
 var ProviderConfig = &models.Provider{}
 
+// Process-local options preserve the defaults of other providers.
+var AndroidManualAPKInstall bool
+var AndroidUSBOnly bool
+var AndroidPreserveIME bool
+
 func SetupConfig(nickname, folder, hubAddress string) {
 	provider, err := db.GlobalMongoStore.GetProvider(nickname)
 	if err != nil {
diff --git a/provider/devices/android.go b/provider/devices/android.go
index 33c734c..5e0d6c0 100644
--- a/provider/devices/android.go
+++ b/provider/devices/android.go
@@ -185,6 +185,9 @@ func (d *AndroidDevice) allocatePorts() error {
 }
 
 func (d *AndroidDevice) cleanupOldApps() error {
+	if config.AndroidManualAPKInstall {
+		return nil
+	}
 	d.InstalledApps = d.GetInstalledAppBundleIDs()
 	logger.ProviderLogger.LogDebugf("android_device_setup", "Updated installed apps for Android device `%v`", d.GetUDID())
 
@@ -370,6 +373,14 @@ func (d *AndroidDevice) stopStreamService() {
 }
 
 func (d *AndroidDevice) installGadsSettingsApp() error {
+	if config.AndroidManualAPKInstall {
+		cmd := exec.CommandContext(d.Context, "adb", "-s", d.GetSerial(), "shell", "pm", "path", "com.gads.settings")
+		output, err := cmd.Output()
+		if err != nil || !strings.HasPrefix(strings.TrimSpace(string(output)), "package:") {
+			return fmt.Errorf("GADS Settings must be installed manually on the phone before enabling this device; ADB installation is disabled")
+		}
+		return nil
+	}
 	logger.ProviderLogger.LogInfof("android_device_setup", "Installing GADS Settings apk on device `%v`", d.GetUDID())
 	cmd := exec.CommandContext(d.Context, "adb", "-s", d.GetSerial(), "install", "-r", fmt.Sprintf("%s/gads-settings.apk", config.ProviderConfig.ProviderFolder))
 	if err := cmd.Run(); err != nil {
@@ -426,6 +437,10 @@ func (d *AndroidDevice) startH264Stream() {
 }
 
 func (d *AndroidDevice) setupIME() error {
+	if config.AndroidPreserveIME {
+		d.AndroidIMEPort = ""
+		return nil
+	}
 	logger.ProviderLogger.LogInfof("android_device_setup", "Enabling GADS Android IME on device `%v`", d.GetUDID())
 	cmd := exec.CommandContext(d.Context, "adb", "-s", d.GetSerial(), "shell", "ime", "enable", "com.gads.settings/com.shamanec.settings.RemoteKeyboardIME")
 	if err := cmd.Run(); err != nil {
@@ -499,6 +514,9 @@ func (d *AndroidDevice) forwardStream() error {
 }
 
 func (d *AndroidDevice) forwardIME() error {
+	if config.AndroidPreserveIME {
+		return nil
+	}
 	return d.forwardPort("1993", d.AndroidIMEPort)
 }
 
@@ -607,6 +625,10 @@ func (d *AndroidDevice) disableKeyguard() {
 }
 
 func (d *AndroidDevice) enableADBTCPMode() error {
+	if config.AndroidUSBOnly {
+		d.ADBPort = ""
+		return nil
+	}
 	// Check if tcpip mode is already enabled to avoid restarting adbd unnecessarily
 	checkCmd := exec.CommandContext(d.Context, "adb", "-s", d.GetSerial(), "shell", "getprop", "service.adb.tcp.port")
 	var outBuffer bytes.Buffer
@@ -629,6 +651,9 @@ func (d *AndroidDevice) enableADBTCPMode() error {
 }
 
 func (d *AndroidDevice) forwardADB() error {
+	if config.AndroidUSBOnly {
+		return nil
+	}
 	return d.forwardPort(adbTCPPort, d.ADBPort)
 }
 
diff --git a/provider/devices/android_manual_setup_test.go b/provider/devices/android_manual_setup_test.go
new file mode 100644
index 0000000..6aefa15
--- /dev/null
+++ b/provider/devices/android_manual_setup_test.go
@@ -0,0 +1,77 @@
+package devices
+
+import (
+	"context"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"GADS/common/models"
+	"GADS/provider/config"
+)
+
+// A command recorder verifies the important absence of installation, removal,
+// and TCP-mode commands. No real handset is accessed by these tests.
+func manualSetupFixture(t *testing.T, installed bool) (*AndroidDevice, string) {
+	t.Helper()
+	previousManual, previousUSB := config.AndroidManualAPKInstall, config.AndroidUSBOnly
+	previousIME := config.AndroidPreserveIME
+	config.AndroidManualAPKInstall, config.AndroidUSBOnly = true, true
+	config.AndroidPreserveIME = true
+	t.Cleanup(func() { config.AndroidManualAPKInstall, config.AndroidUSBOnly = previousManual, previousUSB })
+	t.Cleanup(func() { config.AndroidPreserveIME = previousIME })
+	dir := t.TempDir()
+	log := filepath.Join(dir, "commands.log")
+	script := "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$GADS_TEST_ADB_LOG\"\n"
+	script += "if [ \"$3 $4 $5 $6 $7\" != 'shell pm path com.gads.settings ' ]; then exit 99; fi\n"
+	if installed {
+		script += "printf 'package:/data/app/com.gads.settings/base.apk\\n'\n"
+	} else {
+		script += "exit 1\n"
+	}
+	if err := os.WriteFile(filepath.Join(dir, "adb"), []byte(script), 0755); err != nil {
+		t.Fatal(err)
+	}
+	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
+	t.Setenv("GADS_TEST_ADB_LOG", log)
+	d := &AndroidDevice{RuntimeState: RuntimeState{Context: context.Background(), DBDevice: models.DBDevice{UDID: "test-device"}}, ADBPort: "43210", AndroidIMEPort: "43211"}
+	return d, log
+}
+
+func TestManualAndroidSetupKeepsInstalledHelperAndUSBConnection(t *testing.T) {
+	d, log := manualSetupFixture(t, true)
+	for _, action := range []func() error{d.cleanupOldApps, d.installGadsSettingsApp, d.enableADBTCPMode, d.forwardADB, d.setupIME, d.forwardIME} {
+		if err := action(); err != nil {
+			t.Fatal(err)
+		}
+	}
+	commands, err := os.ReadFile(log)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if strings.TrimSpace(string(commands)) != "-s test-device shell pm path com.gads.settings" {
+		t.Fatalf("unexpected device mutations: %s", commands)
+	}
+	if d.ADBPort != "" {
+		t.Fatal("USB-only mode still advertises an ADB tunnel")
+	}
+	if d.AndroidIMEPort != "" {
+		t.Fatal("preserve-IME mode still advertises a remote keyboard")
+	}
+}
+
+func TestMissingManualHelperDoesNotFallBackToADBInstallation(t *testing.T) {
+	d, log := manualSetupFixture(t, false)
+	err := d.installGadsSettingsApp()
+	if err == nil || !strings.Contains(err.Error(), "installed manually") {
+		t.Fatalf("expected actionable manual installation error, got %v", err)
+	}
+	commands, err := os.ReadFile(log)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if strings.Contains(string(commands), " install ") || strings.Contains(string(commands), "uninstall") {
+		t.Fatal("missing helper triggered an ADB installation")
+	}
+}
diff --git a/provider/provider.go b/provider/provider.go
index 631a946..0b883fe 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -38,6 +38,9 @@ func StartProvider(flags *pflag.FlagSet, resourceFiles embed.FS) {
 	hubAddress, _ := flags.GetString("hub")
 	turnUsernameSuffix, _ := flags.GetString("turn-username-suffix")
 	useIOSPairCache, _ := flags.GetBool("use-ios-pair-cache")
+	config.AndroidManualAPKInstall, _ = flags.GetBool("android-manual-apk-install")
+	config.AndroidUSBOnly, _ = flags.GetBool("android-usb-only")
+	config.AndroidPreserveIME, _ = flags.GetBool("android-preserve-ime")
 
 	if nickname == "" {
 		log.Fatalf("Please provide valid provider instance nickname via the --nickname flag, e.g. --nickname=Provider1")
diff --git a/provider/router/control.go b/provider/router/control.go
index 22c0264..61e5234 100644
--- a/provider/router/control.go
+++ b/provider/router/control.go
@@ -321,7 +321,11 @@ func iOSAppSwitcher(dev devices.PlatformDevice) (*http.Response, error) {
 func androidRecents(dev devices.PlatformDevice) error {
 	if dev.GetOS() == "android" {
 		cmd := exec.CommandContext(dev.GetContext(), "adb", "-s", dev.GetSerial(), "shell", "input", "keyevent", "KEYCODE_APP_SWITCH")
-		return cmd.Run()
+		output, err := cmd.CombinedOutput()
+		if err != nil {
+			return fmt.Errorf("Android recent-apps navigation failed: %w: %s", err, bytes.TrimSpace(output))
+		}
+		return nil
 	}
 	return fmt.Errorf("Device is not an Android device")
 }
