mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-14 03:39:05 +01:00
Address review feedback
This commit is contained in:
@@ -19,17 +19,17 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
|
||||
import static androidx.core.content.ContextCompat.checkSelfPermission;
|
||||
import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission;
|
||||
import static org.briarproject.bramble.util.AndroidUtils.hasBtScanPermission;
|
||||
import static org.briarproject.briar.android.util.Permission.GRANTED;
|
||||
import static org.briarproject.briar.android.util.Permission.PERMANENTLY_DENIED;
|
||||
import static org.briarproject.briar.android.util.Permission.SHOW_RATIONALE;
|
||||
import static org.briarproject.briar.android.util.Permission.UNKNOWN;
|
||||
import static org.briarproject.briar.android.util.UiUtils.isLocationEnabled;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showDenialDialog;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showLocationDialog;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showRationale;
|
||||
import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.areBluetoothPermissionsGranted;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.gotPermission;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.isLocationEnabledForBt;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.showDenialDialog;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.showLocationDialog;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.showRationale;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.wasGrantedBluetoothPermissions;
|
||||
|
||||
class AddNearbyContactPermissionManager {
|
||||
|
||||
@@ -64,9 +64,7 @@ class AddNearbyContactPermissionManager {
|
||||
} else if (SDK_INT < 31) {
|
||||
bluetoothOk = checkSelfPermission(ctx, ACCESS_FINE_LOCATION) == ok;
|
||||
} else {
|
||||
bluetoothOk = hasBtConnectPermission(ctx) &&
|
||||
hasBtScanPermission(ctx) &&
|
||||
checkSelfPermission(ctx, BLUETOOTH_ADVERTISE) == ok;
|
||||
bluetoothOk = areBluetoothPermissionsGranted(ctx);
|
||||
}
|
||||
return bluetoothOk && checkSelfPermission(ctx, CAMERA) == ok;
|
||||
}
|
||||
@@ -79,7 +77,7 @@ class AddNearbyContactPermissionManager {
|
||||
}
|
||||
|
||||
boolean checkPermissions() {
|
||||
boolean locationEnabled = isLocationEnabled(ctx);
|
||||
boolean locationEnabled = isLocationEnabledForBt(ctx);
|
||||
if (locationEnabled && areEssentialPermissionsGranted()) return true;
|
||||
// If an essential permission has been permanently denied, ask the
|
||||
// user to change the setting
|
||||
@@ -141,7 +139,7 @@ class AddNearbyContactPermissionManager {
|
||||
}
|
||||
|
||||
void onRequestPermissionResult(Map<String, Boolean> result) {
|
||||
if (gotPermission(CAMERA, result)) {
|
||||
if (gotPermission(ctx, result, CAMERA)) {
|
||||
cameraPermission = GRANTED;
|
||||
} else if (shouldShowRationale(CAMERA)) {
|
||||
cameraPermission = SHOW_RATIONALE;
|
||||
@@ -150,7 +148,7 @@ class AddNearbyContactPermissionManager {
|
||||
}
|
||||
if (isBluetoothSupported) {
|
||||
if (SDK_INT < 31) {
|
||||
if (gotPermission(ACCESS_FINE_LOCATION, result)) {
|
||||
if (gotPermission(ctx, result, ACCESS_FINE_LOCATION)) {
|
||||
locationPermission = GRANTED;
|
||||
} else if (shouldShowRationale(ACCESS_FINE_LOCATION)) {
|
||||
locationPermission = SHOW_RATIONALE;
|
||||
@@ -158,7 +156,7 @@ class AddNearbyContactPermissionManager {
|
||||
locationPermission = PERMANENTLY_DENIED;
|
||||
}
|
||||
} else {
|
||||
if (wasGrantedBluetoothPermissions(result)) {
|
||||
if (wasGrantedBluetoothPermissions(ctx, result)) {
|
||||
bluetoothPermissions = GRANTED;
|
||||
} else if (shouldShowRationale(BLUETOOTH_CONNECT)) {
|
||||
bluetoothPermissions = SHOW_RATIONALE;
|
||||
@@ -169,17 +167,6 @@ class AddNearbyContactPermissionManager {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean gotPermission(String permission,
|
||||
Map<String, Boolean> result) {
|
||||
Boolean permissionResult = result.get(permission);
|
||||
return permissionResult == null ?
|
||||
isGranted(permission) : permissionResult;
|
||||
}
|
||||
|
||||
private boolean isGranted(String permission) {
|
||||
return checkSelfPermission(ctx, permission) == PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private boolean shouldShowRationale(String permission) {
|
||||
return shouldShowRequestPermissionRationale(ctx, permission);
|
||||
}
|
||||
|
||||
@@ -89,8 +89,8 @@ import static org.briarproject.briar.android.contact.add.nearby.AddNearbyContact
|
||||
import static org.briarproject.briar.android.contact.add.nearby.AddNearbyContactViewModel.BluetoothDecision.NO_ADAPTER;
|
||||
import static org.briarproject.briar.android.contact.add.nearby.AddNearbyContactViewModel.BluetoothDecision.REFUSED;
|
||||
import static org.briarproject.briar.android.contact.add.nearby.AddNearbyContactViewModel.BluetoothDecision.UNKNOWN;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.isLocationEnabledForBt;
|
||||
import static org.briarproject.briar.android.util.UiUtils.handleException;
|
||||
import static org.briarproject.briar.android.util.UiUtils.isLocationEnabled;
|
||||
|
||||
@NotNullByDefault
|
||||
class AddNearbyContactViewModel extends AndroidViewModel
|
||||
@@ -396,7 +396,7 @@ class AddNearbyContactViewModel extends AndroidViewModel
|
||||
void showQrCodeFragmentIfAllowed() {
|
||||
boolean permissionsGranted = areEssentialPermissionsGranted(
|
||||
getApplication(), isBluetoothSupported());
|
||||
boolean locationEnabled = isLocationEnabled(getApplication());
|
||||
boolean locationEnabled = isLocationEnabledForBt(getApplication());
|
||||
if (wasContinueClicked && permissionsGranted && locationEnabled) {
|
||||
if (isWifiReady() && isBluetoothReady()) {
|
||||
LOG.info("Wifi and Bluetooth are ready");
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
package org.briarproject.briar.android.contact.connect;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.util.Permission;
|
||||
import org.briarproject.briar.android.util.UiUtils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
|
||||
import static android.Manifest.permission.BLUETOOTH_CONNECT;
|
||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.widget.Toast.LENGTH_LONG;
|
||||
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
|
||||
import static androidx.core.content.ContextCompat.checkSelfPermission;
|
||||
import static org.briarproject.briar.android.util.Permission.GRANTED;
|
||||
import static org.briarproject.briar.android.util.Permission.PERMANENTLY_DENIED;
|
||||
import static org.briarproject.briar.android.util.Permission.SHOW_RATIONALE;
|
||||
import static org.briarproject.briar.android.util.Permission.UNKNOWN;
|
||||
import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
|
||||
import static org.briarproject.briar.android.util.UiUtils.isLocationEnabled;
|
||||
import static org.briarproject.briar.android.util.UiUtils.requestBluetoothPermissions;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showLocationDialog;
|
||||
import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.gotPermission;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.isLocationEnabledForBt;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.requestBluetoothPermissions;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.showDenialDialog;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.showLocationDialog;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.showRationale;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.wasGrantedBluetoothPermissions;
|
||||
|
||||
class BluetoothConditionManager {
|
||||
|
||||
@@ -48,7 +47,7 @@ class BluetoothConditionManager {
|
||||
@UiThread
|
||||
void requestPermissions(ActivityResultLauncher<String[]> launcher) {
|
||||
if (SDK_INT < 31) {
|
||||
launcher.launch(new String[] {ACCESS_FINE_LOCATION});
|
||||
requestLocationPermission(launcher);
|
||||
} else {
|
||||
requestBluetoothPermissions(launcher);
|
||||
}
|
||||
@@ -58,7 +57,7 @@ class BluetoothConditionManager {
|
||||
void onLocationPermissionResult(Activity activity,
|
||||
@Nullable Map<String, Boolean> result) {
|
||||
if (SDK_INT < 31) {
|
||||
if (gotPermission(activity, result)) {
|
||||
if (gotPermission(activity, result, ACCESS_FINE_LOCATION)) {
|
||||
locationPermission = GRANTED;
|
||||
} else if (shouldShowRequestPermissionRationale(activity,
|
||||
ACCESS_FINE_LOCATION)) {
|
||||
@@ -67,7 +66,7 @@ class BluetoothConditionManager {
|
||||
locationPermission = PERMANENTLY_DENIED;
|
||||
}
|
||||
} else {
|
||||
if (wasGrantedBluetoothPermissions(result)) {
|
||||
if (wasGrantedBluetoothPermissions(activity, result)) {
|
||||
bluetoothPermissions = GRANTED;
|
||||
} else if (shouldShowRequestPermissionRationale(activity,
|
||||
BLUETOOTH_CONNECT)) {
|
||||
@@ -84,58 +83,38 @@ class BluetoothConditionManager {
|
||||
boolean permissionGranted =
|
||||
(SDK_INT < 23 || locationPermission == GRANTED) &&
|
||||
bluetoothPermissions == GRANTED;
|
||||
boolean locationEnabled = isLocationEnabled(ctx);
|
||||
boolean locationEnabled = isLocationEnabledForBt(ctx);
|
||||
if (permissionGranted && locationEnabled) return true;
|
||||
|
||||
if (locationPermission == PERMANENTLY_DENIED) {
|
||||
showDenialDialog(ctx, onLocationDenied);
|
||||
showDenialDialog(ctx, R.string.permission_location_title,
|
||||
R.string.permission_location_denied_body, onLocationDenied);
|
||||
} else if (locationPermission == SHOW_RATIONALE) {
|
||||
showRationale(ctx, permissionRequest);
|
||||
showRationale(ctx, R.string.permission_location_title,
|
||||
R.string.permission_location_request_body,
|
||||
() -> requestLocationPermission(permissionRequest));
|
||||
} else if (!locationEnabled) {
|
||||
showLocationDialog(ctx);
|
||||
} else if (bluetoothPermissions == PERMANENTLY_DENIED) {
|
||||
UiUtils.showDenialDialog(ctx, R.string.permission_bluetooth_title,
|
||||
R.string.permission_bluetooth_denied_body);
|
||||
Runnable onDenied = () -> Toast.makeText(ctx,
|
||||
R.string.connect_via_bluetooth_no_bluetooth_permission,
|
||||
LENGTH_LONG).show();
|
||||
showDenialDialog(ctx, R.string.permission_bluetooth_title,
|
||||
R.string.permission_bluetooth_denied_body, onDenied);
|
||||
} else if (bluetoothPermissions == SHOW_RATIONALE && SDK_INT >= 31) {
|
||||
UiUtils.showRationale(ctx, R.string.permission_bluetooth_title,
|
||||
// SDK_INT is checked to make linter happy, because
|
||||
// requestBluetoothPermissions() requires SDK_INT 31
|
||||
showRationale(ctx,
|
||||
R.string.permission_bluetooth_title,
|
||||
R.string.permission_bluetooth_body, () ->
|
||||
requestBluetoothPermissions(permissionRequest));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void showDenialDialog(Context ctx, Runnable onLocationDenied) {
|
||||
new AlertDialog.Builder(ctx, R.style.BriarDialogTheme)
|
||||
.setTitle(R.string.permission_location_title)
|
||||
.setMessage(R.string.permission_location_denied_body)
|
||||
.setPositiveButton(R.string.ok, getGoToSettingsListener(ctx))
|
||||
.setNegativeButton(R.string.cancel, (v, d) ->
|
||||
onLocationDenied.run())
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showRationale(Context ctx,
|
||||
ActivityResultLauncher<String[]> permissionRequest) {
|
||||
new AlertDialog.Builder(ctx, R.style.BriarDialogTheme)
|
||||
.setTitle(R.string.permission_location_title)
|
||||
.setMessage(R.string.permission_location_request_body)
|
||||
.setPositiveButton(R.string.ok, (dialog, which) ->
|
||||
permissionRequest.launch(
|
||||
new String[] {ACCESS_FINE_LOCATION}))
|
||||
.show();
|
||||
}
|
||||
|
||||
private boolean gotPermission(Context ctx,
|
||||
@Nullable Map<String, Boolean> result) {
|
||||
Boolean permissionResult =
|
||||
result == null ? null : result.get(ACCESS_FINE_LOCATION);
|
||||
return permissionResult == null ? isLocationPermissionGranted(ctx) :
|
||||
permissionResult;
|
||||
}
|
||||
|
||||
private boolean isLocationPermissionGranted(Context ctx) {
|
||||
return checkSelfPermission(ctx, ACCESS_FINE_LOCATION) ==
|
||||
PERMISSION_GRANTED;
|
||||
private void requestLocationPermission(
|
||||
ActivityResultLauncher<String[]> launcher) {
|
||||
launcher.launch(new String[] {ACCESS_FINE_LOCATION});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
|
||||
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.areBluetoothPermissionsGranted;
|
||||
|
||||
@UiThread
|
||||
@NotNullByDefault
|
||||
@@ -260,7 +261,8 @@ class ConnectViaBluetoothViewModel extends DbViewModel implements
|
||||
|
||||
private void stopConnecting() {
|
||||
BluetoothPlugin bluetoothPlugin = this.bluetoothPlugin;
|
||||
if (bluetoothPlugin != null) {
|
||||
if (bluetoothPlugin != null &&
|
||||
areBluetoothPermissionsGranted(getApplication())) {
|
||||
bluetoothPlugin.stopDiscoverAndConnect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.net.wifi.WifiManager;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
@@ -20,6 +19,12 @@ import static android.content.Context.WIFI_SERVICE;
|
||||
*/
|
||||
abstract class AbstractConditionManager {
|
||||
|
||||
/**
|
||||
* Consumes false, if permissions have been denied. Then we don't call
|
||||
* {@link HotspotIntroFragment#startHotspotIfConditionsFulfilled()},
|
||||
* which would result in the same permission being requested again
|
||||
* immediately.
|
||||
*/
|
||||
final Consumer<Boolean> permissionUpdateCallback;
|
||||
protected FragmentActivity ctx;
|
||||
WifiManager wifiManager;
|
||||
@@ -52,19 +57,6 @@ abstract class AbstractConditionManager {
|
||||
*/
|
||||
abstract boolean checkAndRequestConditions();
|
||||
|
||||
void showDenialDialog(FragmentActivity ctx,
|
||||
@StringRes int title, @StringRes int body,
|
||||
DialogInterface.OnClickListener onOkClicked, Runnable onDismiss) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
|
||||
builder.setTitle(title);
|
||||
builder.setMessage(body);
|
||||
builder.setPositiveButton(R.string.ok, onOkClicked);
|
||||
builder.setNegativeButton(R.string.cancel,
|
||||
(dialog, which) -> ctx.supportFinishAfterTransition());
|
||||
builder.setOnDismissListener(dialog -> onDismiss.run());
|
||||
builder.show();
|
||||
}
|
||||
|
||||
void showRationale(Context ctx, @StringRes int title,
|
||||
@StringRes int body, Runnable onContinueClicked,
|
||||
Runnable onDismiss) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.location.LocationManager;
|
||||
import android.provider.Settings;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.util.Permission;
|
||||
import org.briarproject.briar.android.util.PermissionUtils;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -23,8 +23,8 @@ import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRation
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showLocationDialog;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.isLocationEnabledForWiFi;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.showLocationDialog;
|
||||
|
||||
/**
|
||||
* This class ensures that the conditions to open a hotspot are fulfilled on
|
||||
@@ -47,6 +47,7 @@ class ConditionManager29 extends AbstractConditionManager {
|
||||
ConditionManager29(ActivityResultCaller arc,
|
||||
Consumer<Boolean> permissionUpdateCallback) {
|
||||
super(permissionUpdateCallback);
|
||||
// permissionUpdateCallback receives false if permissions were denied
|
||||
locationRequest = arc.registerForActivityResult(
|
||||
new RequestPermission(), granted -> {
|
||||
onRequestPermissionResult(granted);
|
||||
@@ -66,7 +67,7 @@ class ConditionManager29 extends AbstractConditionManager {
|
||||
|
||||
private boolean areEssentialPermissionsGranted() {
|
||||
boolean isWifiEnabled = wifiManager.isWifiEnabled();
|
||||
boolean isLocationEnabled = isLocationEnabled();
|
||||
boolean isLocationEnabled = isLocationEnabledForWiFi(ctx);
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info(String.format("areEssentialPermissionsGranted(): " +
|
||||
"locationPermission? %s, " +
|
||||
@@ -87,7 +88,7 @@ class ConditionManager29 extends AbstractConditionManager {
|
||||
return false;
|
||||
}
|
||||
// ensure location is enabled (if needed on this device)
|
||||
if (!isLocationEnabled()) {
|
||||
if (!isLocationEnabledForWiFi(ctx)) {
|
||||
showLocationDialog(ctx, false);
|
||||
return false;
|
||||
}
|
||||
@@ -98,8 +99,8 @@ class ConditionManager29 extends AbstractConditionManager {
|
||||
int res = SDK_INT >= 31 ?
|
||||
R.string.permission_hotspot_location_denied_precise_body :
|
||||
R.string.permission_hotspot_location_denied_body;
|
||||
showDenialDialog(ctx, R.string.permission_location_title, res,
|
||||
getGoToSettingsListener(ctx),
|
||||
PermissionUtils.showDenialDialog(ctx,
|
||||
R.string.permission_location_title, res,
|
||||
() -> permissionUpdateCallback.accept(false));
|
||||
return false;
|
||||
}
|
||||
@@ -149,12 +150,4 @@ class ConditionManager29 extends AbstractConditionManager {
|
||||
wifiRequest.launch(new Intent(Settings.Panel.ACTION_WIFI));
|
||||
}
|
||||
|
||||
private boolean isLocationEnabled() {
|
||||
if (SDK_INT >= 31) {
|
||||
LocationManager lm = ctx.getSystemService(LocationManager.class);
|
||||
return lm.isLocationEnabled();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import static android.Manifest.permission.CAMERA;
|
||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
|
||||
import static androidx.core.content.ContextCompat.checkSelfPermission;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showDenialDialog;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showRationale;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.showDenialDialog;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.showRationale;
|
||||
|
||||
class CameraPermissionManager {
|
||||
|
||||
|
||||
@@ -53,13 +53,12 @@ import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_BATTERY;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_COUNTRY_BLOCKED;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_MOBILE_DATA;
|
||||
import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission;
|
||||
import static org.briarproject.bramble.util.AndroidUtils.hasBtScanPermission;
|
||||
import static org.briarproject.briar.android.util.UiUtils.requestBluetoothPermissions;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showDenialDialog;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.areBluetoothPermissionsGranted;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.requestBluetoothPermissions;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.showDenialDialog;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.showRationale;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.wasGrantedBluetoothPermissions;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showRationale;
|
||||
import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
@@ -220,8 +219,7 @@ public class TransportsActivity extends BriarActivity {
|
||||
}
|
||||
|
||||
private void onClicked(TransportId transportId, boolean enable) {
|
||||
if (enable && SDK_INT >= 31 &&
|
||||
(!hasBtConnectPermission(this) || !hasBtScanPermission(this))) {
|
||||
if (enable && SDK_INT >= 31 && !areBluetoothPermissionsGranted(this)) {
|
||||
if (shouldShowRequestPermissionRationale(BLUETOOTH_CONNECT)) {
|
||||
showRationale(this, R.string.permission_bluetooth_title,
|
||||
R.string.permission_bluetooth_body,
|
||||
@@ -354,9 +352,10 @@ public class TransportsActivity extends BriarActivity {
|
||||
|
||||
@RequiresApi(31)
|
||||
private void handleBtPermissionResult(Map<String, Boolean> grantedMap) {
|
||||
if (wasGrantedBluetoothPermissions(grantedMap)) {
|
||||
if (wasGrantedBluetoothPermissions(this, grantedMap)) {
|
||||
viewModel.enableTransport(BluetoothConstants.ID, true);
|
||||
} else {
|
||||
// update adapter to reflect the "off" toggle state after denying
|
||||
transportsAdapter.notifyDataSetChanged();
|
||||
showDenialDialog(this,
|
||||
R.string.permission_bluetooth_title,
|
||||
|
||||
@@ -61,10 +61,10 @@ import static java.util.Objects.requireNonNull;
|
||||
import static java.util.TimeZone.getTimeZone;
|
||||
import static org.briarproject.bramble.util.AndroidUtils.getBluetoothAddressAndMethod;
|
||||
import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission;
|
||||
import static org.briarproject.bramble.util.AndroidUtils.hasBtScanPermission;
|
||||
import static org.briarproject.bramble.util.PrivacyUtils.scrubInetAddress;
|
||||
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
|
||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.areBluetoothPermissionsGranted;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
@@ -289,7 +289,8 @@ class BriarReportCollector {
|
||||
|
||||
// Is Bluetooth connectable?
|
||||
@SuppressLint("MissingPermission")
|
||||
int scanMode = hasBtScanPermission(ctx) ? bt.getScanMode() : -1;
|
||||
int scanMode = areBluetoothPermissionsGranted(ctx) ?
|
||||
bt.getScanMode() : -1;
|
||||
boolean btConnectable = scanMode == SCAN_MODE_CONNECTABLE ||
|
||||
scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE;
|
||||
connectivityInfo.add("BluetoothConnectable", btConnectable);
|
||||
|
||||
@@ -26,14 +26,13 @@ import androidx.preference.SwitchPreferenceCompat;
|
||||
|
||||
import static android.Manifest.permission.BLUETOOTH_CONNECT;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission;
|
||||
import static org.briarproject.bramble.util.AndroidUtils.hasBtScanPermission;
|
||||
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
|
||||
import static org.briarproject.briar.android.settings.SettingsActivity.enableAndPersist;
|
||||
import static org.briarproject.briar.android.util.UiUtils.requestBluetoothPermissions;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showDenialDialog;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showRationale;
|
||||
import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.areBluetoothPermissionsGranted;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.requestBluetoothPermissions;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.showDenialDialog;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.showRationale;
|
||||
import static org.briarproject.briar.android.util.PermissionUtils.wasGrantedBluetoothPermissions;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
@@ -91,7 +90,7 @@ public class ConnectionsFragment extends PreferenceFragmentCompat {
|
||||
if (SDK_INT >= 31) {
|
||||
enableBluetooth.setOnPreferenceChangeListener((p, value) -> {
|
||||
FragmentActivity ctx = requireActivity();
|
||||
if (hasBtConnectPermission(ctx) && hasBtScanPermission(ctx)) {
|
||||
if (areBluetoothPermissionsGranted(ctx)) {
|
||||
return true;
|
||||
} else if (shouldShowRequestPermissionRationale(
|
||||
BLUETOOTH_CONNECT)) {
|
||||
@@ -160,7 +159,7 @@ public class ConnectionsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@RequiresApi(31)
|
||||
private void handleBtPermissionResult(Map<String, Boolean> grantedMap) {
|
||||
if (wasGrantedBluetoothPermissions(grantedMap)) {
|
||||
if (wasGrantedBluetoothPermissions(requireActivity(), grantedMap)) {
|
||||
enableBluetooth.setChecked(true);
|
||||
} else {
|
||||
showDenialDialog(requireActivity(),
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package org.briarproject.briar.android.util;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.location.LocationManager;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.nullsafety.ParametersNotNullByDefault;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import static android.Manifest.permission.BLUETOOTH_ADVERTISE;
|
||||
import static android.Manifest.permission.BLUETOOTH_CONNECT;
|
||||
import static android.Manifest.permission.BLUETOOTH_SCAN;
|
||||
import static android.content.Intent.CATEGORY_DEFAULT;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS;
|
||||
import static android.widget.Toast.LENGTH_LONG;
|
||||
import static androidx.core.content.ContextCompat.checkSelfPermission;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static org.briarproject.briar.BuildConfig.APPLICATION_ID;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class PermissionUtils {
|
||||
|
||||
public static boolean gotPermission(Context ctx,
|
||||
@Nullable Map<String, Boolean> grantedMap, String permission) {
|
||||
if (grantedMap == null || !grantedMap.containsKey(permission)) {
|
||||
return isPermissionGranted(ctx, permission);
|
||||
} else {
|
||||
return TRUE.equals(grantedMap.get(permission));
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isPermissionGranted(Context ctx, String permission) {
|
||||
return checkSelfPermission(ctx, permission) ==
|
||||
PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
public static boolean areBluetoothPermissionsGranted(Context ctx) {
|
||||
if (SDK_INT < 31) return true;
|
||||
return isPermissionGranted(ctx, BLUETOOTH_ADVERTISE) &&
|
||||
isPermissionGranted(ctx, BLUETOOTH_CONNECT) &&
|
||||
isPermissionGranted(ctx, BLUETOOTH_SCAN);
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
public static boolean wasGrantedBluetoothPermissions(Context ctx,
|
||||
@Nullable Map<String, Boolean> grantedMap) {
|
||||
return grantedMap != null &&
|
||||
gotPermission(ctx, grantedMap, BLUETOOTH_ADVERTISE) &&
|
||||
gotPermission(ctx, grantedMap, BLUETOOTH_CONNECT) &&
|
||||
gotPermission(ctx, grantedMap, BLUETOOTH_SCAN);
|
||||
}
|
||||
|
||||
public static DialogInterface.OnClickListener getGoToSettingsListener(
|
||||
Context context) {
|
||||
return (dialog, which) -> {
|
||||
Intent i = new Intent();
|
||||
i.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
|
||||
i.addCategory(CATEGORY_DEFAULT);
|
||||
i.setData(Uri.parse("package:" + APPLICATION_ID));
|
||||
i.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(i);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if location is enabled for Bluetooth purposes,
|
||||
* or it isn't required due to this being a device with SDK < 28 or >= 31.
|
||||
*/
|
||||
public static boolean isLocationEnabledForBt(Context ctx) {
|
||||
if (SDK_INT >= 28 && SDK_INT < 31) {
|
||||
LocationManager lm = ctx.getSystemService(LocationManager.class);
|
||||
return lm.isLocationEnabled();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if location is enabled for Wi-Fi hotspot purposes,
|
||||
* or it isn't required due to this being a device with SDK < 31.
|
||||
*/
|
||||
public static boolean isLocationEnabledForWiFi(Context ctx) {
|
||||
if (SDK_INT >= 31) {
|
||||
LocationManager lm = ctx.getSystemService(LocationManager.class);
|
||||
return lm.isLocationEnabled();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void showLocationDialog(Context ctx) {
|
||||
showLocationDialog(ctx, true);
|
||||
}
|
||||
|
||||
public static void showLocationDialog(Context ctx, boolean forBluetooth) {
|
||||
AlertDialog.Builder builder =
|
||||
new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
|
||||
builder.setTitle(R.string.permission_location_setting_title);
|
||||
if (forBluetooth) {
|
||||
builder.setMessage(R.string.permission_location_setting_body);
|
||||
} else {
|
||||
builder.setMessage(
|
||||
R.string.permission_location_setting_hotspot_body);
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel, null);
|
||||
builder.setPositiveButton(R.string.permission_location_setting_button,
|
||||
(dialog, which) -> {
|
||||
Intent i = new Intent(ACTION_LOCATION_SOURCE_SETTINGS);
|
||||
try {
|
||||
ctx.startActivity(i);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(ctx, R.string.error_start_activity,
|
||||
LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* This closes the given activity when dialog gets cancelled.
|
||||
*/
|
||||
public static void showDenialDialog(FragmentActivity ctx,
|
||||
@StringRes int title, @StringRes int body) {
|
||||
showDenialDialog(ctx, title, body, ctx::supportFinishAfterTransition);
|
||||
}
|
||||
|
||||
public static void showDenialDialog(FragmentActivity ctx,
|
||||
@StringRes int title, @StringRes int body, Runnable onDenied) {
|
||||
AlertDialog.Builder builder =
|
||||
new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
|
||||
builder.setTitle(title);
|
||||
builder.setMessage(body);
|
||||
builder.setPositiveButton(R.string.ok, getGoToSettingsListener(ctx));
|
||||
builder.setNegativeButton(R.string.cancel, (dialog, which) ->
|
||||
onDenied.run());
|
||||
builder.show();
|
||||
}
|
||||
|
||||
public static void showRationale(FragmentActivity ctx, @StringRes int title,
|
||||
@StringRes int body, @Nullable Runnable onOk) {
|
||||
AlertDialog.Builder builder =
|
||||
new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
|
||||
builder.setTitle(title);
|
||||
builder.setMessage(body);
|
||||
builder.setNeutralButton(R.string.continue_button, (dialog, which) -> {
|
||||
if (onOk != null) onOk.run();
|
||||
dialog.dismiss();
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
public static void requestBluetoothPermissions(
|
||||
ActivityResultLauncher<String[]> launcher) {
|
||||
String[] perms = new String[] {BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT,
|
||||
BLUETOOTH_SCAN};
|
||||
launcher.launch(perms);
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,9 @@ import android.app.ActivityManager.MemoryInfo;
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.location.LocationManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Debug;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
@@ -44,7 +41,6 @@ import org.briarproject.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.nullsafety.ParametersNotNullByDefault;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
@@ -55,7 +51,6 @@ import androidx.annotation.ColorRes;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
@@ -71,15 +66,10 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
|
||||
|
||||
import static android.Manifest.permission.BLUETOOTH_ADVERTISE;
|
||||
import static android.Manifest.permission.BLUETOOTH_CONNECT;
|
||||
import static android.Manifest.permission.BLUETOOTH_SCAN;
|
||||
import static android.content.Context.KEYGUARD_SERVICE;
|
||||
import static android.content.Intent.CATEGORY_DEFAULT;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||
import static android.os.Build.MANUFACTURER;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS;
|
||||
import static android.text.format.DateUtils.DAY_IN_MILLIS;
|
||||
import static android.text.format.DateUtils.FORMAT_ABBREV_ALL;
|
||||
import static android.text.format.DateUtils.FORMAT_ABBREV_MONTH;
|
||||
@@ -109,13 +99,11 @@ import static androidx.core.content.ContextCompat.getColor;
|
||||
import static androidx.core.content.ContextCompat.getSystemService;
|
||||
import static androidx.core.graphics.drawable.DrawableCompat.setTint;
|
||||
import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_RTL;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.concurrent.TimeUnit.DAYS;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.briar.BuildConfig.APPLICATION_ID;
|
||||
import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE;
|
||||
import static org.briarproject.briar.android.TestingConstants.IS_OLD_ANDROID;
|
||||
import static org.briarproject.briar.android.TestingConstants.OLD_ANDROID_WARN_DATE;
|
||||
@@ -330,17 +318,6 @@ public class UiUtils {
|
||||
textView.setMovementMethod(new LinkMovementMethod());
|
||||
}
|
||||
|
||||
public static OnClickListener getGoToSettingsListener(Context context) {
|
||||
return (dialog, which) -> {
|
||||
Intent i = new Intent();
|
||||
i.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
|
||||
i.addCategory(CATEGORY_DEFAULT);
|
||||
i.setData(Uri.parse("package:" + APPLICATION_ID));
|
||||
i.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(i);
|
||||
};
|
||||
}
|
||||
|
||||
public static void showOnboardingDialog(Context ctx, String text) {
|
||||
new AlertDialog.Builder(ctx, R.style.OnboardingDialogTheme)
|
||||
.setMessage(text)
|
||||
@@ -349,19 +326,6 @@ public class UiUtils {
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if location is enabled,
|
||||
* or it isn't required due to this being a device with SDK < 28 or >= 31.
|
||||
*/
|
||||
public static boolean isLocationEnabled(Context ctx) {
|
||||
if (SDK_INT >= 28 && SDK_INT < 31) {
|
||||
LocationManager lm = ctx.getSystemService(LocationManager.class);
|
||||
return lm.isLocationEnabled();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isSamsung7() {
|
||||
return (SDK_INT == 24 || SDK_INT == 25) &&
|
||||
MANUFACTURER.equalsIgnoreCase("Samsung");
|
||||
@@ -528,34 +492,6 @@ public class UiUtils {
|
||||
return isoCode;
|
||||
}
|
||||
|
||||
public static void showLocationDialog(Context ctx) {
|
||||
showLocationDialog(ctx, true);
|
||||
}
|
||||
|
||||
public static void showLocationDialog(Context ctx, boolean forBluetooth) {
|
||||
AlertDialog.Builder builder =
|
||||
new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
|
||||
builder.setTitle(R.string.permission_location_setting_title);
|
||||
if (forBluetooth) {
|
||||
builder.setMessage(R.string.permission_location_setting_body);
|
||||
} else {
|
||||
builder.setMessage(
|
||||
R.string.permission_location_setting_hotspot_body);
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel, null);
|
||||
builder.setPositiveButton(R.string.permission_location_setting_button,
|
||||
(dialog, which) -> {
|
||||
Intent i = new Intent(ACTION_LOCATION_SOURCE_SETTINGS);
|
||||
try {
|
||||
ctx.startActivity(i);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(ctx, R.string.error_start_activity,
|
||||
LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
public static Drawable getDialogIcon(Context ctx, @DrawableRes int resId) {
|
||||
Drawable icon =
|
||||
VectorDrawableCompat.create(ctx.getResources(), resId, null);
|
||||
@@ -595,29 +531,6 @@ public class UiUtils {
|
||||
SOFT_INPUT_STATE_HIDDEN);
|
||||
}
|
||||
|
||||
public static void showDenialDialog(FragmentActivity ctx,
|
||||
@StringRes int title, @StringRes int body) {
|
||||
AlertDialog.Builder builder =
|
||||
new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
|
||||
builder.setTitle(title);
|
||||
builder.setMessage(body);
|
||||
builder.setPositiveButton(R.string.ok, getGoToSettingsListener(ctx));
|
||||
builder.setNegativeButton(R.string.cancel,
|
||||
(dialog, which) -> ctx.supportFinishAfterTransition());
|
||||
builder.show();
|
||||
}
|
||||
|
||||
public static void showRationale(FragmentActivity ctx, @StringRes int title,
|
||||
@StringRes int body, Runnable requestPermissions) {
|
||||
AlertDialog.Builder builder =
|
||||
new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
|
||||
builder.setTitle(title);
|
||||
builder.setMessage(body);
|
||||
builder.setNeutralButton(R.string.continue_button,
|
||||
(dialog, which) -> requestPermissions.run());
|
||||
builder.show();
|
||||
}
|
||||
|
||||
public static void launchActivityToOpenFile(Context ctx,
|
||||
@Nullable ActivityResultLauncher<String[]> docLauncher,
|
||||
ActivityResultLauncher<String> contentLauncher,
|
||||
@@ -640,20 +553,4 @@ public class UiUtils {
|
||||
Toast.makeText(ctx, R.string.error_start_activity, LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
public static void requestBluetoothPermissions(
|
||||
ActivityResultLauncher<String[]> launcher) {
|
||||
String[] perms = new String[] {BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT,
|
||||
BLUETOOTH_SCAN};
|
||||
launcher.launch(perms);
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
public static boolean wasGrantedBluetoothPermissions(
|
||||
@Nullable Map<String, Boolean> grantedMap) {
|
||||
return grantedMap != null &&
|
||||
TRUE.equals(grantedMap.get(BLUETOOTH_ADVERTISE)) &&
|
||||
TRUE.equals(grantedMap.get(BLUETOOTH_CONNECT)) &&
|
||||
TRUE.equals(grantedMap.get(BLUETOOTH_SCAN));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user