mirror of
https://github.com/gitnex-org/gitnex.git
synced 2026-07-11 09:14:34 -05:00
Implement Android 17+ local network permission check
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.gitnex.tea4j.v2.models.GeneralAPISettings;
|
||||
import org.gitnex.tea4j.v2.models.ServerVersion;
|
||||
import org.gitnex.tea4j.v2.models.User;
|
||||
import org.mian.gitnex.R;
|
||||
import org.mian.gitnex.bottomsheets.AndroidPermissionsBottomSheet;
|
||||
import org.mian.gitnex.clients.RetrofitClient;
|
||||
import org.mian.gitnex.database.api.BaseApi;
|
||||
import org.mian.gitnex.database.api.UserAccountsApi;
|
||||
@@ -45,6 +46,7 @@ import org.mian.gitnex.databinding.BottomsheetTokenHelpBinding;
|
||||
import org.mian.gitnex.helpers.AppUtil;
|
||||
import org.mian.gitnex.helpers.NetworkStatusObserver;
|
||||
import org.mian.gitnex.helpers.PathsHelper;
|
||||
import org.mian.gitnex.helpers.PermissionHelper;
|
||||
import org.mian.gitnex.helpers.Toasty;
|
||||
import org.mian.gitnex.helpers.UIHelper;
|
||||
import org.mian.gitnex.helpers.UrlHelper;
|
||||
@@ -220,6 +222,30 @@ public class LoginActivity extends BaseActivity {
|
||||
|
||||
activityLoginBinding.tokenHelper.setOnClickListener(token -> showTokenHelpSheet());
|
||||
|
||||
activityLoginBinding.grantLocalNetworkPermission.setVisibility(
|
||||
PermissionHelper.isBelowApiLevel(37)
|
||||
|| PermissionHelper.isGranted(
|
||||
ctx, "android.permission.ACCESS_LOCAL_NETWORK", 37)
|
||||
? View.GONE
|
||||
: View.VISIBLE);
|
||||
|
||||
activityLoginBinding.grantLocalNetworkPermission.setOnClickListener(
|
||||
v -> {
|
||||
AndroidPermissionsBottomSheet sheet =
|
||||
AndroidPermissionsBottomSheet.newInstance(
|
||||
"local_network_permission",
|
||||
"android.permission.ACCESS_LOCAL_NETWORK",
|
||||
37);
|
||||
sheet.setOnPermissionResultListener(
|
||||
granted -> {
|
||||
if (granted) {
|
||||
activityLoginBinding.grantLocalNetworkPermission.setVisibility(
|
||||
View.GONE);
|
||||
}
|
||||
});
|
||||
sheet.show(getSupportFragmentManager(), "AndroidPermissionsBottomSheet");
|
||||
});
|
||||
|
||||
networkStatusObserver.registerNetworkStatusListener(
|
||||
hasNetworkConnection ->
|
||||
runOnUiThread(
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package org.mian.gitnex.bottomsheets;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import org.mian.gitnex.R;
|
||||
import org.mian.gitnex.databinding.BottomsheetAndroidPermissionsBinding;
|
||||
import org.mian.gitnex.helpers.AppUtil;
|
||||
import org.mian.gitnex.helpers.PermissionHelper;
|
||||
|
||||
/**
|
||||
* @author mmarif
|
||||
*/
|
||||
public class AndroidPermissionsBottomSheet extends BottomSheetDialogFragment {
|
||||
|
||||
private BottomsheetAndroidPermissionsBinding binding;
|
||||
private String permission;
|
||||
private int requiredApiLevel;
|
||||
private String title;
|
||||
private String description;
|
||||
private ActivityResultLauncher<String> permissionLauncher;
|
||||
private OnPermissionResultListener listener;
|
||||
|
||||
public interface OnPermissionResultListener {
|
||||
void onPermissionResult(boolean granted);
|
||||
}
|
||||
|
||||
public static AndroidPermissionsBottomSheet newInstance(
|
||||
String permissionType, String permission, int requiredApiLevel) {
|
||||
AndroidPermissionsBottomSheet sheet = new AndroidPermissionsBottomSheet();
|
||||
Bundle args = new Bundle();
|
||||
args.putString("permissionType", permissionType);
|
||||
args.putString("permission", permission);
|
||||
args.putInt("requiredApiLevel", requiredApiLevel);
|
||||
sheet.setArguments(args);
|
||||
return sheet;
|
||||
}
|
||||
|
||||
public void setOnPermissionResultListener(OnPermissionResultListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (getArguments() != null) {
|
||||
String permissionType = getArguments().getString("permissionType");
|
||||
permission = getArguments().getString("permission");
|
||||
requiredApiLevel = getArguments().getInt("requiredApiLevel");
|
||||
|
||||
if ((permissionType != null ? permissionType : "").equals("local_network_permission")) {
|
||||
title = getString(R.string.localNetworkPermissionTitle);
|
||||
description = getString(R.string.localNetworkPermissionDescription);
|
||||
} else {
|
||||
title = getString(R.string.permissionRequired);
|
||||
description = getString(R.string.permissionRequiredDescription);
|
||||
}
|
||||
} else {
|
||||
permission = null;
|
||||
requiredApiLevel = 0;
|
||||
}
|
||||
|
||||
permissionLauncher =
|
||||
registerForActivityResult(
|
||||
new ActivityResultContracts.RequestPermission(),
|
||||
isGranted -> {
|
||||
updateButtonState();
|
||||
if (listener != null) {
|
||||
listener.onPermissionResult(isGranted);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable @Override
|
||||
public View onCreateView(
|
||||
@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
binding = BottomsheetAndroidPermissionsBinding.inflate(inflater, container, false);
|
||||
|
||||
binding.permissionTitle.setText(title);
|
||||
binding.permissionDescription.setText(description);
|
||||
|
||||
updateButtonState();
|
||||
|
||||
binding.btnGrantPermission.setOnClickListener(
|
||||
v -> {
|
||||
if (permission == null) return;
|
||||
|
||||
if (PermissionHelper.isGranted(
|
||||
requireContext(), permission, requiredApiLevel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (PermissionHelper.isPermanentlyDenied(
|
||||
requireActivity(), permission, requiredApiLevel)) {
|
||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(Uri.parse("package:" + requireContext().getPackageName()));
|
||||
startActivity(intent);
|
||||
} else {
|
||||
permissionLauncher.launch(permission);
|
||||
}
|
||||
});
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void updateButtonState() {
|
||||
if (permission == null) return;
|
||||
|
||||
if (PermissionHelper.isGranted(requireContext(), permission, requiredApiLevel)) {
|
||||
binding.btnGrantPermission.setText(R.string.permissionGranted);
|
||||
binding.btnGrantPermission.setEnabled(false);
|
||||
} else if (PermissionHelper.isPermanentlyDenied(
|
||||
requireActivity(), permission, requiredApiLevel)) {
|
||||
binding.btnGrantPermission.setText(R.string.openSettings);
|
||||
binding.btnGrantPermission.setEnabled(true);
|
||||
} else {
|
||||
binding.btnGrantPermission.setText(R.string.grantPermission);
|
||||
binding.btnGrantPermission.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
Dialog dialog = getDialog();
|
||||
if (dialog instanceof BottomSheetDialog) {
|
||||
AppUtil.applySheetStyle((BottomSheetDialog) dialog, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,10 @@ package org.mian.gitnex.bottomsheets;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -22,6 +25,7 @@ import org.mian.gitnex.databinding.BottomsheetSettingsSecurityBinding;
|
||||
import org.mian.gitnex.helpers.AppDatabaseSettings;
|
||||
import org.mian.gitnex.helpers.AppUIStateManager;
|
||||
import org.mian.gitnex.helpers.AppUtil;
|
||||
import org.mian.gitnex.helpers.PermissionHelper;
|
||||
import org.mian.gitnex.helpers.Toasty;
|
||||
import org.mian.gitnex.helpers.ssl.MemorizingTrustManager;
|
||||
|
||||
@@ -189,6 +193,16 @@ public class SettingsSecurityBottomSheet extends BottomSheetDialogFragment {
|
||||
}
|
||||
});
|
||||
|
||||
if (!PermissionHelper.isBelowApiLevel(37)) {
|
||||
binding.localNetworkPermissionFrame.setVisibility(View.VISIBLE);
|
||||
binding.openLocalNetworkPermissionSettings.setOnClickListener(
|
||||
v -> {
|
||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(Uri.parse("package:" + requireContext().getPackageName()));
|
||||
startActivity(intent);
|
||||
});
|
||||
}
|
||||
|
||||
binding.clearCacheButton.setOnClickListener(
|
||||
v -> {
|
||||
MaterialAlertDialogBuilder dialog =
|
||||
|
||||
@@ -146,13 +146,13 @@ public abstract class GitnexDatabase extends RoomDatabase {
|
||||
"CREATE TABLE IF NOT EXISTS 'bookmarks' ("
|
||||
+ "'bookmarkId' INTEGER NOT NULL, "
|
||||
+ "'accountId' INTEGER NOT NULL, "
|
||||
+ "'type' TEXT NOT NULL, "
|
||||
+ "'type' TEXT, "
|
||||
+ "'bookmarkAction' TEXT, "
|
||||
+ "'owner' TEXT, "
|
||||
+ "'repo' TEXT, "
|
||||
+ "'identifier' TEXT, "
|
||||
+ "'branch' TEXT, "
|
||||
+ "'title' TEXT NOT NULL, "
|
||||
+ "'title' TEXT, "
|
||||
+ "'url' TEXT, "
|
||||
+ "'createdAt' INTEGER NOT NULL, "
|
||||
+ "PRIMARY KEY('bookmarkId'))");
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.mian.gitnex.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
/**
|
||||
* @author mmarif
|
||||
*/
|
||||
public class PermissionHelper {
|
||||
|
||||
private PermissionHelper() {}
|
||||
|
||||
public static boolean isBelowApiLevel(int apiLevel) {
|
||||
return Build.VERSION.SDK_INT < apiLevel;
|
||||
}
|
||||
|
||||
public static boolean isGranted(Context context, String permission, int requiredApiLevel) {
|
||||
if (isBelowApiLevel(requiredApiLevel)) {
|
||||
return true;
|
||||
}
|
||||
return ContextCompat.checkSelfPermission(context, permission)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
public static boolean shouldShowRationale(
|
||||
android.app.Activity activity, String permission, int requiredApiLevel) {
|
||||
if (isBelowApiLevel(requiredApiLevel)) {
|
||||
return false;
|
||||
}
|
||||
if (isGranted(activity, permission, requiredApiLevel)) {
|
||||
return false;
|
||||
}
|
||||
return activity.shouldShowRequestPermissionRationale(permission);
|
||||
}
|
||||
|
||||
public static boolean isPermanentlyDenied(
|
||||
android.app.Activity activity, String permission, int requiredApiLevel) {
|
||||
if (isBelowApiLevel(requiredApiLevel)) {
|
||||
return false;
|
||||
}
|
||||
if (isGranted(activity, permission, requiredApiLevel)) {
|
||||
return false;
|
||||
}
|
||||
return !activity.shouldShowRequestPermissionRationale(permission);
|
||||
}
|
||||
}
|
||||
62
app/src/main/res/drawable/ic_network.xml
Normal file
62
app/src/main/res/drawable/ic_network.xml
Normal file
@@ -0,0 +1,62 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M6,9a6,6 0,1 0,12 0a6,6 0,0 0,-12 0"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="?attr/iconsColor"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,3c1.333,0.333 2,2.333 2,6s-0.667,5.667 -2,6"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="?attr/iconsColor"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,3c-1.333,0.333 -2,2.333 -2,6s0.667,5.667 2,6"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="?attr/iconsColor"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M6,9h12"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="?attr/iconsColor"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M3,20h7"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="?attr/iconsColor"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M14,20h7"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="?attr/iconsColor"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M10,20a2,2 0,1 0,4 0a2,2 0,0 0,-4 0"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="?attr/iconsColor"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,15v3"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="?attr/iconsColor"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
@@ -259,6 +259,19 @@
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/grantLocalNetworkPermission"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/dimen8dp"
|
||||
android:text="@string/localNetworkPermissionTitle"
|
||||
android:textColor="?attr/inputTextColor"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/ic_network"
|
||||
app:iconGravity="textStart"
|
||||
app:iconTint="?attr/iconsColor" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appVersion"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
55
app/src/main/res/layout/bottomsheet_android_permissions.xml
Normal file
55
app/src/main/res/layout/bottomsheet_android_permissions.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
|
||||
android:id="@+id/drag_handle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:paddingHorizontal="@dimen/dimen16dp"
|
||||
android:paddingBottom="@dimen/dimen32dp"
|
||||
app:layout_constrainedHeight="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/drag_handle">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/permissionTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/permissionDescription"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/dimen12dp"
|
||||
android:alpha="0.8"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:textColor="?attr/primaryTextColor" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnGrantPermission"
|
||||
style="@style/Widget.Material3.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/dimen56dp"
|
||||
android:layout_marginTop="@dimen/dimen28dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -230,11 +230,50 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/localNetworkPermissionFrame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/dimen24dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/localNetworkPermissionHeader"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/localNetworkPermissionTitle"
|
||||
android:textColor="?attr/primaryTextColor"
|
||||
android:textSize="@dimen/dimen16sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/dimen4dp"
|
||||
android:text="@string/localNetworkPermissionDescription"
|
||||
android:textColor="?attr/hintColor"
|
||||
android:textSize="@dimen/dimen12sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/openLocalNetworkPermissionSettings"
|
||||
style="@style/Widget.Material3.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/dimen56dp"
|
||||
android:layout_marginTop="@dimen/dimen12dp"
|
||||
android:text="@string/openSettings"
|
||||
android:backgroundTint="?attr/iconsColor"
|
||||
android:textColor="?attr/materialCardBackgroundColor"
|
||||
app:icon="@drawable/ic_settings"
|
||||
app:iconGravity="textStart"
|
||||
app:iconTint="?attr/materialCardBackgroundColor" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/clearCacheButton"
|
||||
style="@style/Widget.Material3.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/dimen54dp"
|
||||
android:layout_height="@dimen/dimen56dp"
|
||||
android:layout_marginTop="@dimen/dimen24dp"
|
||||
android:backgroundTint="?attr/iconsColor"
|
||||
android:text="@string/clearCacheSelectionHeaderText"
|
||||
@@ -244,7 +283,7 @@
|
||||
android:id="@+id/deleteCertsButton"
|
||||
style="@style/Widget.Material3.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/dimen54dp"
|
||||
android:layout_height="@dimen/dimen56dp"
|
||||
android:layout_marginTop="@dimen/dimen16dp"
|
||||
android:backgroundTint="?attr/iconsColor"
|
||||
android:text="@string/settingsCertsSelectorHeader"
|
||||
|
||||
@@ -768,6 +768,13 @@
|
||||
<string name="bookmark_tab">Bookmark tab</string>
|
||||
<string name="repository_section">Repository section</string>
|
||||
<string name="wiki_page">Wiki page</string>
|
||||
<string name="localNetworkPermissionTitle">Local Network Permission</string>
|
||||
<string name="localNetworkPermissionDescription">Android 17+ requires this permission to connect to local network. Without it, connections to servers on your local network may fail.</string>
|
||||
<string name="grantPermission">Grant permission</string>
|
||||
<string name="openSettings">Open settings</string>
|
||||
<string name="permissionGranted">Permission granted</string>
|
||||
<string name="permissionRequired">Permission required</string>
|
||||
<string name="permissionRequiredDescription">This permission is needed for certain features to work properly.</string>
|
||||
|
||||
<!-- generic copy -->
|
||||
<string name="all">All</string>
|
||||
|
||||
Reference in New Issue
Block a user