fix init app not found id,change ffi from MainActivity to MainService,add boot service but not open
This commit is contained in:
parent
2137f4b3f2
commit
9c3b10d6a9
@ -9,11 +9,23 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<!--<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />-->
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:label="RustDesk">
|
||||
android:label="RustDesk"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
|
||||
<!-- 暂时不开启接收开机广播的功能 enabled设置为false-->
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<intent-filter android:priority="1000">
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".InputService"
|
||||
android:enabled="true"
|
||||
@ -23,6 +35,7 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/accessibility_service_config" />
|
||||
@ -31,11 +44,11 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<!--
|
||||
Specifies an Android theme to apply to this Activity as soon as
|
||||
|
@ -0,0 +1,24 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
|
||||
// 开机自启动 此功能暂不开启
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if ("android.intent.action.BOOT_COMPLETED" == intent.action){
|
||||
val it = Intent(context,MainService::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(it)
|
||||
}else{
|
||||
context.startService(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -50,7 +50,6 @@ class MainActivity : FlutterActivity() {
|
||||
result.success(true)
|
||||
}
|
||||
"start_capture" -> {
|
||||
// return bool
|
||||
mainService?.let {
|
||||
result.success(it.startCapture())
|
||||
} ?: let {
|
||||
|
@ -95,6 +95,7 @@ class MainService : Service() {
|
||||
fun rustSetByName(name: String, arg1: String, arg2: String) {
|
||||
when (name) {
|
||||
"try_start_without_auth" -> {
|
||||
// TODO 改成 json 三个参数 类型 name id
|
||||
// to UI
|
||||
Log.d(logTag, "from rust:got try_start_without_auth")
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
@ -105,7 +106,6 @@ class MainService : Service() {
|
||||
Log.d(logTag, "activity.runOnUiThread invokeMethod try_start_without_auth,done")
|
||||
}
|
||||
// TODO notify
|
||||
Log.d(logTag, "kotlin invokeMethod try_start_without_auth,done")
|
||||
}
|
||||
"start_capture" -> {
|
||||
Log.d(logTag, "from rust:start_capture")
|
||||
@ -114,7 +114,7 @@ class MainService : Service() {
|
||||
name,
|
||||
mapOf("peerID" to arg1, "name" to arg2)
|
||||
)
|
||||
Log.d(logTag, "activity.runOnUiThread invokeMethod try_start_without_auth,done")
|
||||
Log.d(logTag, "activity.runOnUiThread invokeMethod start_capture,done")
|
||||
}
|
||||
if (isStart) {
|
||||
Log.d(logTag, "正在录制")
|
||||
@ -129,7 +129,7 @@ class MainService : Service() {
|
||||
stopCapture()
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
MainActivity.flutterMethodChannel.invokeMethod(name, null)
|
||||
Log.d(logTag, "activity.runOnUiThread invokeMethod try_start_without_auth,done")
|
||||
Log.d(logTag, "activity.runOnUiThread invokeMethod stop_capture,done")
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
@ -139,13 +139,14 @@ class MainService : Service() {
|
||||
// jvm call rust
|
||||
private external fun init(ctx: Context)
|
||||
private external fun startServer()
|
||||
private external fun ready()
|
||||
private external fun sendVp9(data: ByteArray)
|
||||
|
||||
private val logTag = "LOG_SERVICE"
|
||||
private val useVP9 = false
|
||||
private val binder = LocalBinder()
|
||||
private var _isReady = false
|
||||
private var _isStart = false
|
||||
private var _isReady = false // 是否获取了录屏权限
|
||||
private var _isStart = false // 是否正在进行录制
|
||||
val isReady: Boolean
|
||||
get() = _isReady
|
||||
val isStart: Boolean
|
||||
@ -177,6 +178,7 @@ class MainService : Service() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
initNotification()
|
||||
startServer() // 开启了rust服务但是没有设置可以接收连接 如果不开启 首次启动没法获得服务ID
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
@ -194,11 +196,9 @@ class MainService : Service() {
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.d("whichService", "this service:${Thread.currentThread()}")
|
||||
// initService是关键的逻辑 在用户点击开始监听或者获取到视频捕捉权限的时候执行initService
|
||||
// 只有init的时候通过onStartCommand 且开启前台服务
|
||||
if (intent?.action == INIT_SERVICE) {
|
||||
Log.d(logTag, "service starting:${startId}:${Thread.currentThread()}")
|
||||
// createForegroundNotification(this)
|
||||
createForegroundNotification()
|
||||
val mMediaProjectionManager =
|
||||
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
@ -209,12 +209,13 @@ class MainService : Service() {
|
||||
checkMediaPermission()
|
||||
surface = createSurface()
|
||||
init(this)
|
||||
startServer()
|
||||
ready()
|
||||
_isReady = true
|
||||
} ?: let {
|
||||
Log.d(logTag, "获取mMediaProjection失败!")
|
||||
}
|
||||
} else if (intent?.action == ACTION_LOGIN_REQ_NOTIFY) {
|
||||
// TODO notify 重新适配多连接的情况
|
||||
val notifyLoginRes = intent.getBooleanExtra(EXTRA_LOGIN_REQ_NOTIFY, false)
|
||||
Log.d(logTag, "从通知栏点击了:$notifyLoginRes")
|
||||
}
|
||||
@ -264,6 +265,9 @@ class MainService : Service() {
|
||||
}
|
||||
|
||||
fun startCapture(): Boolean {
|
||||
if (isStart){
|
||||
return true
|
||||
}
|
||||
if (mediaProjection == null) {
|
||||
Log.w(logTag, "startCapture fail,mediaProjection is null")
|
||||
return false
|
||||
@ -288,6 +292,7 @@ class MainService : Service() {
|
||||
fun stopCapture() {
|
||||
Log.d(logTag, "Stop Capture")
|
||||
_isStart = false
|
||||
audioRecordStat = false
|
||||
virtualDisplay?.release()
|
||||
videoEncoder?.let {
|
||||
it.signalEndOfInputStream()
|
||||
@ -295,15 +300,12 @@ class MainService : Service() {
|
||||
it.release()
|
||||
}
|
||||
audioRecorder?.startRecording()
|
||||
audioRecordStat = false
|
||||
|
||||
// audioRecorder 如果无法重新创建 保留服务的情况不要释放
|
||||
// audioRecorder?.stop()
|
||||
// mediaProjection?.stop()
|
||||
|
||||
virtualDisplay = null
|
||||
videoEncoder = null
|
||||
videoData = null
|
||||
// audioRecorder 如果无法重新创建 保留服务的情况不要释放
|
||||
// audioRecorder?.stop()
|
||||
// audioRecorder = null
|
||||
// audioData = null
|
||||
}
|
||||
@ -318,6 +320,7 @@ class MainService : Service() {
|
||||
|
||||
mediaProjection = null
|
||||
checkMediaPermission()
|
||||
stopService(Intent(this,InputService::class.java)) // close input service maybe not work
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
}
|
||||
|
@ -35,34 +35,6 @@ fun testVP9Support(): Boolean {
|
||||
return res != null
|
||||
}
|
||||
|
||||
fun createForegroundNotification(ctx: Service) {
|
||||
// 设置通知渠道 android8开始引入 老版本会被忽略 这个东西的作用相当于为通知分类 给用户选择通知消息的种类
|
||||
val channelId =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channelId = "RustDeskForeground"
|
||||
val channelName = "RustDesk屏幕分享服务状态"
|
||||
val channel = NotificationChannel(
|
||||
channelId,
|
||||
channelName, NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
description = "Share your Android Screen with RustDeskService"
|
||||
}
|
||||
channel.lightColor = Color.BLUE
|
||||
channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
val service = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
service.createNotificationChannel(channel)
|
||||
channelId
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
val notification: Notification = NotificationCompat.Builder(ctx, channelId)
|
||||
.setOngoing(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
ctx.startForeground(11, notification)
|
||||
}
|
||||
|
||||
fun createNormalNotification(
|
||||
ctx: Context,
|
||||
title: String,
|
||||
|
@ -291,6 +291,50 @@ https://developer.android.com/about/versions/oreo/background?hl=zh-cn#services
|
||||
|
||||
<hr>
|
||||
|
||||
### 开机自启动
|
||||
利用接收RECEIVE_BOOT_COMPLETED的系统广播
|
||||
- 权限:
|
||||
- 清单文件
|
||||
```xml
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
...
|
||||
<application...>
|
||||
...
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter android:priority="1000">
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
...
|
||||
```
|
||||
- 创建一个class文件,类继承自BroadcastReceiver()
|
||||
```kotlin
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent != null) {
|
||||
if ("android.intent.action.BOOT_COMPLETED" == intent.action){
|
||||
val it = Intent(context,MainService::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context?.startForegroundService(it)
|
||||
}else{
|
||||
context?.startService(it)
|
||||
}
|
||||
Log.d(LOG_TAG,"onReceive done")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
|
||||
### 其他
|
||||
- Kotlin 与 compose 版本设置问题
|
||||
- https://stackoverflow.com/questions/67600344/jetpack-compose-on-kotlin-1-5-0
|
||||
|
@ -228,9 +228,8 @@ toAndroidChannelInit() {
|
||||
}
|
||||
case "start_capture":
|
||||
{
|
||||
var peerID = call.arguments["peerID"] as String;
|
||||
var name = call.arguments["name"] as String;
|
||||
ServerPage.serverModel.setPeer(true, name: name, id: peerID);
|
||||
clearLoginReqAlert();
|
||||
ServerPage.serverModel.updateClientState();
|
||||
break;
|
||||
}
|
||||
case "stop_capture":
|
||||
|
@ -547,8 +547,8 @@ class ClientState {
|
||||
class ServerModel with ChangeNotifier {
|
||||
bool _mediaOk;
|
||||
bool _inputOk;
|
||||
|
||||
bool _isStart;
|
||||
// bool _needServerOpen;
|
||||
bool _isPeerStart;
|
||||
bool _isFileTransfer;
|
||||
String _peerName;
|
||||
String _peerID;
|
||||
@ -557,7 +557,9 @@ class ServerModel with ChangeNotifier {
|
||||
|
||||
bool get inputOk => _inputOk;
|
||||
|
||||
bool get isStart => _isStart;
|
||||
// bool get needServerOpen => _needServerOpen;
|
||||
|
||||
bool get isPeerStart => _isPeerStart;
|
||||
|
||||
bool get isFileTransfer => _isFileTransfer;
|
||||
|
||||
@ -568,11 +570,16 @@ class ServerModel with ChangeNotifier {
|
||||
ServerModel() {
|
||||
_mediaOk = false;
|
||||
_inputOk = false;
|
||||
_isStart = false;
|
||||
_isPeerStart = false;
|
||||
_peerName = "";
|
||||
_peerID = "";
|
||||
}
|
||||
|
||||
// setNeedServerOpen(bool v){
|
||||
// _needServerOpen = v;
|
||||
// notifyListeners();
|
||||
// }
|
||||
|
||||
changeStatue(String name, bool value) {
|
||||
switch (name) {
|
||||
case "media":
|
||||
@ -588,7 +595,7 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
setPeer(bool enabled, {String name = "", String id = ""}) {
|
||||
_isStart = enabled;
|
||||
_isPeerStart = enabled;
|
||||
if (name != "") _peerName = name;
|
||||
if (id != "") _peerID = id;
|
||||
notifyListeners();
|
||||
@ -599,7 +606,7 @@ class ServerModel with ChangeNotifier {
|
||||
debugPrint("getByName client_state string:$res");
|
||||
try {
|
||||
var clientState = ClientState.fromJson(jsonDecode(res));
|
||||
_isStart = clientState.isStart;
|
||||
_isPeerStart = clientState.isStart;
|
||||
_isFileTransfer = clientState.isFileTransfer;
|
||||
_peerName = clientState.name;
|
||||
_peerID = clientState.peerId;
|
||||
@ -609,7 +616,7 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
clearPeer() {
|
||||
_isStart = false;
|
||||
_isPeerStart = false;
|
||||
_peerName = "";
|
||||
_peerID = "";
|
||||
notifyListeners();
|
||||
|
@ -57,13 +57,8 @@ class ServerPage extends StatelessWidget {
|
||||
|
||||
void checkService() {
|
||||
// 检测当前服务状态,若已存在服务则异步更新数据回来
|
||||
toAndroidChannel.invokeMethod("check_service"); // jvm
|
||||
toAndroidChannel.invokeMethod("check_service"); // jvm
|
||||
ServerPage.serverModel.updateClientState();
|
||||
// var state = FFI.getByName("client_state").split(":"); // rust
|
||||
// var isStart = FFI.getByName("client_is_start") !="";// 使用JSON
|
||||
// if(state.length == 2){
|
||||
// ServerPage.serverModel.setPeer(isStart,name:state[0],id:state[1]);
|
||||
// }
|
||||
}
|
||||
|
||||
class ServerInfo extends StatefulWidget {
|
||||
@ -75,14 +70,20 @@ class _ServerInfoState extends State<ServerInfo> {
|
||||
var _passwdShow = false;
|
||||
|
||||
// TODO set ID / PASSWORD
|
||||
var _serverId = "";
|
||||
var _serverPasswd = "";
|
||||
var _serverId = TextEditingController(text: "");
|
||||
var _serverPasswd = TextEditingController(text: "");
|
||||
static const _emptyIdShow = "正在获取ID...";
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_serverId = FFI.getByName("server_id");
|
||||
_serverPasswd = FFI.getByName("server_password");
|
||||
var id = FFI.getByName("server_id");
|
||||
_serverId.text = id==""?_emptyIdShow:id;
|
||||
_serverPasswd.text = FFI.getByName("server_password");
|
||||
if(_serverId.text == _emptyIdShow || _serverPasswd.text == ""){
|
||||
fetchConfigAgain();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -96,7 +97,7 @@ class _ServerInfoState extends State<ServerInfo> {
|
||||
fontSize: 25.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: MyTheme.accent),
|
||||
initialValue: _serverId,
|
||||
controller: _serverId,
|
||||
decoration: InputDecoration(
|
||||
icon: const Icon(Icons.perm_identity),
|
||||
labelText: '服务ID',
|
||||
@ -112,7 +113,7 @@ class _ServerInfoState extends State<ServerInfo> {
|
||||
fontSize: 25.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: MyTheme.accent),
|
||||
initialValue: _serverPasswd,
|
||||
controller: _serverPasswd,
|
||||
decoration: InputDecoration(
|
||||
icon: const Icon(Icons.lock),
|
||||
labelText: '密码',
|
||||
@ -131,6 +132,23 @@ class _ServerInfoState extends State<ServerInfo> {
|
||||
],
|
||||
));
|
||||
}
|
||||
fetchConfigAgain()async{
|
||||
FFI.setByName("start_service");
|
||||
var count = 0;
|
||||
const maxCount = 10;
|
||||
while(count<maxCount){
|
||||
if(_serverId.text!=_emptyIdShow && _serverPasswd.text!=""){
|
||||
break;
|
||||
}
|
||||
await Future.delayed(Duration(seconds: 2));
|
||||
var id = FFI.getByName("server_id");
|
||||
_serverId.text = id==""?_emptyIdShow:id;
|
||||
_serverPasswd.text = FFI.getByName("server_password");
|
||||
debugPrint("fetch id & passwd again at $count:id:${_serverId.text},passwd:${_serverPasswd.text}");
|
||||
count++;
|
||||
}
|
||||
FFI.setByName("stop_service");
|
||||
}
|
||||
}
|
||||
|
||||
class PermissionChecker extends StatefulWidget {
|
||||
@ -171,32 +189,46 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
||||
}
|
||||
}
|
||||
|
||||
void showLoginReqAlert(BuildContext context, String peerID, String name) {
|
||||
BuildContext loginReqAlertCtx;
|
||||
|
||||
void showLoginReqAlert(BuildContext context, String peerID, String name)async {
|
||||
debugPrint("got try_start_without_auth");
|
||||
showDialog(
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text("收到连接请求"),
|
||||
content: Text("是否同意来自$name:$peerID的控制?"),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text("接受"),
|
||||
onPressed: () {
|
||||
FFI.setByName("login_res", "true");
|
||||
if(!ServerPage.serverModel.isFileTransfer){
|
||||
_toAndroidStartCapture();
|
||||
}
|
||||
ServerPage.serverModel.setPeer(true);
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
TextButton(
|
||||
child: Text("不接受"),
|
||||
onPressed: () {
|
||||
FFI.setByName("login_res", "false");
|
||||
Navigator.of(context).pop();
|
||||
})
|
||||
],
|
||||
));
|
||||
builder: (alertContext) {
|
||||
loginReqAlertCtx = alertContext;
|
||||
return AlertDialog(
|
||||
title: Text("收到连接请求"),
|
||||
content: Text("是否同意来自$name:$peerID的控制?"),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text("接受"),
|
||||
onPressed: () {
|
||||
FFI.setByName("login_res", "true");
|
||||
if (!ServerPage.serverModel.isFileTransfer) {
|
||||
_toAndroidStartCapture();
|
||||
}
|
||||
ServerPage.serverModel.setPeer(true);
|
||||
Navigator.of(alertContext).pop();
|
||||
}),
|
||||
TextButton(
|
||||
child: Text("不接受"),
|
||||
onPressed: () {
|
||||
FFI.setByName("login_res", "false");
|
||||
Navigator.of(alertContext).pop();
|
||||
})
|
||||
],
|
||||
);
|
||||
});
|
||||
debugPrint("alert done");
|
||||
loginReqAlertCtx = null;
|
||||
}
|
||||
|
||||
clearLoginReqAlert(){
|
||||
if (loginReqAlertCtx!=null){
|
||||
Navigator.of(loginReqAlertCtx).pop();
|
||||
ServerPage.serverModel.updateClientState();
|
||||
}
|
||||
}
|
||||
|
||||
class PermissionRow extends StatelessWidget {
|
||||
@ -213,7 +245,7 @@ class PermissionRow extends StatelessWidget {
|
||||
children: [
|
||||
Text.rich(TextSpan(children: [
|
||||
TextSpan(
|
||||
text: name + ":",
|
||||
text: name + ": ",
|
||||
style: TextStyle(fontSize: 16.0, color: MyTheme.accent50)),
|
||||
TextSpan(
|
||||
text: isOk ? "已开启" : "未开启",
|
||||
@ -237,7 +269,7 @@ class ConnectionManager extends StatelessWidget {
|
||||
final serverModel = Provider.of<ServerModel>(context);
|
||||
var info =
|
||||
"${serverModel.peerName != "" ? serverModel.peerName : "NA"}-${serverModel.peerID != "" ? serverModel.peerID : "NA"}";
|
||||
return serverModel.isStart
|
||||
return serverModel.isPeerStart
|
||||
? myCard(Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -305,8 +337,11 @@ Future<Null> _toAndroidStartCapture() async {
|
||||
// }
|
||||
|
||||
Future<Null> _toAndroidStopService() async {
|
||||
FFI.setByName("stop_service");
|
||||
FFI.setByName("close_conn");
|
||||
ServerPage.serverModel.setPeer(false);
|
||||
|
||||
bool res = await toAndroidChannel.invokeMethod("stop_service");
|
||||
FFI.setByName("stop_service");
|
||||
debugPrint("_toAndroidStopSer:$res");
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user