diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 02c18dd4d..40d2592d7 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -17,8 +17,8 @@ jobs:
# Available versions may lag behind https://github.com/flutter/flutter.git
- uses: subosito/flutter-action@v2
with:
- flutter-version: '3.0.2'
- channel: 'stable'
+ flutter-version: '3.3.0-0.0.pre'
+ channel: 'beta'
- name: Clone the repository.
uses: actions/checkout@v2
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 273a45cba..890051962 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,8 +19,8 @@ jobs:
# Available versions may lag behind https://github.com/flutter/flutter.git
- uses: subosito/flutter-action@v2
with:
- flutter-version: '3.0.2'
- channel: 'stable'
+ flutter-version: '3.3.0-0.0.pre'
+ channel: 'beta'
# Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1):
# https://issuetracker.google.com/issues/144111441
@@ -56,15 +56,15 @@ jobs:
rm release.keystore.asc
mkdir outputs
(cd scripts/; ./apply_flavor_play.sh)
- flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.0.2.sksl.json
+ flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.3.0-0.0.pre.sksl.json
cp build/app/outputs/bundle/playRelease/*.aab outputs
- flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.0.2.sksl.json
+ flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.3.0-0.0.pre.sksl.json
cp build/app/outputs/apk/play/release/*.apk outputs
(cd scripts/; ./apply_flavor_huawei.sh)
- flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_3.0.2.sksl.json
+ flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_3.3.0-0.0.pre.sksl.json
cp build/app/outputs/apk/huawei/release/*.apk outputs
(cd scripts/; ./apply_flavor_izzy.sh)
- flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_3.0.2.sksl.json
+ flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_3.3.0-0.0.pre.sksl.json
cp build/app/outputs/apk/izzy/release/*.apk outputs
rm $AVES_STORE_FILE
env:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac8faa429..7a1ce5e9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+## [v1.6.10] - 2022-07-24
+
+### Added
+
+- Search: `on this day` and month filters in date filter section
+- Stats: histogram and contextual date filters
+- Screen saver
+- Widget: photo frame
+
+### Changed
+
+- viewer: black background when overlay is disabled with light theme
+- filter chip long press menu shows full label
+- upgraded Flutter to beta v3.3.0-0.0.pre
+
+### Fixed
+
+- analysis service stuck when storage has ambiguous directories
+
## [v1.6.9] - 2022-06-18
### Added
diff --git a/README.md b/README.md
index 9af7d5a6a..4a6d48055 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@ It scans your media collection to identify **motion photos**, **panoramas** (aka
**Navigation and search** is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc.
-Aves integrates with Android (from **API 19 to 32**, i.e. from KitKat to Android 12L) with features such as **app shortcuts** and **global search** handling. It also works as a **media viewer and picker**.
+Aves integrates with Android (from **API 19 to 33**, i.e. from KitKat to Android 13) with features such as **widgets**, **app shortcuts**, **screen saver** and **global search** handling. It also works as a **media viewer and picker**.
## Screenshots
@@ -101,7 +101,7 @@ Some users have expressed the wish to financially support the project. Thanks!
[
](https://paypal.me/ThibaultDeckers)
+ height="40">](https://paypal.me/ThibaultDeckersFr)
[
](https://liberapay.com/deckerst/donate)
diff --git a/android/app/agconnect-services.json b/android/app/agconnect-services.json
index 876ecb775..d406d5ce2 100644
--- a/android/app/agconnect-services.json
+++ b/android/app/agconnect-services.json
@@ -5,18 +5,25 @@
"DE":"connect-dre.dbankcloud.cn",
"DE_back":"connect-dre.hispace.hicloud.com",
"RU":"connect-drru.hispace.dbankcloud.ru",
- "RU_back":"connect-drru.hispace.dbankcloud.ru",
+ "RU_back":"connect-drru.hispace.dbankcloud.cn",
"SG":"connect-dra.dbankcloud.cn",
"SG_back":"connect-dra.hispace.hicloud.com"
},
+ "websocketgw_all":{
+ "CN":"connect-ws-drcn.hispace.dbankcloud.cn",
+ "CN_back":"connect-ws-drcn.hispace.dbankcloud.com",
+ "DE":"connect-ws-dre.hispace.dbankcloud.cn",
+ "DE_back":"connect-ws-dre.hispace.dbankcloud.com",
+ "RU":"connect-ws-drru.hispace.dbankcloud.ru",
+ "RU_back":"connect-ws-drru.hispace.dbankcloud.cn",
+ "SG":"connect-ws-dra.hispace.dbankcloud.cn",
+ "SG_back":"connect-ws-dra.hispace.dbankcloud.com"
+ },
"client":{
"cp_id":"2640082000020010713",
"product_id":"99536292102197525",
- "client_id":"874325707927340288",
- "client_secret":"DCAFAE5C0440ABDBD6DDB2B6EBD7D9B0870C10FCA64759CCD63020D168803AB5",
"project_id":"99536292102197525",
"app_id":"106014023",
- "api_key":"DAEDAEzScQA5ri36P2NEiVPSFrOJeYZ0DbEJZMGJrBadW+QudBr5BGHD3vO0tsL1VeBy0RPZefPic3hAWUijcBxCv0zRv0iBjQEptQ==",
"package_name":"deckers.thibault.aves"
},
"oauth_client":{
@@ -30,17 +37,17 @@
"configuration_version":"3.0",
"appInfos":[
{
- "package_name":"deckers.thibault.aves.profile",
+ "package_name":"deckers.thibault.aves",
"client":{
- "app_id":"106031461"
+ "app_id":"106014023"
},
"app_info":{
- "package_name":"deckers.thibault.aves.profile",
- "app_id":"106031461"
+ "package_name":"deckers.thibault.aves",
+ "app_id":"106014023"
},
"oauth_client":{
"client_type":1,
- "client_id":"106031461"
+ "client_id":"106014023"
}
},
{
@@ -58,17 +65,17 @@
}
},
{
- "package_name":"deckers.thibault.aves",
+ "package_name":"deckers.thibault.aves.profile",
"client":{
- "app_id":"106014023"
+ "app_id":"106031461"
},
"app_info":{
- "package_name":"deckers.thibault.aves",
- "app_id":"106014023"
+ "package_name":"deckers.thibault.aves.profile",
+ "app_id":"106031461"
},
"oauth_client":{
"client_type":1,
- "client_id":"106014023"
+ "client_id":"106031461"
}
}
]
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 78f84d8c2..aebce2c0e 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -38,6 +38,7 @@ if (keystorePropertiesFile.exists()) {
keystoreProperties['keyAlias'] = System.getenv('AVES_KEY_ALIAS')
keystoreProperties['keyPassword'] = System.getenv('AVES_KEY_PASSWORD')
keystoreProperties['googleApiKey'] = System.getenv('AVES_GOOGLE_API_KEY')
+ keystoreProperties['huaweiApiKey'] = System.getenv('AVES_HUAWEI_API_KEY')
}
android {
@@ -47,7 +48,6 @@ android {
main.java.srcDirs += 'src/main/kotlin'
}
-
defaultConfig {
applicationId appId
// minSdkVersion constraints:
@@ -60,7 +60,8 @@ android {
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
- manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey']]
+ manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey'],
+ huaweiApiKey: keystoreProperties['huaweiApiKey']]
multiDexEnabled true
resValue 'string', 'search_provider', "${appId}.search_provider"
}
@@ -140,7 +141,6 @@ android {
lint {
disable 'InvalidPackage'
}
- namespace 'deckers.thibault.aves'
}
flutter {
@@ -169,7 +169,7 @@ dependencies {
// huawei flavor only
huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.5.2.300'
- kapt 'androidx.annotation:annotation:1.3.0'
+ kapt 'androidx.annotation:annotation:1.4.0'
kapt 'com.github.bumptech.glide:compiler:4.13.0'
compileOnly rootProject.findProject(':streams_channel')
diff --git a/android/app/src/debug/res/xml/screen_saver.xml b/android/app/src/debug/res/xml/screen_saver.xml
new file mode 100644
index 000000000..7b51a7bea
--- /dev/null
+++ b/android/app/src/debug/res/xml/screen_saver.xml
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 68c90244b..8e1be5b28 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,5 +1,14 @@
+
+
+
+
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt
index aa84e0c63..3d98d201d 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt
@@ -11,10 +11,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.MainActivity.Companion.OPEN_FROM_ANALYSIS_SERVICE
-import deckers.thibault.aves.channel.calls.DeviceHandler
-import deckers.thibault.aves.channel.calls.GeocodingHandler
-import deckers.thibault.aves.channel.calls.MediaStoreHandler
-import deckers.thibault.aves.channel.calls.MetadataFetchHandler
+import deckers.thibault.aves.channel.calls.*
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
import deckers.thibault.aves.utils.FlutterUtils
@@ -25,7 +22,7 @@ import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.runBlocking
class AnalysisService : MethodChannel.MethodCallHandler, Service() {
- private var backgroundFlutterEngine: FlutterEngine? = null
+ private var flutterEngine: FlutterEngine? = null
private var backgroundChannel: MethodChannel? = null
private var serviceLooper: Looper? = null
private var serviceHandler: ServiceHandler? = null
@@ -37,18 +34,27 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
runBlocking {
FlutterUtils.initFlutterEngine(context, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) {
- backgroundFlutterEngine = it
+ flutterEngine = it
}
}
- val messenger = backgroundFlutterEngine!!.dartExecutor.binaryMessenger
+ val messenger = flutterEngine!!.dartExecutor
+
// channels for analysis
+
+ // dart -> platform -> dart
+ // - need Context
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
+ MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
+
+ // result streaming: dart -> platform ->->-> dart
+ // - need Context
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) }
+
// channel for service management
backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply {
setMethodCallHandler(context)
@@ -67,7 +73,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
override fun onBind(intent: Intent) = analysisServiceBinder
- override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val channel = NotificationChannelCompat.Builder(CHANNEL_ANALYSIS, NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getText(R.string.analysis_channel_name))
.setShowBadge(false)
@@ -76,7 +82,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
startForeground(NOTIFICATION_ID, buildNotification())
val msgData = Bundle()
- intent.extras?.let {
+ intent?.extras?.let {
msgData.putAll(it)
}
serviceHandler?.obtainMessage()?.let { msg ->
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetConfigureActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetConfigureActivity.kt
new file mode 100644
index 000000000..70c55dbaf
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetConfigureActivity.kt
@@ -0,0 +1,61 @@
+package deckers.thibault.aves
+
+import android.appwidget.AppWidgetManager
+import android.content.Intent
+import android.os.Bundle
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+
+class HomeWidgetSettingsActivity : MainActivity() {
+ private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // cancel if user does not complete widget setup
+ setResult(RESULT_CANCELED)
+
+ intent.extras?.let {
+ appWidgetId = it.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
+ intentDataMap = extractIntentData(intent)
+ }
+
+ if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
+ finish()
+ return
+ }
+
+ val messenger = flutterEngine!!.dartExecutor
+ MethodChannel(messenger, CHANNEL).setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
+ when (call.method) {
+ "configure" -> {
+ result.success(null)
+ saveWidget()
+ }
+ else -> result.notImplemented()
+ }
+ }
+ }
+
+ private fun saveWidget() {
+ val appWidgetManager = AppWidgetManager.getInstance(context)
+ val widgetInfo = appWidgetManager.getAppWidgetOptions(appWidgetId)
+ HomeWidgetProvider().onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, widgetInfo)
+
+ val intent = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ setResult(RESULT_OK, intent)
+ finish()
+ }
+
+ override fun extractIntentData(intent: Intent?): MutableMap {
+ return hashMapOf(
+ INTENT_DATA_KEY_ACTION to INTENT_ACTION_WIDGET_SETTINGS,
+ INTENT_DATA_KEY_WIDGET_ID to appWidgetId,
+ )
+ }
+
+ companion object {
+ private const val CHANNEL = "deckers.thibault/aves/widget_configure"
+ }
+}
+
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt
new file mode 100644
index 000000000..eab4536ff
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt
@@ -0,0 +1,202 @@
+package deckers.thibault.aves
+
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.widget.RemoteViews
+import app.loup.streams_channel.StreamsChannel
+import deckers.thibault.aves.channel.calls.DeviceHandler
+import deckers.thibault.aves.channel.calls.MediaFetchHandler
+import deckers.thibault.aves.channel.calls.MediaStoreHandler
+import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
+import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
+import deckers.thibault.aves.utils.FlutterUtils
+import deckers.thibault.aves.utils.LogUtils
+import io.flutter.FlutterInjector
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.embedding.engine.dart.DartExecutor
+import io.flutter.plugin.common.MethodChannel
+import kotlinx.coroutines.*
+import java.nio.ByteBuffer
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+import kotlin.math.roundToInt
+
+class HomeWidgetProvider : AppWidgetProvider() {
+ private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+
+ override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
+ Log.d(LOG_TAG, "Widget onUpdate widgetIds=${appWidgetIds.contentToString()}")
+ for (widgetId in appWidgetIds) {
+ val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId)
+
+ defaultScope.launch {
+ val backgroundBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = false)
+ updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, backgroundBytes)
+
+ val imageBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false)
+ updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageBytes)
+ }
+ }
+ }
+
+ override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager?, widgetId: Int, widgetInfo: Bundle?) {
+ Log.d(LOG_TAG, "Widget onAppWidgetOptionsChanged widgetId=$widgetId")
+ appWidgetManager ?: return
+ widgetInfo ?: return
+
+ if (imageByteFetchJob != null) {
+ imageByteFetchJob?.cancel()
+ }
+ imageByteFetchJob = defaultScope.launch {
+ delay(500)
+ val imageBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = true)
+ updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageBytes)
+ }
+ }
+
+ private suspend fun getBytes(
+ context: Context,
+ widgetId: Int,
+ widgetInfo: Bundle,
+ drawEntryImage: Boolean,
+ reuseEntry: Boolean = false,
+ ): ByteArray? {
+ val devicePixelRatio = context.resources.displayMetrics.density
+ val widthPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) * devicePixelRatio).roundToInt()
+ val heightPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) * devicePixelRatio).roundToInt()
+ if (widthPx == 0 || heightPx == 0) return null
+
+ initFlutterEngine(context)
+ val messenger = flutterEngine!!.dartExecutor
+ val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL)
+ try {
+ val bytes = suspendCoroutine { cont ->
+ defaultScope.launch {
+ FlutterUtils.runOnUiThread {
+ channel.invokeMethod("drawWidget", hashMapOf(
+ "widgetId" to widgetId,
+ "widthPx" to widthPx,
+ "heightPx" to heightPx,
+ "devicePixelRatio" to devicePixelRatio,
+ "drawEntryImage" to drawEntryImage,
+ "reuseEntry" to reuseEntry,
+ ), object : MethodChannel.Result {
+ override fun success(result: Any?) {
+ cont.resume(result)
+ }
+
+ override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
+ cont.resumeWithException(Exception("$errorCode: $errorMessage\n$errorDetails"))
+ }
+
+ override fun notImplemented() {
+ cont.resumeWithException(Exception("not implemented"))
+ }
+ })
+ }
+ }
+ }
+ if (bytes is ByteArray) return bytes
+ } catch (e: Exception) {
+ Log.e(LOG_TAG, "failed to draw widget for widgetId=$widgetId widthPx=$widthPx heightPx=$heightPx", e)
+ }
+ return null
+ }
+
+ private fun updateWidgetImage(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ widgetId: Int,
+ widgetInfo: Bundle,
+ bytes: ByteArray?,
+ ) {
+ bytes ?: return
+
+ val devicePixelRatio = context.resources.displayMetrics.density
+ val widthPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) * devicePixelRatio).roundToInt()
+ val heightPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) * devicePixelRatio).roundToInt()
+
+ try {
+ val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888)
+ bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(bytes))
+
+ // set a unique URI to prevent the intent (and its extras) from being shared by different widgets
+ val intent = Intent(MainActivity.INTENT_ACTION_WIDGET_OPEN, Uri.parse("widget://$widgetId"), context, MainActivity::class.java)
+ .putExtra(MainActivity.EXTRA_KEY_WIDGET_ID, widgetId)
+
+ val activity = PendingIntent.getActivity(
+ context,
+ 0,
+ intent,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+ )
+
+ val views = RemoteViews(context.packageName, R.layout.app_widget).apply {
+ setImageViewBitmap(R.id.widget_img, bitmap)
+ setOnClickPendingIntent(R.id.widget_img, activity)
+ }
+
+ appWidgetManager.updateAppWidget(widgetId, views)
+ bitmap.recycle()
+ } catch (e: Exception) {
+ Log.e(LOG_TAG, "failed to draw widget", e)
+ }
+ }
+
+ companion object {
+ private val LOG_TAG = LogUtils.createTag()
+ private const val WIDGET_DART_ENTRYPOINT = "widgetMain"
+ private const val WIDGET_DRAW_CHANNEL = "deckers.thibault/aves/widget_draw"
+
+ private var flutterEngine: FlutterEngine? = null
+ private var imageByteFetchJob: Job? = null
+
+ private suspend fun initFlutterEngine(context: Context) {
+ if (flutterEngine != null) return
+
+ FlutterUtils.runOnUiThread {
+ flutterEngine = FlutterEngine(context.applicationContext)
+ }
+ initChannels(context)
+
+ flutterEngine!!.apply {
+ if (!dartExecutor.isExecutingDart) {
+ val appBundlePathOverride = FlutterInjector.instance().flutterLoader().findAppBundlePath()
+ val entrypoint = DartExecutor.DartEntrypoint(appBundlePathOverride, WIDGET_DART_ENTRYPOINT)
+ FlutterUtils.runOnUiThread {
+ dartExecutor.executeDartEntrypoint(entrypoint)
+ }
+ }
+ }
+ }
+
+ private fun initChannels(context: Context) {
+ val messenger = flutterEngine!!.dartExecutor
+
+ // dart -> platform -> dart
+ // - need Context
+ MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context))
+ MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context))
+ MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(context))
+
+ // result streaming: dart -> platform ->->-> dart
+ // - need Context
+ StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(context, args) }
+ StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(context, args) }
+ }
+
+ }
+}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
index f7f1a5b4b..81d468d32 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
@@ -2,10 +2,14 @@ package deckers.thibault.aves
import android.annotation.SuppressLint
import android.app.SearchManager
+import android.appwidget.AppWidgetManager
import android.content.ClipData
import android.content.Intent
import android.net.Uri
-import android.os.*
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.pm.ShortcutInfoCompat
@@ -13,6 +17,8 @@ import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.calls.*
+import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
+import deckers.thibault.aves.channel.calls.window.WindowHandler
import deckers.thibault.aves.channel.streams.*
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat
@@ -23,12 +29,12 @@ import io.flutter.plugin.common.MethodChannel
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
-class MainActivity : FlutterActivity() {
+open class MainActivity : FlutterActivity() {
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler
private lateinit var intentStreamHandler: IntentStreamHandler
private lateinit var analysisStreamHandler: AnalysisStreamHandler
- private lateinit var intentDataMap: MutableMap
+ internal lateinit var intentDataMap: MutableMap
private lateinit var analysisHandler: AnalysisHandler
override fun onCreate(savedInstanceState: Bundle?) {
@@ -51,7 +57,7 @@ class MainActivity : FlutterActivity() {
// )
super.onCreate(savedInstanceState)
- val messenger = flutterEngine!!.dartExecutor.binaryMessenger
+ val messenger = flutterEngine!!.dartExecutor
// dart -> platform -> dart
// - need Context
@@ -63,14 +69,17 @@ class MainActivity : FlutterActivity() {
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
MethodChannel(messenger, GlobalSearchHandler.CHANNEL).setMethodCallHandler(GlobalSearchHandler(this))
+ MethodChannel(messenger, HomeWidgetHandler.CHANNEL).setMethodCallHandler(HomeWidgetHandler(this))
+ MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(this))
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
- // - need Activity
+ // - need ContextWrapper
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
- MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this))
+ MethodChannel(messenger, MediaEditHandler.CHANNEL).setMethodCallHandler(MediaEditHandler(this))
MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this))
- MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(WindowHandler(this))
+ // - need Activity
+ MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
// result streaming: dart -> platform ->->-> dart
// - need Context
@@ -78,7 +87,7 @@ class MainActivity : FlutterActivity() {
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) }
// - need Activity
StreamsChannel(messenger, ImageOpStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageOpStreamHandler(this, args) }
- StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL).setStreamHandlerFactory { args -> StorageAccessStreamHandler(this, args) }
+ StreamsChannel(messenger, ActivityResultStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ActivityResultStreamHandler(this, args) }
// change monitoring: platform -> dart
mediaStoreChangeStreamHandler = MediaStoreChangeStreamHandler(this).apply {
@@ -93,15 +102,16 @@ class MainActivity : FlutterActivity() {
intentStreamHandler = IntentStreamHandler().apply {
EventChannel(messenger, IntentStreamHandler.CHANNEL).setStreamHandler(this)
}
- // detail fetch: dart -> platform
+ // intent detail & result: dart -> platform
intentDataMap = extractIntentData(intent)
- MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result ->
+ MethodChannel(messenger, INTENT_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"getIntentData" -> {
result.success(intentDataMap)
intentDataMap.clear()
}
- "pick" -> pick(call)
+ "submitPickedItems" -> submitPickedItems(call)
+ "submitPickedCollectionFilters" -> submitPickedCollectionFilters(call)
}
}
@@ -156,27 +166,33 @@ class MainActivity : FlutterActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
- DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(data, resultCode, requestCode)
+ DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(requestCode, resultCode, data)
DELETE_SINGLE_PERMISSION_REQUEST,
MEDIA_WRITE_BULK_PERMISSION_REQUEST -> onScopedStoragePermissionResult(resultCode)
CREATE_FILE_REQUEST,
OPEN_FILE_REQUEST -> onStorageAccessResult(requestCode, data?.data)
+ PICK_COLLECTION_FILTERS_REQUEST -> onCollectionFiltersPickResult(resultCode, data)
}
}
- @SuppressLint("WrongConstant", "ObsoleteSdkInt")
- private fun onDocumentTreeAccessResult(data: Intent?, resultCode: Int, requestCode: Int) {
- val treeUri = data?.data
+ private fun onCollectionFiltersPickResult(resultCode: Int, intent: Intent?) {
+ val filters = if (resultCode == RESULT_OK) extractFiltersFromIntent(intent) else null
+ pendingCollectionFilterPickHandler?.let { it(filters) }
+ }
+
+ private fun onDocumentTreeAccessResult(requestCode: Int, resultCode: Int, intent: Intent?) {
+ val treeUri = intent?.data
if (resultCode != RESULT_OK || treeUri == null) {
onStorageAccessResult(requestCode, null)
return
}
+ @SuppressLint("WrongConstant", "ObsoleteSdkInt")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- val canPersist = (data.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0
+ val canPersist = (intent.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0
if (canPersist) {
// save access permissions across reboots
- val takeFlags = (data.flags
+ val takeFlags = (intent.flags
and (Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
try {
@@ -197,18 +213,11 @@ class MainActivity : FlutterActivity() {
}
}
- private fun extractIntentData(intent: Intent?): MutableMap {
- when (intent?.action) {
+ open fun extractIntentData(intent: Intent?): MutableMap {
+ when (val action = intent?.action) {
Intent.ACTION_MAIN -> {
- intent.getStringExtra(SHORTCUT_KEY_PAGE)?.let { page ->
- var filters = intent.getStringArrayExtra(SHORTCUT_KEY_FILTERS_ARRAY)?.toList()
- if (filters == null) {
- // fallback for shortcuts created on API < 26
- val filterString = intent.getStringExtra(SHORTCUT_KEY_FILTERS_STRING)
- if (filterString != null) {
- filters = filterString.split(EXTRA_STRING_ARRAY_SEPARATOR)
- }
- }
+ intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page ->
+ val filters = extractFiltersFromIntent(intent)
return hashMapOf(
INTENT_DATA_KEY_PAGE to page,
INTENT_DATA_KEY_FILTERS to filters,
@@ -228,7 +237,7 @@ class MainActivity : FlutterActivity() {
}
Intent.ACTION_GET_CONTENT, Intent.ACTION_PICK -> {
return hashMapOf(
- INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK,
+ INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK_ITEMS,
INTENT_DATA_KEY_MIME_TYPE to intent.type,
INTENT_DATA_KEY_ALLOW_MULTIPLE to (intent.extras?.getBoolean(Intent.EXTRA_ALLOW_MULTIPLE) ?: false),
)
@@ -244,6 +253,22 @@ class MainActivity : FlutterActivity() {
INTENT_DATA_KEY_QUERY to intent.getStringExtra(SearchManager.QUERY),
)
}
+ INTENT_ACTION_PICK_COLLECTION_FILTERS -> {
+ val initialFilters = extractFiltersFromIntent(intent)
+ return hashMapOf(
+ INTENT_DATA_KEY_ACTION to action,
+ INTENT_DATA_KEY_FILTERS to initialFilters,
+ )
+ }
+ INTENT_ACTION_WIDGET_OPEN -> {
+ val widgetId = intent.getIntExtra(EXTRA_KEY_WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
+ if (widgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
+ return hashMapOf(
+ INTENT_DATA_KEY_ACTION to action,
+ INTENT_DATA_KEY_WIDGET_ID to widgetId,
+ )
+ }
+ }
Intent.ACTION_RUN -> {
// flutter run
}
@@ -254,7 +279,22 @@ class MainActivity : FlutterActivity() {
return HashMap()
}
- private fun pick(call: MethodCall) {
+ private fun extractFiltersFromIntent(intent: Intent?): List? {
+ intent ?: return null
+
+ val filters = intent.getStringArrayExtra(EXTRA_KEY_FILTERS_ARRAY)?.toList()
+ if (filters != null) return filters
+
+ // fallback for shortcuts created on API < 26
+ val filterString = intent.getStringExtra(EXTRA_KEY_FILTERS_STRING)
+ if (filterString != null) {
+ return filterString.split(EXTRA_STRING_ARRAY_SEPARATOR)
+ }
+
+ return null
+ }
+
+ private fun submitPickedItems(call: MethodCall) {
val pickedUris = call.argument>("uris")
if (pickedUris != null && pickedUris.isNotEmpty()) {
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(context, Uri.parse(uriString)) }
@@ -278,6 +318,19 @@ class MainActivity : FlutterActivity() {
finish()
}
+ private fun submitPickedCollectionFilters(call: MethodCall) {
+ val filters = call.argument>("filters")
+ if (filters != null) {
+ val intent = Intent()
+ .putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray())
+ .putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
+ setResult(RESULT_OK, intent)
+ } else {
+ setResult(RESULT_CANCELED)
+ }
+ finish()
+ }
+
@RequiresApi(Build.VERSION_CODES.N_MR1)
private fun setupShortcuts() {
// do not use 'route' as extra key, as the Flutter framework acts on it
@@ -291,7 +344,7 @@ class MainActivity : FlutterActivity() {
.setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_search else R.drawable.ic_shortcut_search))
.setIntent(
Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
- .putExtra(SHORTCUT_KEY_PAGE, "/search")
+ .putExtra(EXTRA_KEY_PAGE, "/search")
)
.build()
@@ -300,7 +353,7 @@ class MainActivity : FlutterActivity() {
.setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_movie else R.drawable.ic_shortcut_movie))
.setIntent(
Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
- .putExtra(SHORTCUT_KEY_PAGE, "/collection")
+ .putExtra(EXTRA_KEY_PAGE, "/collection")
.putExtra("filters", arrayOf("{\"type\":\"mime\",\"mime\":\"video/*\"}"))
)
.build()
@@ -314,7 +367,7 @@ class MainActivity : FlutterActivity() {
companion object {
private val LOG_TAG = LogUtils.createTag()
- const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer"
+ const val INTENT_CHANNEL = "deckers.thibault/aves/intent"
const val EXTRA_STRING_ARRAY_SEPARATOR = "###"
const val DOCUMENT_TREE_ACCESS_REQUEST = 1
const val OPEN_FROM_ANALYSIS_SERVICE = 2
@@ -322,29 +375,39 @@ class MainActivity : FlutterActivity() {
const val OPEN_FILE_REQUEST = 4
const val DELETE_SINGLE_PERMISSION_REQUEST = 5
const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 6
+ const val PICK_COLLECTION_FILTERS_REQUEST = 7
- const val INTENT_DATA_KEY_ACTION = "action"
- const val INTENT_DATA_KEY_FILTERS = "filters"
- const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
- const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
- const val INTENT_DATA_KEY_PAGE = "page"
- const val INTENT_DATA_KEY_URI = "uri"
- const val INTENT_DATA_KEY_QUERY = "query"
-
- const val INTENT_ACTION_PICK = "pick"
+ const val INTENT_ACTION_PICK_ITEMS = "pick_items"
+ const val INTENT_ACTION_PICK_COLLECTION_FILTERS = "pick_collection_filters"
+ const val INTENT_ACTION_SCREEN_SAVER = "screen_saver"
+ const val INTENT_ACTION_SCREEN_SAVER_SETTINGS = "screen_saver_settings"
const val INTENT_ACTION_SEARCH = "search"
const val INTENT_ACTION_SET_WALLPAPER = "set_wallpaper"
const val INTENT_ACTION_VIEW = "view"
+ const val INTENT_ACTION_WIDGET_OPEN = "widget_open"
+ const val INTENT_ACTION_WIDGET_SETTINGS = "widget_settings"
- const val SHORTCUT_KEY_PAGE = "page"
- const val SHORTCUT_KEY_FILTERS_ARRAY = "filters"
- const val SHORTCUT_KEY_FILTERS_STRING = "filtersString"
+ const val INTENT_DATA_KEY_ACTION = "action"
+ const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
+ const val INTENT_DATA_KEY_FILTERS = "filters"
+ const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
+ const val INTENT_DATA_KEY_PAGE = "page"
+ const val INTENT_DATA_KEY_QUERY = "query"
+ const val INTENT_DATA_KEY_URI = "uri"
+ const val INTENT_DATA_KEY_WIDGET_ID = "widgetId"
+
+ const val EXTRA_KEY_PAGE = "page"
+ const val EXTRA_KEY_FILTERS_ARRAY = "filters"
+ const val EXTRA_KEY_FILTERS_STRING = "filtersString"
+ const val EXTRA_KEY_WIDGET_ID = "widgetId"
// request code to pending runnable
val pendingStorageAccessResultHandlers = ConcurrentHashMap()
var pendingScopedStoragePermissionCompleter: CompletableFuture? = null
+ var pendingCollectionFilterPickHandler: ((filters: List?) -> Unit)? = null
+
private fun onStorageAccessResult(requestCode: Int, uri: Uri?) {
Log.i(LOG_TAG, "onStorageAccessResult with requestCode=$requestCode, uri=$uri")
val handler = pendingStorageAccessResultHandlers.remove(requestCode) ?: return
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverService.kt b/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverService.kt
new file mode 100644
index 000000000..b6373d3ae
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverService.kt
@@ -0,0 +1,136 @@
+package deckers.thibault.aves
+
+import android.service.dreams.DreamService
+import android.util.Log
+import android.view.View
+import app.loup.streams_channel.StreamsChannel
+import deckers.thibault.aves.channel.calls.*
+import deckers.thibault.aves.channel.calls.window.ServiceWindowHandler
+import deckers.thibault.aves.channel.calls.window.WindowHandler
+import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
+import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
+import deckers.thibault.aves.utils.LogUtils
+import io.flutter.FlutterInjector
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.android.FlutterSurfaceView
+import io.flutter.embedding.android.FlutterView
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint
+import io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister
+import io.flutter.plugin.common.MethodChannel
+
+// for FlutterView-level integration, cf https://docs.flutter.dev/development/add-to-app/android/add-flutter-view
+class ScreenSaverService : DreamService() {
+ private var flutterEngine: FlutterEngine? = null
+ private var flutterView: FlutterView? = null
+
+ override fun onAttachedToWindow() {
+ Log.i(LOG_TAG, "onAttachedToWindow")
+ super.onAttachedToWindow()
+ initDream()
+ createEngine()
+ setContentView(createView())
+ }
+
+ override fun onDreamingStarted() {
+ Log.i(LOG_TAG, "onDreamingStarted")
+ super.onDreamingStarted()
+ onStart()
+ }
+
+ override fun onDreamingStopped() {
+ Log.i(LOG_TAG, "onDreamingStopped")
+ release()
+ super.onDreamingStopped()
+ }
+
+ override fun onDetachedFromWindow() {
+ Log.i(LOG_TAG, "onDetachedFromWindow")
+ destroyView()
+ super.onDetachedFromWindow()
+ }
+
+ private fun initDream() {
+ isInteractive = false
+ isFullscreen = true
+ }
+
+ private fun createEngine() {
+ flutterEngine = flutterEngine ?: FlutterEngine(this, null, false)
+ GeneratedPluginRegister.registerGeneratedPlugins(flutterEngine!!)
+ initChannels()
+ }
+
+ private fun createView(): View {
+ flutterView = FlutterView(this, FlutterSurfaceView(this)).apply {
+ id = FlutterActivity.FLUTTER_VIEW_ID
+ attachToFlutterEngine(flutterEngine!!)
+ }
+ return flutterView!!
+ }
+
+ private fun destroyView() {
+ flutterEngine?.lifecycleChannel?.appIsDetached()
+ flutterView?.detachFromFlutterEngine()
+ }
+
+ private fun release() {
+ destroyView()
+ flutterEngine = null
+ flutterView = null
+ }
+
+ private fun onStart() {
+ flutterEngine!!.apply {
+ if (!dartExecutor.isExecutingDart) {
+ navigationChannel.setInitialRoute(DEFAULT_INITIAL_ROUTE)
+ val appBundlePathOverride = FlutterInjector.instance().flutterLoader().findAppBundlePath()
+ val entrypoint = DartEntrypoint(appBundlePathOverride, DEFAULT_DART_ENTRYPOINT)
+ dartExecutor.executeDartEntrypoint(entrypoint)
+ }
+ lifecycleChannel.appIsResumed()
+ }
+ }
+
+ private fun initChannels() {
+ val messenger = flutterEngine!!.dartExecutor
+
+ // dart -> platform -> dart
+ // - need Context
+ MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
+ MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
+ MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(this))
+ MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
+ MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
+ // - need ContextWrapper
+ MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
+ // - need Service
+ MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ServiceWindowHandler(this))
+
+ // result streaming: dart -> platform ->->-> dart
+ // - need Context
+ StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
+ StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) }
+
+ // intent handling
+ // detail fetch: dart -> platform
+ MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result ->
+ when (call.method) {
+ "getIntentData" -> {
+ result.success(intentDataMap)
+ }
+ }
+ }
+ }
+
+ companion object {
+ private val LOG_TAG = LogUtils.createTag()
+ private val intentDataMap: Map = hashMapOf(
+ MainActivity.INTENT_DATA_KEY_ACTION to MainActivity.INTENT_ACTION_SCREEN_SAVER,
+ )
+
+ // from `FlutterActivityLaunchConfigs`
+ const val DEFAULT_DART_ENTRYPOINT = "main"
+ const val DEFAULT_INITIAL_ROUTE = "/"
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverSettingsActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverSettingsActivity.kt
new file mode 100644
index 000000000..dbc2e74f3
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverSettingsActivity.kt
@@ -0,0 +1,11 @@
+package deckers.thibault.aves
+
+import android.content.Intent
+
+class ScreenSaverSettingsActivity : MainActivity() {
+ override fun extractIntentData(intent: Intent?): MutableMap {
+ return hashMapOf(
+ INTENT_DATA_KEY_ACTION to INTENT_ACTION_SCREEN_SAVER_SETTINGS,
+ )
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt
index 8d4472dbe..59920a934 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt
@@ -23,7 +23,7 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
-class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvider() {
+class SearchSuggestionsProvider : ContentProvider() {
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? {
@@ -67,15 +67,23 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid
}
private suspend fun getSuggestions(context: Context, query: String): List {
- if (backgroundFlutterEngine == null) {
+ if (flutterEngine == null) {
FlutterUtils.initFlutterEngine(context, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) {
- backgroundFlutterEngine = it
+ flutterEngine = it
}
}
- val messenger = backgroundFlutterEngine!!.dartExecutor.binaryMessenger
+ val messenger = flutterEngine!!.dartExecutor
val backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL)
- backgroundChannel.setMethodCallHandler(this)
+ backgroundChannel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
+ when (call.method) {
+ "initialized" -> {
+ Log.d(LOG_TAG, "background channel is ready")
+ result.success(null)
+ }
+ else -> result.notImplemented()
+ }
+ }
try {
return suspendCoroutine { cont ->
@@ -96,7 +104,7 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid
}
override fun notImplemented() {
- cont.resumeWithException(NotImplementedError("getSuggestions"))
+ cont.resumeWithException(Exception("not implemented"))
}
})
}
@@ -108,16 +116,6 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid
}
}
- override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
- when (call.method) {
- "initialized" -> {
- Log.d(LOG_TAG, "background channel is ready")
- result.success(null)
- }
- else -> result.notImplemented()
- }
- }
-
override fun onCreate(): Boolean = true
override fun getType(uri: Uri): String? = null
@@ -137,6 +135,6 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid
const val SHARED_PREFERENCES_KEY = "platform_search"
const val CALLBACK_HANDLE_KEY = "callback_handle"
- private var backgroundFlutterEngine: FlutterEngine? = null
+ private var flutterEngine: FlutterEngine? = null
}
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt
index d7f28867e..4fe1f9a6a 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt
@@ -2,10 +2,15 @@ package deckers.thibault.aves
import android.content.Intent
import android.net.Uri
-import android.os.*
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
import android.util.Log
import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.calls.*
+import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
+import deckers.thibault.aves.channel.calls.window.WindowHandler
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat
@@ -23,18 +28,19 @@ class WallpaperActivity : FlutterActivity() {
super.onCreate(savedInstanceState)
- val messenger = flutterEngine!!.dartExecutor.binaryMessenger
+ val messenger = flutterEngine!!.dartExecutor
// dart -> platform -> dart
// - need Context
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
+ MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
- // - need Activity
+ // - need ContextWrapper
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
- MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this))
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this))
- MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(WindowHandler(this))
+ // - need Activity
+ MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
// result streaming: dart -> platform ->->-> dart
// - need Context
@@ -43,7 +49,7 @@ class WallpaperActivity : FlutterActivity() {
// intent handling
// detail fetch: dart -> platform
intentDataMap = extractIntentData(intent)
- MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result ->
+ MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"getIntentData" -> {
result.success(intentDataMap)
@@ -67,16 +73,6 @@ class WallpaperActivity : FlutterActivity() {
}
}
- override fun onStop() {
- Log.i(LOG_TAG, "onStop")
- super.onStop()
- }
-
- override fun onDestroy() {
- Log.i(LOG_TAG, "onDestroy")
- super.onDestroy()
- }
-
private fun extractIntentData(intent: Intent?): MutableMap {
when (intent?.action) {
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
@@ -102,6 +98,5 @@ class WallpaperActivity : FlutterActivity() {
companion object {
private val LOG_TAG = LogUtils.createTag()
- const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer"
}
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt
index 6deead748..eaf3e0bfa 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt
@@ -1,8 +1,8 @@
package deckers.thibault.aves.channel.calls
import android.annotation.SuppressLint
-import android.app.Activity
import android.content.Context
+import android.content.ContextWrapper
import android.os.Build
import android.provider.Settings
import android.util.Log
@@ -13,7 +13,7 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
-class AccessibilityHandler(private val activity: Activity) : MethodCallHandler {
+class AccessibilityHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"areAnimationsRemoved" -> safe(call, result, ::areAnimationsRemoved)
@@ -28,7 +28,7 @@ class AccessibilityHandler(private val activity: Activity) : MethodCallHandler {
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
try {
- removed = Settings.Global.getFloat(activity.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f
+ removed = Settings.Global.getFloat(contextWrapper.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
}
@@ -49,7 +49,7 @@ class AccessibilityHandler(private val activity: Activity) : MethodCallHandler {
val originalTimeoutMillis = call.argument("originalTimeoutMillis")
val content = call.argument>("content")
if (originalTimeoutMillis == null || content == null) {
- result.error("getRecommendedTimeoutMillis-args", "failed because of missing arguments", null)
+ result.error("getRecommendedTimeoutMillis-args", "missing arguments", null)
return
}
@@ -66,7 +66,7 @@ class AccessibilityHandler(private val activity: Activity) : MethodCallHandler {
}
}
- val am = activity.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
+ val am = contextWrapper.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
if (am == null) {
result.error("getRecommendedTimeoutMillis-service", "failed to get accessibility manager", null)
return
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt
index 7f083ae55..75fa2a306 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt
@@ -16,7 +16,10 @@ import deckers.thibault.aves.utils.ContextUtils.isMyServiceRunning
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
class AnalysisHandler(private val activity: Activity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler, AnalysisServiceListener {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@@ -33,7 +36,7 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
private fun registerCallback(call: MethodCall, result: MethodChannel.Result) {
val callbackHandle = call.argument("callbackHandle")?.toLong()
if (callbackHandle == null) {
- result.error("registerCallback-args", "failed because of missing arguments", null)
+ result.error("registerCallback-args", "missing arguments", null)
return
}
@@ -47,7 +50,7 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
private fun startAnalysis(call: MethodCall, result: MethodChannel.Result) {
val force = call.argument("force")
if (force == null) {
- result.error("startAnalysis-args", "failed because of missing arguments", null)
+ result.error("startAnalysis-args", "missing arguments", null)
return
}
@@ -56,9 +59,9 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
if (!activity.isMyServiceRunning(AnalysisService::class.java)) {
val intent = Intent(activity, AnalysisService::class.java)
- intent.putExtra(AnalysisService.KEY_COMMAND, AnalysisService.COMMAND_START)
- intent.putExtra(AnalysisService.KEY_ENTRY_IDS, entryIds?.toIntArray())
- intent.putExtra(AnalysisService.KEY_FORCE, force)
+ .putExtra(AnalysisService.KEY_COMMAND, AnalysisService.COMMAND_START)
+ .putExtra(AnalysisService.KEY_ENTRY_IDS, entryIds?.toIntArray())
+ .putExtra(AnalysisService.KEY_FORCE, force)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.startForegroundService(intent)
} else {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
index c5b2d2223..5a9ea5ace 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
@@ -20,9 +20,9 @@ import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.MainActivity.Companion.EXTRA_STRING_ARRAY_SEPARATOR
-import deckers.thibault.aves.MainActivity.Companion.SHORTCUT_KEY_FILTERS_ARRAY
-import deckers.thibault.aves.MainActivity.Companion.SHORTCUT_KEY_FILTERS_STRING
-import deckers.thibault.aves.MainActivity.Companion.SHORTCUT_KEY_PAGE
+import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_ARRAY
+import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING
+import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE
import deckers.thibault.aves.R
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
@@ -142,7 +142,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val packageName = call.argument("packageName")
val sizeDip = call.argument("sizeDip")?.toDouble()
if (packageName == null || sizeDip == null) {
- result.error("getAppIcon-args", "failed because of missing arguments", null)
+ result.error("getAppIcon-args", "missing arguments", null)
return
}
@@ -208,7 +208,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument("uri")?.let { Uri.parse(it) }
val label = call.argument("label")
if (uri == null) {
- result.error("copyToClipboard-args", "failed because of missing arguments", null)
+ result.error("copyToClipboard-args", "missing arguments", null)
return
}
@@ -235,7 +235,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument("uri")?.let { Uri.parse(it) }
val mimeType = call.argument("mimeType")
if (uri == null) {
- result.error("edit-args", "failed because of missing arguments", null)
+ result.error("edit-args", "missing arguments", null)
return
}
@@ -252,7 +252,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument("uri")?.let { Uri.parse(it) }
val mimeType = call.argument("mimeType")
if (uri == null) {
- result.error("open-args", "failed because of missing arguments", null)
+ result.error("open-args", "missing arguments", null)
return
}
@@ -267,7 +267,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
private fun openMap(call: MethodCall, result: MethodChannel.Result) {
val geoUri = call.argument("geoUri")?.let { Uri.parse(it) }
if (geoUri == null) {
- result.error("openMap-args", "failed because of missing arguments", null)
+ result.error("openMap-args", "missing arguments", null)
return
}
@@ -282,7 +282,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument("uri")?.let { Uri.parse(it) }
val mimeType = call.argument("mimeType")
if (uri == null) {
- result.error("setAs-args", "failed because of missing arguments", null)
+ result.error("setAs-args", "missing arguments", null)
return
}
@@ -298,7 +298,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val title = call.argument("title")
val urisByMimeType = call.argument