From e1ce697f1fb2ba599ce5c22e1c5e6a276b5023f5 Mon Sep 17 00:00:00 2001 From: Fooq <88929720+fooqman@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:39:50 +1200 Subject: [PATCH] implement robust multi-permission handling for local scan Migrated from old location permission request to ActivityResultLauncher using RequestMultiplePermissions. Added dynamic permission filtering based on SDK version (handling NEARBY_WIFI_DEVICES for Android 13+). Implemented state tracking to properly detect "Never ask again" scenarios. Added a warning button (`warning_button`) to the layout that toggles visibility on resume based on permission status. Introduced `showMissingPermissionsDialog` with options to grant missing permissions or jump straight to system app settings. Other UI improvements. --- .../stryker/localnetwork/LocalMain.java | 151 +++++++++++++++++- .../java/com/zalexdev/stryker/utils/Core.java | 42 ----- app/src/main/res/layout/local_fragment.xml | 48 ++++-- app/src/main/res/values/strings.xml | 2 + 4 files changed, 189 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/zalexdev/stryker/localnetwork/LocalMain.java b/app/src/main/java/com/zalexdev/stryker/localnetwork/LocalMain.java index 2e51fc3a..fd5cacad 100644 --- a/app/src/main/java/com/zalexdev/stryker/localnetwork/LocalMain.java +++ b/app/src/main/java/com/zalexdev/stryker/localnetwork/LocalMain.java @@ -1,15 +1,20 @@ package com.zalexdev.stryker.localnetwork; +import android.Manifest; import android.app.Activity; import android.app.Dialog; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.net.ConnectivityManager; import android.net.DhcpInfo; import android.net.NetworkInfo; +import android.net.Uri; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; +import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -19,6 +24,10 @@ import android.widget.LinearLayout; import android.widget.TextView; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -38,8 +47,10 @@ import com.zalexdev.stryker.utils.Core; import java.util.ArrayList; +import java.util.List; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; public class LocalMain extends Fragment { @@ -65,6 +76,7 @@ public class LocalMain extends Fragment { public MaterialTextView networkGateway; public MaterialTextView networkSubnet; public MaterialTextView networkCount; + private MaterialButton warningButton; public MaterialButton rescanBtn; private RecyclerView mRecyclerView; @@ -78,6 +90,14 @@ public class LocalMain extends Fragment { private final AtomicBoolean alive = new AtomicBoolean(true); private AdvancedLocalScanner scanner; + private ActivityResultLauncher multiplePermissionsLauncher; + private BiConsumer multipleCallback; + private String[] currentPermissionsGroup; + private final String[] REQUIRED_PERMISSIONS = new String[]{ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ? Manifest.permission.NEARBY_WIFI_DEVICES : null + }; public LocalMain() { } @@ -86,6 +106,31 @@ private boolean uiSafe() { return alive.get() && activity != null && isAdded(); } + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + multiplePermissionsLauncher = registerForActivityResult( + new ActivityResultContracts.RequestMultiplePermissions(), + result -> { + if (multipleCallback == null || currentPermissionsGroup == null) return; + + boolean allGranted = true; + boolean anyNeverAskAgain = false; + + for (String permission : currentPermissionsGroup) { + Boolean isGranted = result.get(permission); + if (isGranted == null || !isGranted) { + allGranted = false; + if (!ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), permission)) { + anyNeverAskAgain = true; + } + } + } + multipleCallback.accept(allGranted, anyNeverAskAgain); + } + ); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.local_fragment, container, false); @@ -106,6 +151,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa getResources().getColor(R.color.stryker_accent), 0xFFAB47BC); refresh.setOnRefreshListener(this::triggerScan); + warningButton.setOnClickListener(v -> showMissingPermissionsDialog(true)); rescanBtn.setOnClickListener(v -> triggerScan()); View statusRow = view.findViewById(R.id.local_status_row); @@ -152,6 +198,24 @@ public void onViewCreated(View view, Bundle savedInstanceState) { } } + @Override + public void onResume() { + super.onResume(); + List missingPermissions = new ArrayList<>(); + + for (String perm : getActualRequiredPermissions()) { + if (ActivityCompat.checkSelfPermission(requireContext(), perm) != PackageManager.PERMISSION_GRANTED) { + missingPermissions.add(perm); + } + } + + if (missingPermissions.isEmpty()) { + warningButton.setVisibility(View.GONE); + } else { + warningButton.setVisibility(View.VISIBLE); + } + } + private void reloadDevicesFromPersistentSource() { ArrayList restored = null; if (mainActivity != null && mainActivity.getDevices() != null @@ -189,6 +253,7 @@ private void bindViews(View view) { networkGateway = view.findViewById(R.id.network_gateway); networkSubnet = view.findViewById(R.id.network_subnet); networkCount = view.findViewById(R.id.network_count); + warningButton = view.findViewById(R.id.warning_button); rescanBtn = view.findViewById(R.id.network_rescan); } @@ -216,10 +281,90 @@ private void triggerScan() { startScan(); } - public void startScan() { - if (core != null && !core.hasLocationPermission()) { - core.requestLocationPermission(activity); + private void openAppSettings() { + Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData( + Uri.fromParts( + "package", requireContext().getPackageName(), null)); + startActivity(intent); + } + + @NonNull + private List getActualRequiredPermissions() { + List actual = new ArrayList<>(); + for (String perm : REQUIRED_PERMISSIONS) { + if (perm != null) actual.add(perm); } + return actual; + } + + private void showMissingPermissionsDialog(boolean asWarning) { + List missingPermissions = new ArrayList<>(); + + for (String perm : getActualRequiredPermissions()) { + if (ActivityCompat.checkSelfPermission(requireContext(), perm) != PackageManager.PERMISSION_GRANTED) { + String shortName = perm.substring(perm.lastIndexOf(".") + 1); + missingPermissions.add("«" + shortName + "»"); + } + } + + if (missingPermissions.isEmpty()) return; + + String missingText = String.join(", ", missingPermissions); + + String message = String.format(getString(R.string.local_scan_permissions_desc), missingText); + + MaterialAlertDialogBuilder dialog = new MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.warning)) + .setMessage(message); + dialog.setCancelable(false); + if (asWarning) { + dialog.setPositiveButton(getString(R.string.ok), null); + } else { + dialog.setPositiveButton(android.R.string.ok, (d, w) -> _startScan()); + } + dialog.setNeutralButton(getString(R.string.settings), (d, w) -> openAppSettings()); + if (!asWarning) + dialog.setNegativeButton(getString(R.string.cancel), null); + dialog.show(); + } + + private void checkMultiplePermissions(@NonNull String[] permissions, BiConsumer callback) { + boolean allGranted = true; + for (String permission : permissions) { + if (ActivityCompat.checkSelfPermission(requireContext(), permission) != PackageManager.PERMISSION_GRANTED) { + allGranted = false; + break; + } + } + + if (allGranted) { + callback.accept(true, false); + return; + } + + this.currentPermissionsGroup = permissions; + this.multipleCallback = callback; + multiplePermissionsLauncher.launch(permissions); + } + + public void startScan() { + String[] permissionsArray = getActualRequiredPermissions().toArray(new String[0]); + + checkMultiplePermissions(permissionsArray, (isAllGranted, isNeverAskAgain) -> { + if (isAllGranted) { + warningButton.setVisibility(View.GONE); + _startScan(); + } else if (isNeverAskAgain) { + warningButton.setVisibility(View.VISIBLE); + _startScan(); + } else { + showMissingPermissionsDialog(false); + } + }); + } + + private void _startScan() { if (scanner != null) { try { scanner.stopScan(); diff --git a/app/src/main/java/com/zalexdev/stryker/utils/Core.java b/app/src/main/java/com/zalexdev/stryker/utils/Core.java index d1768fb0..4609cfc0 100644 --- a/app/src/main/java/com/zalexdev/stryker/utils/Core.java +++ b/app/src/main/java/com/zalexdev/stryker/utils/Core.java @@ -6,9 +6,7 @@ import static android.os.Build.VERSION.SDK_INT; import static android.os.Environment.getExternalStorageDirectory; -import android.annotation.SuppressLint; import android.app.Activity; -import android.app.Dialog; import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipboardManager; @@ -20,13 +18,10 @@ import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.os.Build; -import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.VibrationEffect; @@ -35,12 +30,9 @@ import android.text.TextUtils; import android.text.format.Formatter; import android.view.View; -import android.widget.LinearLayout; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; -import androidx.cardview.widget.CardView; import androidx.core.app.ActivityCompat; import androidx.core.content.FileProvider; import androidx.security.crypto.EncryptedSharedPreferences; @@ -48,9 +40,7 @@ import androidx.viewpager.widget.ViewPager; import androidx.viewpager2.widget.ViewPager2; -import com.google.android.material.switchmaterial.SwitchMaterial; import com.zalexdev.stryker.BuildConfig; -import com.zalexdev.stryker.R; import com.zalexdev.stryker.custom.Credentials; import com.zalexdev.stryker.custom.Device; import com.zalexdev.stryker.custom.Exploit; @@ -63,19 +53,16 @@ import com.zalexdev.stryker.logger.LogTool; import com.zalexdev.stryker.logger.Logger; -import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.File; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.net.HttpURLConnection; import java.net.URI; import java.security.GeneralSecurityException; @@ -86,7 +73,6 @@ import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Locale; -import java.util.Objects; import java.util.UUID; import com.stryker.terminal.bridge.StrykerLog; @@ -1442,34 +1428,6 @@ public void checkPermission(Activity activity) { } } - public boolean hasLocationPermission() { - try { - return context.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) - == PackageManager.PERMISSION_GRANTED; - } catch (Exception e) { - return false; - } - } - - public void requestLocationPermission(Activity activity) { - if (activity == null) return; - try { - ArrayList wanted = new ArrayList<>(); - if (!hasLocationPermission()) { - wanted.add(android.Manifest.permission.ACCESS_FINE_LOCATION); - wanted.add(android.Manifest.permission.ACCESS_COARSE_LOCATION); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU - && context.checkSelfPermission(android.Manifest.permission.NEARBY_WIFI_DEVICES) - != PackageManager.PERMISSION_GRANTED) { - wanted.add(android.Manifest.permission.NEARBY_WIFI_DEVICES); - } - if (wanted.isEmpty()) return; - ActivityCompat.requestPermissions(activity, wanted.toArray(new String[0]), 124); - } catch (Exception ignored) { - } - } - public void requestAllFilesAccess(Activity activity) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { diff --git a/app/src/main/res/layout/local_fragment.xml b/app/src/main/res/layout/local_fragment.xml index 5b5defe9..215927ab 100644 --- a/app/src/main/res/layout/local_fragment.xml +++ b/app/src/main/res/layout/local_fragment.xml @@ -181,6 +181,23 @@ android:textSize="12sp" /> + + + android:orientation="vertical" + android:paddingTop="4dp" + android:paddingBottom="4dp"> - + android:gravity="center_vertical" + android:orientation="horizontal"> + + + Active clients not found. Error OK + Warning Settings Customize Stryker to fit your workflow @@ -452,6 +453,7 @@ manager Enter a valid IPv4 address Enter a CIDR mask between 1 and 32 Custom + It is recommended to grant the following permissions to the app:\n%s, so that scan results are more accurate. Wi-Fi Networks Scanning the airwaves…