mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-07-11 01:25:07 +02:00
Merge pull request #21836 from hrydgard/misc-work
Implement support for Android 17 behavior changes
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
|
||||
enum SystemPermission {
|
||||
SYSTEM_PERMISSION_STORAGE,
|
||||
SYSTEM_PERMISSION_LOCAL_NETWORK,
|
||||
};
|
||||
|
||||
enum PermissionStatus {
|
||||
@@ -242,6 +243,8 @@ enum SystemProperty {
|
||||
SYSPROP_USE_APP_STORE,
|
||||
SYSPROP_SUPPORTS_SHARE_TEXT,
|
||||
|
||||
SYSPROP_CAN_RESTRICT_ORIENTATION,
|
||||
|
||||
SYSPROP_INSTALLER_NAME, // Useful on Android to check if we were installed from the play store.
|
||||
};
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ void DisplayLayoutScreen::CreateViews() {
|
||||
rightColumn->Add(new CheckBox(&config.bIgnoreScreenInsets, gr->T("Ignore camera notch when centering")));
|
||||
}
|
||||
|
||||
if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_MOBILE) {
|
||||
if (System_GetPropertyBool(SYSPROP_CAN_RESTRICT_ORIENTATION)) {
|
||||
rightColumn->Add(new Spacer(12.0f));
|
||||
AddRotationPicker(screenManager(), rightColumn, true);
|
||||
}
|
||||
|
||||
@@ -1424,15 +1424,15 @@ void GameSettingsScreen::CreateSystemSettings(UI::ViewGroup *systemSettings) {
|
||||
PopupSliderChoice *exitConfirmation = systemSettings->Add(new PopupSliderChoice(&g_Config.iAskForExitConfirmationAfterSeconds, 0, 1200, 300, sy->T("Ask for exit confirmation after seconds"), screenManager(), "s"));
|
||||
exitConfirmation->SetZeroLabel(sy->T("Off"));
|
||||
|
||||
if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_MOBILE) {
|
||||
if (System_GetPropertyBool(SYSPROP_CAN_RESTRICT_ORIENTATION)) {
|
||||
auto co = GetI18NCategory(I18NCat::CONTROLS);
|
||||
|
||||
// Display rotation
|
||||
AddRotationPicker(screenManager(), systemSettings, true);
|
||||
}
|
||||
|
||||
if (System_GetPropertyBool(SYSPROP_SUPPORTS_SUSTAINED_PERF_MODE)) {
|
||||
systemSettings->Add(new CheckBox(&g_Config.bSustainedPerformanceMode, sy->T("Sustained performance mode")))->OnClick.Handle(this, &GameSettingsScreen::OnSustainedPerformanceModeChange);
|
||||
}
|
||||
if (System_GetPropertyBool(SYSPROP_SUPPORTS_SUSTAINED_PERF_MODE)) {
|
||||
systemSettings->Add(new CheckBox(&g_Config.bSustainedPerformanceMode, sy->T("Sustained performance mode")))->OnClick.Handle(this, &GameSettingsScreen::OnSustainedPerformanceModeChange);
|
||||
}
|
||||
|
||||
systemSettings->Add(new Choice(sy->T("Restore Default Settings")))->OnClick.Handle(this, &GameSettingsScreen::OnRestoreDefaultSettings);
|
||||
|
||||
+1
-1
@@ -760,7 +760,7 @@ void GamePauseScreen::CreateViews() {
|
||||
screenManager()->push(new GameScreen(gamePath_, true));
|
||||
});
|
||||
|
||||
if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_MOBILE) {
|
||||
if (System_GetPropertyBool(SYSPROP_CAN_RESTRICT_ORIENTATION)) {
|
||||
AddRotationPicker(screenManager(), middleColumn, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "Common/Net/Resolve.h"
|
||||
#include "Common/Net/URL.h"
|
||||
#include "Common/Thread/ThreadUtil.h"
|
||||
#include "Common/System/System.h"
|
||||
#include "Common/System/Request.h"
|
||||
|
||||
#include "Common/File/PathBrowser.h"
|
||||
@@ -313,12 +314,34 @@ void RemoteISOScreen::update() {
|
||||
serverRunning_ = nowRunning;
|
||||
}
|
||||
|
||||
void RemoteISOScreen::sendMessage(UIMessage message, const char *value) {
|
||||
UITabbedBaseDialogScreen::sendMessage(message, value);
|
||||
switch (message) {
|
||||
case UIMessage::PERMISSION_GRANTED:
|
||||
if (equals(value, "local_network")) {
|
||||
RecreateViews();
|
||||
} else {
|
||||
ERROR_LOG(Log::UI, "Unknown permission granted: %s", value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteISOScreen::CreateConnectTab(UI::ViewGroup *tab) {
|
||||
auto di = GetI18NCategory(I18NCat::DIALOG);
|
||||
auto ri = GetI18NCategory(I18NCat::REMOTEISO);
|
||||
|
||||
using namespace UI;
|
||||
|
||||
PermissionStatus status = System_GetPermissionStatus(SYSTEM_PERMISSION_LOCAL_NETWORK);
|
||||
if (status != PERMISSION_STATUS_GRANTED) {
|
||||
// Proceed with local network functionality
|
||||
tab->Add(new Choice(ri->T("Ask for network permission")))->OnClick.Add([this](UI::EventParams &e) {
|
||||
System_AskForPermission(SYSTEM_PERMISSION_LOCAL_NETWORK);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverRunning_) {
|
||||
tab->Add(new NoticeView(NoticeLevel::SUCCESS, ri->T("Currently sharing"), "", new LinearLayoutParams(Margins(12, 5, 0, 5))));
|
||||
} else {
|
||||
|
||||
@@ -46,6 +46,8 @@ protected:
|
||||
void HandleStopServer(UI::EventParams &e);
|
||||
void HandleBrowse(UI::EventParams &e);
|
||||
|
||||
void sendMessage(UIMessage message, const char *value) override;
|
||||
|
||||
UI::TextView *firewallWarning_ = nullptr;
|
||||
bool serverRunning_ = false;
|
||||
bool serverStopping_ = false;
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<uses-permission-sdk-23 android:name="android.permission.TRANSMIT_IR" />
|
||||
<uses-permission-sdk-23 android:name="android.permission.CAMERA" />
|
||||
<uses-permission-sdk-23 android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
|
||||
|
||||
<!-- AndroidX minimum SDK workaround. We don't care if it's broken on older versions. -->
|
||||
<uses-sdk tools:overrideLibrary="androidx.appcompat.resources,androidx.appcompat,androidx.fragment,androidx.drawerlayout,androidx.vectordrawable.animated,androidx.vectordrawable,androidx.viewpager,androidx.loader,androidx.activity,androidx.annotation,androidx.customview,androidx.cursoradapter,androidx.arch,androidx.collection,androidx.core,androidx.versionedparcelable,androidx.interpolator,androidx.lifecycle,androidx.loader,androidx.savedstate,androidx.lifecycle.viewmodel,androidx.lifecycle.livedata,androidx.lifecycle.livedata.core,androidx.arch.core,androidx.documentfile"/>
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import com.google.protobuf.gradle.*
|
||||
import org.gradle.process.ExecOperations
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("com.google.protobuf")
|
||||
@@ -67,7 +63,7 @@ dependencies {
|
||||
// Will replace with a different plugin soon.
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.documentfile:documentfile:1.1.0")
|
||||
implementation("com.google.protobuf:protobuf-javalite:4.33.5")
|
||||
implementation("com.google.protobuf:protobuf-javalite:4.35.1")
|
||||
}
|
||||
|
||||
protobuf {
|
||||
@@ -110,8 +106,8 @@ android {
|
||||
ndkVersion = "29.0.14206865"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
lint {
|
||||
|
||||
+33
-11
@@ -150,6 +150,7 @@ static int optimalSampleRate = 0;
|
||||
static int sampleRate = 0;
|
||||
static int framesPerBuffer = 0;
|
||||
static int androidVersion;
|
||||
static int smallestScreenWidthDp;
|
||||
static int deviceType;
|
||||
|
||||
// This is the ACTUAL display size, not the hardware scaled display size.
|
||||
@@ -529,6 +530,12 @@ bool System_GetPropertyBool(SystemProperty prop) {
|
||||
return false; // We can't create shortcuts directly from game code, but we can from the Android UI.
|
||||
case SYSPROP_DISPLAY_HAS_CAMERA_CUTOUT:
|
||||
return g_hasCameraCutout;
|
||||
case SYSPROP_CAN_RESTRICT_ORIENTATION:
|
||||
// On Android 17+, large displays (sw600dp+) ignore orientation restrictions.
|
||||
if (androidVersion >= 37 && smallestScreenWidthDp >= 600) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#ifndef HTTPS_NOT_AVAILABLE
|
||||
case SYSPROP_SUPPORTS_HTTPS:
|
||||
return !g_Config.bDisableHTTPS;
|
||||
@@ -738,7 +745,7 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init
|
||||
(JNIEnv * env, jclass, jstring jmodel, jint jdeviceType, jstring jlangRegion, jstring japkpath,
|
||||
jstring jdataDir, jstring jexternalStorageDir, jstring jexternalFilesDir, jstring jNativeLibDir,
|
||||
jstring jadditionalStorageDirs, jstring jcacheDir, jstring jshortcutParam, jstring jInstallerName,
|
||||
jint jAndroidVersion, jstring jboard) {
|
||||
jint jAndroidVersion, jstring jboard, jint jSmallestScreenWidthDp) {
|
||||
SetCurrentThreadName("androidInit");
|
||||
|
||||
// Makes sure we get early permission grants.
|
||||
@@ -750,6 +757,7 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init
|
||||
renderer_inited = false;
|
||||
exitRenderLoop = false;
|
||||
androidVersion = jAndroidVersion;
|
||||
smallestScreenWidthDp = jSmallestScreenWidthDp;
|
||||
deviceType = jdeviceType;
|
||||
|
||||
Path apkPath(GetJavaString(env, japkpath));
|
||||
@@ -1246,6 +1254,9 @@ void System_AskForPermission(SystemPermission permission) {
|
||||
case SYSTEM_PERMISSION_STORAGE:
|
||||
PushCommand("ask_permission", "storage");
|
||||
break;
|
||||
case SYSTEM_PERMISSION_LOCAL_NETWORK:
|
||||
PushCommand("ask_permission", "local_network");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1450,18 +1461,29 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendMessageFromJava(JNI
|
||||
if (msg == "moga") {
|
||||
mogaVersion = prm;
|
||||
} else if (msg == "permission_pending") {
|
||||
INFO_LOG(Log::System, "STORAGE PERMISSION: PENDING");
|
||||
// TODO: Add support for other permissions
|
||||
permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_PENDING;
|
||||
// Don't need to send along, nothing else is listening.
|
||||
if (prm == "storage") {
|
||||
INFO_LOG(Log::System, "STORAGE PERMISSION: PENDING");
|
||||
permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_PENDING;
|
||||
} else if (prm == "local_network") {
|
||||
INFO_LOG(Log::System, "LOCAL NETWORK PERMISSION: PENDING");
|
||||
permissions[SYSTEM_PERMISSION_LOCAL_NETWORK] = PERMISSION_STATUS_PENDING;
|
||||
}
|
||||
} else if (msg == "permission_denied") {
|
||||
INFO_LOG(Log::System, "STORAGE PERMISSION: DENIED");
|
||||
permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_DENIED;
|
||||
// Don't need to send along, nothing else is listening.
|
||||
if (prm == "storage") {
|
||||
INFO_LOG(Log::System, "STORAGE PERMISSION: DENIED");
|
||||
permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_DENIED;
|
||||
} else if (prm == "local_network") {
|
||||
INFO_LOG(Log::System, "LOCAL NETWORK PERMISSION: DENIED");
|
||||
permissions[SYSTEM_PERMISSION_LOCAL_NETWORK] = PERMISSION_STATUS_DENIED;
|
||||
}
|
||||
} else if (msg == "permission_granted") {
|
||||
INFO_LOG(Log::System, "STORAGE PERMISSION: GRANTED");
|
||||
permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_GRANTED;
|
||||
// Send along.
|
||||
if (prm == "storage") {
|
||||
INFO_LOG(Log::System, "STORAGE PERMISSION: GRANTED");
|
||||
permissions[SYSTEM_PERMISSION_STORAGE] = PERMISSION_STATUS_GRANTED;
|
||||
} else if (prm == "local_network") {
|
||||
INFO_LOG(Log::System, "LOCAL NETWORK PERMISSION: GRANTED");
|
||||
permissions[SYSTEM_PERMISSION_LOCAL_NETWORK] = PERMISSION_STATUS_GRANTED;
|
||||
}
|
||||
System_PostUIMessage(UIMessage::PERMISSION_GRANTED, prm);
|
||||
} else if (msg == "sustained_perf_supported") {
|
||||
sustainedPerfSupported = true;
|
||||
|
||||
@@ -44,7 +44,9 @@ public class ContentUri {
|
||||
Log.e(TAG, "Failed to get file descriptor for " + uriString);
|
||||
return -1;
|
||||
}
|
||||
return filePfd.detachFd(); // Take ownership of the fd.
|
||||
// Review note: We intentionally "leak" the fd here, the caller takes ownership and closes
|
||||
// it when done with it.
|
||||
return filePfd.detachFd();
|
||||
}
|
||||
} catch (java.lang.IllegalArgumentException e) {
|
||||
// This exception is long and ugly and really just means file not found.
|
||||
@@ -70,13 +72,13 @@ public class ContentUri {
|
||||
final String documentName = c.getString(0);
|
||||
if (prefixLowercase != null && !prefixLowercase.isEmpty()) {
|
||||
// Filter by prefix, case insensitive.
|
||||
if (!documentName.toLowerCase(Locale.ROOT).startsWith(prefixLowercase)) {
|
||||
if (!documentName.regionMatches(true, 0, prefixLowercase, 0, prefixLowercase.length())) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final String mimeType = c.getString(3);
|
||||
final boolean isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
|
||||
final boolean isDirectory = DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType);
|
||||
final long size = isDirectory ? 0 : c.getLong(1);
|
||||
final long lastModified = c.getLong(4);
|
||||
|
||||
@@ -222,6 +224,8 @@ public class ContentUri {
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
// NOTE: If the file already exists, this will NOT return an error, instead the OS will create a new
|
||||
// file appending an incrementing number, for example (1), to the filename.
|
||||
public static int contentUriCreateFile(Activity activity, String rootTreeUri, String fileName) {
|
||||
try {
|
||||
Uri uri = Uri.parse(rootTreeUri);
|
||||
@@ -232,7 +236,7 @@ public class ContentUri {
|
||||
DocumentFile createdFile = documentFile.createFile("application/octet-stream", fileName);
|
||||
return createdFile != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN;
|
||||
} else {
|
||||
Log.e(TAG, "contentUriCreateFile: fromTreeUrisv returned null");
|
||||
Log.e(TAG, "contentUriCreateFile: fromTreeUri returned null");
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -29,7 +29,7 @@ public class NativeApp {
|
||||
public static final int RESULT_ERROR_ACTIVITY_NOT_FOUND = 1;
|
||||
public static final int RESULT_ERROR_OTHER_ACTIVITY_ERROR = 2;
|
||||
|
||||
public static native void init(String model, int deviceType, String languageRegion, String apkPath, String dataDir, String externalStorageDir, String extFilesDir, String nativeLibDir, String additionalStorageDirs, String cacheDir, String shortcutParam, String installerName, int androidVersion, String board);
|
||||
public static native void init(String model, int deviceType, String languageRegion, String apkPath, String dataDir, String externalStorageDir, String extFilesDir, String nativeLibDir, String additionalStorageDirs, String cacheDir, String shortcutParam, String installerName, int androidVersion, String board, int smallestScreenWidthDp);
|
||||
public static native void audioInit();
|
||||
public static native void audioShutdown();
|
||||
public static native void audioConfig(int optimalFramesPerBuffer, int optimalSampleRate);
|
||||
|
||||
@@ -143,11 +143,15 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
private static final String[] permissionsForMicrophone = {
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
};
|
||||
private static final String[] permissionsForLocalNetwork = {
|
||||
"android.permission.ACCESS_LOCAL_NETWORK"
|
||||
};
|
||||
|
||||
public static final int REQUEST_CODE_STORAGE_PERMISSION = 1;
|
||||
public static final int REQUEST_CODE_LOCATION_PERMISSION = 2;
|
||||
public static final int REQUEST_CODE_CAMERA_PERMISSION = 3;
|
||||
public static final int REQUEST_CODE_MICROPHONE_PERMISSION = 4;
|
||||
public static final int REQUEST_CODE_LOCAL_NETWORK_PERMISSION = 5;
|
||||
|
||||
// Once we received a "modern" mouse event, we stop listening to old style mouse
|
||||
// button events.
|
||||
@@ -255,6 +259,16 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
} else {
|
||||
NativeApp.sendMessageFromJava("permission_denied", "storage");
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 35) {
|
||||
if (this.checkSelfPermission("android.permission.ACCESS_LOCAL_NETWORK") == PackageManager.PERMISSION_GRANTED) {
|
||||
NativeApp.sendMessageFromJava("permission_granted", "local_network");
|
||||
} else {
|
||||
NativeApp.sendMessageFromJava("permission_denied", "local_network");
|
||||
}
|
||||
} else {
|
||||
NativeApp.sendMessageFromJava("permission_granted", "local_network");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,6 +306,13 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
NativeApp.audioRecording_Start();
|
||||
}
|
||||
break;
|
||||
case REQUEST_CODE_LOCAL_NETWORK_PERMISSION:
|
||||
if (permissionsGranted(permissions, grantResults)) {
|
||||
NativeApp.sendMessageFromJava("permission_granted", "local_network");
|
||||
} else {
|
||||
NativeApp.sendMessageFromJava("permission_denied", "local_network");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -512,8 +533,9 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
PackageManager packageManager = getPackageManager();
|
||||
String installerName = getInstallerName(packageManager);
|
||||
|
||||
int smallestScreenWidthDp = getResources().getConfiguration().smallestScreenWidthDp;
|
||||
NativeApp.audioConfig(optimalFramesPerBuffer, optimalSampleRate);
|
||||
NativeApp.init(model, deviceType, languageRegion, apkFilePath, dataDir, extStorageDir, externalFilesDir, nativeLibDir, additionalStorageDirs, cacheDir, shortcut, installerName, Build.VERSION.SDK_INT, Build.BOARD);
|
||||
NativeApp.init(model, deviceType, languageRegion, apkFilePath, dataDir, extStorageDir, externalFilesDir, nativeLibDir, additionalStorageDirs, cacheDir, shortcut, installerName, Build.VERSION.SDK_INT, Build.BOARD, smallestScreenWidthDp);
|
||||
|
||||
// Allow C++ to tell us to use JavaGL or not.
|
||||
javaGL = "true".equalsIgnoreCase(NativeApp.queryConfig("androidJavaGL"));
|
||||
@@ -584,6 +606,11 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
private void updateScreenRotation(String cause) {
|
||||
if (Build.VERSION.SDK_INT >= 37 && getResources().getConfiguration().smallestScreenWidthDp >= 600) {
|
||||
// Android 17+ on large screens (sw600dp+) ignores orientation requests to push for adaptive apps.
|
||||
// If we try anyway, it's just a waste of time and might cause weirdness.
|
||||
return;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
if (isInMultiWindowMode()) {
|
||||
// Do not try to enforce rotation! This can result in re-init loops.
|
||||
@@ -1761,6 +1788,17 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
NativeApp.sendMessageFromJava("permission_granted", "storage");
|
||||
}
|
||||
return true;
|
||||
} else if (command.equals("ask_permission") && params.equals("local_network")) {
|
||||
if (Build.VERSION.SDK_INT >= 35) {
|
||||
if (askForPermissions(permissionsForLocalNetwork, REQUEST_CODE_LOCAL_NETWORK_PERMISSION)) {
|
||||
NativeApp.sendMessageFromJava("permission_pending", "local_network");
|
||||
} else {
|
||||
NativeApp.sendMessageFromJava("permission_granted", "local_network");
|
||||
}
|
||||
} else {
|
||||
NativeApp.sendMessageFromJava("permission_granted", "local_network");
|
||||
}
|
||||
return true;
|
||||
} else if (command.equals("gps_command")) {
|
||||
if (params.equals("open")) {
|
||||
if (!askForPermissions(permissionsForLocation, REQUEST_CODE_LOCATION_PERMISSION)) {
|
||||
|
||||
@@ -412,6 +412,9 @@ bool System_GetPropertyBool(SystemProperty prop) {
|
||||
case SYSPROP_CAN_READ_BATTERY_PERCENTAGE:
|
||||
return true;
|
||||
|
||||
case SYSPROP_CAN_RESTRICT_ORIENTATION:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user