From f083816fc79b3f38967949a895473065606c33eb Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 4 Apr 2022 14:54:00 +0800 Subject: [PATCH] android late request permission;update chat UI,launch chat from UI cm --- .../com/carriez/flutter_hbb/MainActivity.kt | 20 ++- .../com/carriez/flutter_hbb/MainService.kt | 2 +- .../kotlin/com/carriez/flutter_hbb/common.kt | 46 +++++-- lib/common.dart | 1 + lib/models/chat_model.dart | 11 ++ lib/models/server_model.dart | 114 ++++++++++++++++-- lib/pages/chat_page.dart | 44 +++++-- lib/pages/home_page.dart | 1 + lib/pages/server_page.dart | 43 +++++-- 9 files changed, 236 insertions(+), 46 deletions(-) diff --git a/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index 209acddd7..bbf41580a 100644 --- a/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -15,7 +15,6 @@ import androidx.annotation.RequiresApi import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel -import java.lang.Exception const val MEDIA_REQUEST_CODE = 42 @@ -36,7 +35,6 @@ class MainActivity : FlutterActivity() { Intent(this, MainService::class.java).also { bindService(it, serviceConnection, Context.BIND_AUTO_CREATE) } - checkPermissions(this) updateMachineInfo() flutterMethodChannel = MethodChannel( flutterEngine.dartExecutor.binaryMessenger, @@ -67,6 +65,16 @@ class MainActivity : FlutterActivity() { result.success(false) } } + "check_permission" -> { + if(call.arguments is String){ + result.success(checkPermission(context, call.arguments as String)) + } + } + "request_permission" -> { + if(call.arguments is String){ + requestPermission(context, call.arguments as String) + } + } "check_video_permission" -> { mainService?.let { result.success(it.checkMediaPermission()) @@ -76,11 +84,11 @@ class MainActivity : FlutterActivity() { } "check_service" -> { flutterMethodChannel.invokeMethod( - "on_permission_changed", + "on_state_changed", mapOf("name" to "input", "value" to InputService.isOpen.toString()) ) flutterMethodChannel.invokeMethod( - "on_permission_changed", + "on_state_changed", mapOf("name" to "media", "value" to mainService?.isReady.toString()) ) } @@ -94,7 +102,7 @@ class MainActivity : FlutterActivity() { } InputService.ctx = null flutterMethodChannel.invokeMethod( - "on_permission_changed", + "on_state_changed", mapOf("name" to "input", "value" to InputService.isOpen.toString()) ) } @@ -154,7 +162,7 @@ class MainActivity : FlutterActivity() { Log.d(logTag, "onResume inputPer:$inputPer") activity.runOnUiThread { flutterMethodChannel.invokeMethod( - "on_permission_changed", + "on_state_changed", mapOf("name" to "input", "value" to inputPer.toString()) ) } diff --git a/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index 861123d1d..83f760cfc 100644 --- a/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -340,7 +340,7 @@ class MainService : Service() { fun checkMediaPermission(): Boolean { Handler(Looper.getMainLooper()).post { MainActivity.flutterMethodChannel.invokeMethod( - "on_permission_changed", + "on_state_changed", mapOf("name" to "media", "value" to isReady.toString()) ) } diff --git a/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt b/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt index 1b1c5944f..b5bd259ec 100644 --- a/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt +++ b/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt @@ -1,17 +1,14 @@ package com.carriez.flutter_hbb import android.annotation.SuppressLint -import android.app.* -import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.content.Context -import android.content.Intent -import android.graphics.Color import android.media.MediaCodecList import android.media.MediaFormat import android.os.Build +import android.os.Handler +import android.os.Looper import android.util.Log import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat import com.hjq.permissions.Permission import com.hjq.permissions.XXPermissions import java.util.* @@ -40,13 +37,44 @@ fun testVP9Support(): Boolean { return res != null } -fun checkPermissions(context: Context) { +fun requestPermission(context: Context,type: String){ + val permission = when (type) { + "audio" -> { + Permission.RECORD_AUDIO + } + "file" -> { + Permission.MANAGE_EXTERNAL_STORAGE + } + else -> { + return + } + } XXPermissions.with(context) - .permission(Permission.RECORD_AUDIO) - .permission(Permission.MANAGE_EXTERNAL_STORAGE) + .permission(permission) .request { permissions, all -> if (all) { - Log.d("loglog", "获取存储权限成功:$permissions") + Log.d("checkPermissions", "获取存储权限成功:$permissions") + Handler(Looper.getMainLooper()).post { + MainActivity.flutterMethodChannel.invokeMethod( + "on_android_permission_result", + mapOf("type" to type, "result" to all) + ) + } } } } + +fun checkPermission(context: Context,type: String): Boolean { + val permission = when (type) { + "audio" -> { + Permission.RECORD_AUDIO + } + "file" -> { + Permission.MANAGE_EXTERNAL_STORAGE + } + else -> { + return false + } + } + return XXPermissions.isGranted(context,permission) +} diff --git a/lib/common.dart b/lib/common.dart index 9b94a9cf6..033519164 100644 --- a/lib/common.dart +++ b/lib/common.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter_easyloading/flutter_easyloading.dart'; final globalKey = GlobalKey(); +final navigationBarKey = GlobalKey(); var isAndroid = false; var isIOS = false; diff --git a/lib/models/chat_model.dart b/lib/models/chat_model.dart index 0c3aaa798..e9c34f507 100644 --- a/lib/models/chat_model.dart +++ b/lib/models/chat_model.dart @@ -29,10 +29,21 @@ class ChatModel with ChangeNotifier { int get currentID => _currentID; + ChatUser get currentUser => + FFI.serverModel.clients[_currentID]?.chatUser ?? me; + + changeCurrentID(int id){ if(_messages.containsKey(id)){ _currentID = id; notifyListeners(); + } else { + final chatUser = FFI.serverModel.clients[id]?.chatUser; + if(chatUser == null){ + return debugPrint("Failed to changeCurrentID,remote user doesn't exist"); + } + _messages[id] = []; + _currentID = id; } } diff --git a/lib/models/server_model.dart b/lib/models/server_model.dart index 23367e0bd..c3a501d36 100644 --- a/lib/models/server_model.dart +++ b/lib/models/server_model.dart @@ -38,12 +38,40 @@ class ServerModel with ChangeNotifier { ServerModel() { ()async{ - await Future.delayed(Duration(seconds: 2)); - final audioOption = FFI.getByName('option', 'enable-audio'); - _audioOk = audioOption.isEmpty; // audio true by default + /** + * 1. check android permission + * 2. check config + * audio true by default (if permission on) + * file true by default (if permission on) + * input false by default (it need turning on manually everytime) + */ + await Future.delayed(Duration(seconds: 1)); - final fileOption = FFI.getByName('option', 'enable-file-transfer'); - _fileOk = fileOption.isEmpty; + // audio + if(!await PermissionManager.check("audio")){ + _audioOk = false; + FFI.setByName('option', jsonEncode( + Map() + ..["name"] = "enable-audio" + ..["value"] = "N")); + }else{ + final audioOption = FFI.getByName('option', 'enable-audio'); + _audioOk = audioOption.isEmpty; + } + + // file + if(!await PermissionManager.check("file")) { + _fileOk = false; + FFI.setByName('option', jsonEncode( + Map() + ..["name"] = "enable-file-transfer" + ..["value"] = "N")); + } else{ + final fileOption = FFI.getByName('option', 'enable-file-transfer'); + _fileOk = fileOption.isEmpty; + } + + // input (mouse control) Map res = Map() ..["name"] = "enable-keyboard" ..["value"] = 'N'; @@ -52,7 +80,18 @@ class ServerModel with ChangeNotifier { }(); } - toggleAudio(){ + toggleAudio() async { + if(!_audioOk && !await PermissionManager.check("audio")){ + debugPrint("toggleAudio 无权限 开始获取权限"); + final res = await PermissionManager.request("audio"); + debugPrint("权限请求结果:$res"); + + if(!res){ + // TODO handle fail + return; + } + } + _audioOk = !_audioOk; Map res = Map() ..["name"] = "enable-audio" @@ -61,7 +100,15 @@ class ServerModel with ChangeNotifier { notifyListeners(); } - toggleFile() { + toggleFile() async { + if(!_fileOk && !await PermissionManager.check("file")){ + final res = await PermissionManager.request("file"); + if(!res){ + // TODO handle fail + return; + } + } + _fileOk = !_fileOk; Map res = Map() ..["name"] = "enable-file-transfer" @@ -128,7 +175,7 @@ class ServerModel with ChangeNotifier { getIDPasswd(); } - Future stopService() async { + Future stopService() async { _isStart = false; _interval?.cancel(); _interval = null; @@ -408,3 +455,54 @@ showInputWarnAlert() async { }); DialogManager.drop(); } + +class PermissionManager { + static Completer? _completer; + static Timer? _timer; + static var _current = ""; + + static final permissions = ["audio","file"]; + + static bool isWaitingFile(){ + if(_completer != null){ + return !_completer!.isCompleted && _current == "file"; + } + return false; + } + + static Future check(String type){ + if(!permissions.contains(type)) + return Future.error("Wrong permission!$type"); + return FFI.invokeMethod("check_permission",type); + } + + static Future request(String type){ + if(!permissions.contains(type)) + return Future.error("Wrong permission!$type"); + + _current = type; + _completer = Completer(); + FFI.invokeMethod("request_permission",type); + + // timeout + _timer?.cancel(); + _timer = Timer(Duration(seconds: 60),(){ + if(_completer == null) return; + if(!_completer!.isCompleted){ + _completer!.complete(false); + } + _completer = null; + _current = ""; + }); + return _completer!.future; + } + + static complete(String type,bool res){ + if(type != _current){ + res = false; + } + _timer?.cancel(); + _completer?.complete(res); + _current = ""; + } +} diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index ee72d4908..ed08ce399 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -30,7 +30,7 @@ class ChatPage extends StatelessWidget implements PageShape { final id = entry.key; final user = serverModel.clients[id]?.chatUser ?? chatModel.me; return PopupMenuItem( - child: Text("${user.name} - ${user.uid}"), + child: Text("${user.name} ${user.uid}"), value: id, ); }).toList(); @@ -47,18 +47,36 @@ class ChatPage extends StatelessWidget implements PageShape { child: Container( color: MyTheme.grayBg, child: Consumer(builder: (context, chatModel, child) { - return DashChat( - inputContainerStyle: BoxDecoration(color: Colors.white70), - sendOnEnter: false, - // if true,reload keyboard everytime,need fix - onSend: (chatMsg) { - chatModel.send(chatMsg); - }, - user: chatModel.me, - messages: chatModel.messages[chatModel.currentID] ?? [], - // default scrollToBottom has bug https://github.com/fayeed/dash_chat/issues/53 - scrollToBottom: false, - scrollController: chatModel.scroller, + final currentUser = chatModel.currentUser; + return Stack( + children: [ + DashChat( + inputContainerStyle: BoxDecoration(color: Colors.white70), + sendOnEnter: false, + // if true,reload keyboard everytime,need fix + onSend: (chatMsg) { + chatModel.send(chatMsg); + }, + user: chatModel.me, + messages: chatModel.messages[chatModel.currentID] ?? [], + // default scrollToBottom has bug https://github.com/fayeed/dash_chat/issues/53 + scrollToBottom: false, + scrollController: chatModel.scroller, + ), + chatModel.currentID == ChatModel.clientModeID + ? SizedBox.shrink() + : Padding( + padding: EdgeInsets.all(10), + child: Row( + children: [ + Icon(Icons.account_circle, + color: MyTheme.accent80), + SizedBox(width: 5), + Text( + "${currentUser.name ?? ""} ${currentUser.uid ?? ""}",style: TextStyle(color: MyTheme.accent50),), + ], + )), + ], ); }))); } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index f84f0775e..863377920 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -56,6 +56,7 @@ class _HomePageState extends State { actions: _pages.elementAt(_selectedIndex).appBarActions, ), bottomNavigationBar: BottomNavigationBar( + key: navigationBarKey, items: _pages .map((page) => BottomNavigationBarItem(icon: page.icon, label: page.title)) diff --git a/lib/pages/server_page.dart b/lib/pages/server_page.dart index 47fe6f360..5b40b2046 100644 --- a/lib/pages/server_page.dart +++ b/lib/pages/server_page.dart @@ -76,9 +76,13 @@ class ServerPage extends StatelessWidget implements PageShape { } } -void checkService() { - // 检测当前服务状态,若已存在服务则异步更新数据回来 +void checkService() async { FFI.invokeMethod("check_service"); // jvm + // for Android 10/11,MANAGE_EXTERNAL_STORAGE permission from a system setting page + if(PermissionManager.isWaitingFile() && !FFI.serverModel.fileOk){ + PermissionManager.complete("file",await PermissionManager.check("file")); + debugPrint("file permission finished"); + } } class ServerInfo extends StatefulWidget { @@ -268,9 +272,21 @@ class ConnectionManager extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: EdgeInsets.symmetric(vertical: 5.0), - child: clientInfo(entry.value), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + clientInfo(entry.value), + entry.value.isFileTransfer + ?SizedBox.shrink() + :IconButton(onPressed: (){ + FFI.chatModel.changeCurrentID(entry.value.id); + final bar = navigationBarKey.currentWidget; + if(bar!=null){ + bar as BottomNavigationBar; + bar.onTap!(1); + } + }, icon: Icon(Icons.chat,color: MyTheme.accent80,)) + ], ), ElevatedButton.icon( style: ButtonStyle( @@ -338,7 +354,9 @@ class PaddingCard extends StatelessWidget { } Widget clientInfo(Client client) { - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + return Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( @@ -356,7 +374,7 @@ Widget clientInfo(Client client) { ]) ], ), - ]); + ])); } void toAndroidChannelInit() { @@ -370,14 +388,21 @@ void toAndroidChannelInit() { FFI.serverModel.updateClientState(); break; } - case "on_permission_changed": + case "on_state_changed": { var name = arguments["name"] as String; var value = arguments["value"] as String == "true"; - debugPrint("from jvm:on_permission_changed,$name:$value"); + debugPrint("from jvm:on_state_changed,$name:$value"); FFI.serverModel.changeStatue(name, value); break; } + case "on_android_permission_result": + { + var type = arguments["type"] as String; + var result = arguments["result"] as bool; + PermissionManager.complete(type, result); + break; + } } } catch (e) { debugPrint("MethodCallHandler err:$e");