new: custom foreground service on Android (#556)

This commit is contained in:
lollipopkit🏳️‍⚧️
2024-08-28 16:42:09 +08:00
committed by GitHub
parent 479250c207
commit 2d9dc044f9
6 changed files with 95 additions and 75 deletions

View File

@@ -53,7 +53,6 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "tech.lolli.toolbox" applicationId "tech.lolli.toolbox"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.

View File

@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
@@ -15,7 +16,8 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:allowBackup="true" android:allowBackup="true"
android:hasFragileUserData="true" android:hasFragileUserData="true"
android:restoreAnyVersion="true"> android:restoreAnyVersion="true"
tools:targetApi="q">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -43,11 +45,6 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<service
android:name="id.flutter.flutter_background_service.BackgroundService"
android:foregroundServiceType="dataSync"
/>
<receiver <receiver
android:name=".widget.HomeWidget" android:name=".widget.HomeWidget"
android:exported="false" android:exported="false"
@@ -68,8 +65,9 @@
</receiver> </receiver>
<service <service
android:name=".MyForegroundService" android:name=".ForegroundService"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="dataSync"
android:exported="false" /> android:exported="false" />
</application> </application>

View File

@@ -4,11 +4,9 @@ import android.app.*
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat
class MyForegroundService : Service() { class ForegroundService : Service() {
private val chanId = "ForegroundServiceChannel"
private val CHANNEL_ID = "ForegroundServiceChannel"
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@@ -31,8 +29,8 @@ class MyForegroundService : Service() {
private fun createNotificationChannel() { private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel( val serviceChannel = NotificationChannel(
CHANNEL_ID, chanId,
CHANNEL_ID, chanId,
NotificationManager.IMPORTANCE_DEFAULT NotificationManager.IMPORTANCE_DEFAULT
) )
val manager = getSystemService(NotificationManager::class.java) val manager = getSystemService(NotificationManager::class.java)
@@ -49,11 +47,20 @@ class MyForegroundService : Service() {
PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_IMMUTABLE
) )
return NotificationCompat.Builder(this, CHANNEL_ID) return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
.setContentTitle("App is running") Notification.Builder(this, chanId)
.setContentText("Click to open the app") .setContentTitle("App is running")
.setSmallIcon(R.drawable.ic_notification) .setContentText("Click to open the app")
.setContentIntent(pendingIntent) .setSmallIcon(R.mipmap.ic_launcher)
.build() .setContentIntent(pendingIntent)
.build()
} else {
Notification.Builder(this)
.setContentTitle("App is running")
.setContentText("Click to open the app")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.build()
}
} }
} }

View File

@@ -1,6 +1,11 @@
package tech.lolli.toolbox package tech.lolli.toolbox
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.Manifest
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@@ -18,7 +23,8 @@ class MainActivity: FlutterFragmentActivity() {
result.success(null) result.success(null)
} }
"startService" -> { "startService" -> {
val serviceIntent = Intent(this, ForegroundService::class.java) reqPerm()
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent) startForegroundService(serviceIntent)
} else { } else {
@@ -32,4 +38,16 @@ class MainActivity: FlutterFragmentActivity() {
} }
} }
} }
private fun reqPerm() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
123,
)
}
}
} }

View File

@@ -9,6 +9,7 @@ import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:server_box/app.dart'; import 'package:server_box/app.dart';
import 'package:server_box/core/channel/bg_run.dart';
import 'package:server_box/core/utils/sync/icloud.dart'; import 'package:server_box/core/utils/sync/icloud.dart';
import 'package:server_box/core/utils/sync/webdav.dart'; import 'package:server_box/core/utils/sync/webdav.dart';
import 'package:server_box/data/model/app/menu/server_func.dart'; import 'package:server_box/data/model/app/menu/server_func.dart';
@@ -109,6 +110,10 @@ void _doPlatformRelated() async {
if (isAndroid) { if (isAndroid) {
// try switch to highest refresh rate // try switch to highest refresh rate
FlutterDisplayMode.setHighRefreshRate(); FlutterDisplayMode.setHighRefreshRate();
if (Stores.setting.bgRun.fetch()) {
Loggers.app.info('Start foreground service');
BgRunMC.startService();
}
} }
final serversCount = Stores.server.box.keys.length; final serversCount = Stores.server.box.keys.length;

View File

@@ -259,11 +259,11 @@ class _AddPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const viewPadding = 3.0; const viewPadding = 7.0;
final viewWidth = context.media.size.width - 2 * viewPadding; final viewWidth = context.media.size.width - 2 * viewPadding;
final itemCount = ServerProvider.servers.length; final itemCount = ServerProvider.servers.length;
const itemPadding = 3.0; const itemPadding = 1.0;
const itemWidth = 150.0; const itemWidth = 150.0;
const itemHeight = 50.0; const itemHeight = 50.0;
@@ -272,62 +272,55 @@ class _AddPage extends StatelessWidget {
max(viewWidth ~/ (visualCrossCount * itemPadding + itemWidth), 1); max(viewWidth ~/ (visualCrossCount * itemPadding + itemWidth), 1);
final mainCount = itemCount ~/ crossCount + 1; final mainCount = itemCount ~/ crossCount + 1;
return Center( return ServerProvider.serverOrder.listenVal((order) {
key: const Key('sshTabAddServer'), if (order.isEmpty) {
child: ServerProvider.serverOrder.listenVal((order) { return Center(
if (order.isEmpty) { child: Text(libL10n.empty, textAlign: TextAlign.center),
return Center( );
child: Text(libL10n.empty, textAlign: TextAlign.center), }
);
}
// Custom grid // Custom grid
return ListView( return ListView(
padding: const EdgeInsets.all(viewPadding), padding: const EdgeInsets.all(viewPadding),
children: List.generate( children: List.generate(
mainCount, mainCount,
(rowIndex) => Row( (rowIndex) => Row(
children: List.generate(crossCount, (columnIndex) { children: List.generate(crossCount, (columnIndex) {
final idx = rowIndex * crossCount + columnIndex; final idx = rowIndex * crossCount + columnIndex;
final id = order.elementAtOrNull(idx); final id = order.elementAtOrNull(idx);
if (id == null) return _placeholder; final spi = ServerProvider.pick(id: id)?.value.spi;
if (spi == null) return _placeholder;
final spi = ServerProvider.pick(id: order[idx])?.value.spi; return Expanded(
if (spi == null) return _placeholder; child: Padding(
padding: const EdgeInsets.all(itemPadding),
return Expanded( child: InkWell(
child: Padding( onTap: () => onTapInitCard(spi),
padding: const EdgeInsets.all(itemPadding), child: Container(
child: CardX( height: itemHeight,
child: InkWell( alignment: Alignment.centerLeft,
onTap: () => onTapInitCard(spi), padding: const EdgeInsets.only(left: 17, right: 7),
child: Container( child: Row(
height: itemHeight, children: [
alignment: Alignment.centerLeft, Expanded(
padding: const EdgeInsets.only(left: 17, right: 7), child: Text(
child: Row( spi.name,
children: [ style: UIs.text18,
Expanded( maxLines: 1,
child: Text( overflow: TextOverflow.ellipsis,
spi.name, ),
style: UIs.text18,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.chevron_right)
],
), ),
), const Icon(Icons.chevron_right)
],
), ),
), ),
), ).cardx,
); ),
}), );
), }),
), ),
); ),
}), );
); });
} }
} }