feat(android): quick launch tile (#2676)

This commit is contained in:
Voltra
2025-10-27 02:54:06 +01:00
committed by GitHub
parent ea25d1fceb
commit 98cc7488e6
9 changed files with 133 additions and 3 deletions
+18 -2
View File
@@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.localsend.localsend_app">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="org.localsend.localsend_app">
<uses-permission android:name="android.permission.INTERNET"/>
@@ -14,6 +13,8 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.BIND_QUICK_SETTINGS_TILE" tools:targetApi="24" />
<!-- Android TV -->
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
@@ -65,6 +66,21 @@
</intent-filter>
</activity>
<!-- Quick tile to launch the app -->
<service
android:name=".QuickTileService"
android:icon="@mipmap/ic_launcher_quicktile_foreground"
android:label="LocalSend"
android:process=":quick_tile_service"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:exported="true"
tools:targetApi="24">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE"/>
</intent-filter>
</service>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
@@ -2,11 +2,12 @@ package org.localsend.localsend_app
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.provider.DocumentsContract
import android.provider.Settings;
import android.provider.Settings
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
@@ -21,6 +22,18 @@ private const val REQUEST_CODE_PICK_FILE = 3
class MainActivity : FlutterActivity() {
private var pendingResult: MethodChannel.Result? = null
// Overriding the static methods we need from the Java class, as described
// in the documentation of `FlutterActivity.NewEngineIntentBuilder`
companion object {
fun withNewEngine(): NewEngineIntentBuilder {
return NewEngineIntentBuilder(MainActivity::class.java)
}
fun createDefaultIntent(launchContext: Context): Intent {
return withNewEngine().build(launchContext)
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
@@ -0,0 +1,101 @@
package org.localsend.localsend_app
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.PendingIntent
import android.content.Intent
import android.graphics.drawable.Icon
import android.os.Build
import android.service.quicksettings.TileService
import android.util.Log
import androidx.annotation.RequiresApi
/**
* Service used to launch the app as a quick tile from the top/status bar
* @see https://dev.to/djsmk123/fluttercreate-custom-quick-title-android-only-3ehp
* @see https://github.com/ProtonVPN/android-app/blob/2290b3c6b8b5ded339d69ec7c12e15acbb4b4b3d/app/src/main/java/com/protonvpn/android/components/QuickTileService.kt#L171
*/
@RequiresApi(Build.VERSION_CODES.N)
class QuickTileService : TileService() {
override fun onClick() {
super.onClick()
launchApp()
}
override fun onStartListening() {
super.onStartListening()
setupIcon()
}
private fun setupIcon() {
// The tile is only available between `onStartListening` and
// `onStopListening`, so we ensure the tile is available
if (qsTile == null) {
return
}
qsTile.icon =
Icon.createWithResource(this, R.mipmap.ic_launcher_quicktile_foreground)
qsTile.label = packageManager.getApplicationLabel(application.applicationInfo)
qsTile.updateTile()
}
@SuppressLint("StartActivityAndCollapseDeprecated")
private fun launchApp() {
try{
val launchIntent = getLaunchIntent()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// Starting from `Build.VERSION_CODES.UPSIDE_DOWN_CAKE` we can
// no longer start and collapse an Intent. We need to use a
// PendingIntent instead.
//
// The request code can be used to identify the pending intent
// request if needed. We don't, hence the 0.
//
// The launch intent used for the tile doesn't need any data
// thus we mark it as immutable to ensure maximal reuse.
startActivityAndCollapse(
PendingIntent.getActivity(this, 0, launchIntent,
PendingIntent.FLAG_IMMUTABLE)
)
} else {
// For any version below `Build.VERSION_CODES.UPSIDE_DOWN_CAKE`
// we can simply start the intent directly.
startActivityAndCollapse(launchIntent)
}
}
catch (e:Exception){
Log.w(this.javaClass.toString(),"Exception $e")
}
}
private fun getLaunchIntent(): Intent {
// Getting the launch intent from the package manager is the optimal
// way to get the proper intent to launch the app.
val cleanIntent = packageManager.getLaunchIntentForPackage(packageName)
return if (cleanIntent != null) {
cleanIntent
} else {
// If we can't get the launch intent from the PM, then we default
// back to creating one by instantiating the app intent ourself.
val dirtyIntent = MainActivity.createDefaultIntent(this)
dirtyIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
dirtyIntent
}
}
private fun appIsAlreadyRunning(): Boolean {
val info = ActivityManager.RunningAppProcessInfo()
ActivityManager.getMyMemoryState(info)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
info.importance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED
} else {
info.importance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_BACKGROUND
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB