mirror of
https://github.com/localsend/localsend.git
synced 2026-06-22 20:00:07 +00:00
feat(android): quick launch tile (#2676)
This commit is contained in:
@@ -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 |
Reference in New Issue
Block a user