fixed handling wallpaper intent without uri

#1052 handle secure review intent
This commit is contained in:
Thibault Deckers 2024-06-29 01:29:22 +02:00
parent 0f1d8ec760
commit 27db528e67
6 changed files with 69 additions and 119 deletions

View file

@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
### Fixed ### Fixed
- switching to PiP when changing device orientation on Android >=13 - switching to PiP when changing device orientation on Android >=13
- handling wallpaper intent without URI
## <a id="v1.11.3"></a>[v1.11.3] - 2024-06-17 ## <a id="v1.11.3"></a>[v1.11.3] - 2024-06-17

View file

@ -120,6 +120,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:showWhenLocked="true"
android:supportsRtl="true" android:supportsRtl="true"
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
<activity <activity
@ -143,6 +144,7 @@
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<action android:name="android.provider.action.REVIEW" /> <action android:name="android.provider.action.REVIEW" />
<action android:name="android.provider.action.REVIEW_SECURE" />
<action android:name="com.android.camera.action.REVIEW" /> <action android:name="com.android.camera.action.REVIEW" />
<action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" /> <action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" />
@ -163,6 +165,7 @@
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<action android:name="android.provider.action.REVIEW" /> <action android:name="android.provider.action.REVIEW" />
<action android:name="android.provider.action.REVIEW_SECURE" />
<action android:name="com.android.camera.action.REVIEW" /> <action android:name="com.android.camera.action.REVIEW" />
<action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" /> <action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" />

View file

@ -36,6 +36,7 @@ import deckers.thibault.aves.channel.calls.MetadataEditHandler
import deckers.thibault.aves.channel.calls.MetadataFetchHandler import deckers.thibault.aves.channel.calls.MetadataFetchHandler
import deckers.thibault.aves.channel.calls.SecurityHandler import deckers.thibault.aves.channel.calls.SecurityHandler
import deckers.thibault.aves.channel.calls.StorageHandler import deckers.thibault.aves.channel.calls.StorageHandler
import deckers.thibault.aves.channel.calls.WallpaperHandler
import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
import deckers.thibault.aves.channel.calls.window.WindowHandler import deckers.thibault.aves.channel.calls.window.WindowHandler
import deckers.thibault.aves.channel.streams.ActivityResultStreamHandler import deckers.thibault.aves.channel.streams.ActivityResultStreamHandler
@ -135,6 +136,7 @@ open class MainActivity : FlutterFragmentActivity() {
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this)) MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
MethodChannel(messenger, MediaEditHandler.CHANNEL).setMethodCallHandler(MediaEditHandler(this)) MethodChannel(messenger, MediaEditHandler.CHANNEL).setMethodCallHandler(MediaEditHandler(this))
MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this)) MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this))
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this))
// - need Activity // - need Activity
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this)) MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
@ -301,16 +303,32 @@ open class MainActivity : FlutterFragmentActivity() {
Intent.ACTION_VIEW, Intent.ACTION_VIEW,
Intent.ACTION_SEND, Intent.ACTION_SEND,
MediaStore.ACTION_REVIEW, MediaStore.ACTION_REVIEW,
MediaStore.ACTION_REVIEW_SECURE,
"com.android.camera.action.REVIEW", "com.android.camera.action.REVIEW",
"com.android.camera.action.SPLIT_SCREEN_REVIEW" -> { "com.android.camera.action.SPLIT_SCREEN_REVIEW" -> {
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri -> (intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional // MIME type is optional
val type = intent.type ?: intent.resolveType(this) val type = intent.type ?: intent.resolveType(this)
return hashMapOf( val fields = hashMapOf<String, Any?>(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW, INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW,
INTENT_DATA_KEY_MIME_TYPE to type, INTENT_DATA_KEY_MIME_TYPE to type,
INTENT_DATA_KEY_URI to uri.toString(), INTENT_DATA_KEY_URI to uri.toString(),
) )
if (action == MediaStore.ACTION_REVIEW_SECURE) {
val uris = ArrayList<String>()
intent.clipData?.let { clipData ->
for (i in 0 until clipData.itemCount) {
clipData.getItemAt(i).uri?.let { uris.add(it.toString()) }
}
}
fields[INTENT_DATA_KEY_SECURE_URIS] = uris
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && intent.hasExtra(MediaStore.EXTRA_BRIGHTNESS)) {
fields[INTENT_DATA_KEY_BRIGHTNESS] = intent.getFloatExtra(MediaStore.EXTRA_BRIGHTNESS, 0f)
}
return fields
} }
} }
@ -390,7 +408,7 @@ open class MainActivity : FlutterFragmentActivity() {
return null return null
} }
private fun submitPickedItems(call: MethodCall) { open fun submitPickedItems(call: MethodCall) {
val pickedUris = call.argument<List<String>>("uris") val pickedUris = call.argument<List<String>>("uris")
if (!pickedUris.isNullOrEmpty()) { if (!pickedUris.isNullOrEmpty()) {
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) } val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) }
@ -498,11 +516,13 @@ open class MainActivity : FlutterFragmentActivity() {
const val INTENT_DATA_KEY_ACTION = "action" const val INTENT_DATA_KEY_ACTION = "action"
const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple" const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
const val INTENT_DATA_KEY_BRIGHTNESS = "brightness"
const val INTENT_DATA_KEY_FILTERS = "filters" const val INTENT_DATA_KEY_FILTERS = "filters"
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType" const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
const val INTENT_DATA_KEY_PAGE = "page" const val INTENT_DATA_KEY_PAGE = "page"
const val INTENT_DATA_KEY_QUERY = "query" const val INTENT_DATA_KEY_QUERY = "query"
const val INTENT_DATA_KEY_SAFE_MODE = "safeMode" const val INTENT_DATA_KEY_SAFE_MODE = "safeMode"
const val INTENT_DATA_KEY_SECURE_URIS = "secureUris"
const val INTENT_DATA_KEY_URI = "uri" const val INTENT_DATA_KEY_URI = "uri"
const val INTENT_DATA_KEY_WIDGET_ID = "widgetId" const val INTENT_DATA_KEY_WIDGET_ID = "widgetId"

View file

@ -2,132 +2,53 @@ package deckers.thibault.aves
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import deckers.thibault.aves.channel.calls.AppAdapterHandler
import android.os.Handler
import android.os.Looper
import android.util.Log
import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.AvesByteSendingMethodCodec
import deckers.thibault.aves.channel.calls.AccessibilityHandler
import deckers.thibault.aves.channel.calls.DeviceHandler
import deckers.thibault.aves.channel.calls.EmbeddedDataHandler
import deckers.thibault.aves.channel.calls.MediaFetchBytesHandler
import deckers.thibault.aves.channel.calls.MediaFetchObjectHandler
import deckers.thibault.aves.channel.calls.MediaSessionHandler
import deckers.thibault.aves.channel.calls.MetadataFetchHandler
import deckers.thibault.aves.channel.calls.StorageHandler
import deckers.thibault.aves.channel.calls.WallpaperHandler
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.channel.streams.MediaCommandStreamHandler
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class WallpaperActivity : FlutterFragmentActivity() { class WallpaperActivity : MainActivity() {
private lateinit var intentDataMap: FieldMap private var originalIntent: String? = null
private lateinit var mediaSessionHandler: MediaSessionHandler
override fun onCreate(savedInstanceState: Bundle?) { override fun extractIntentData(intent: Intent?): FieldMap {
super.onCreate(savedInstanceState) if (intent != null) {
when (intent.action) {
Log.i(LOG_TAG, "onCreate intent=$intent")
intent.extras?.takeUnless { it.isEmpty }?.let {
Log.i(LOG_TAG, "onCreate intent extras=$it")
}
intentDataMap = extractIntentData(intent)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val messenger = flutterEngine.dartExecutor
// notification: platform -> dart
val mediaCommandStreamHandler = MediaCommandStreamHandler().apply {
EventChannel(messenger, MediaCommandStreamHandler.CHANNEL).setStreamHandler(this)
}
// dart -> platform -> dart
// - need Context
mediaSessionHandler = MediaSessionHandler(this, mediaCommandStreamHandler)
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler)
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
// - need ContextWrapper
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this))
// - need Activity
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
// result streaming: dart -> platform ->->-> dart
// - need Context
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
// intent handling
// detail fetch: dart -> platform
MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result -> onMethodCall(call, result) }
}
override fun onStart() {
Log.i(LOG_TAG, "onStart")
super.onStart()
// as of Flutter v3.0.1, the window `viewInsets` and `viewPadding`
// are incorrect on startup in some environments (e.g. API 29 emulator),
// so we manually request to apply the insets to update the window metrics
Handler(Looper.getMainLooper()).postDelayed({
window.decorView.requestApplyInsets()
}, 100)
}
override fun onDestroy() {
mediaSessionHandler.dispose()
super.onDestroy()
}
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getIntentData" -> {
result.success(intentDataMap)
intentDataMap.clear()
}
}
}
private fun extractIntentData(intent: Intent?): FieldMap {
when (intent?.action) {
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> { Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri -> (intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional // MIME type is optional
val type = intent.type ?: intent.resolveType(this) val type = intent.type ?: intent.resolveType(this)
return hashMapOf( return hashMapOf(
MainActivity.INTENT_DATA_KEY_ACTION to MainActivity.INTENT_ACTION_SET_WALLPAPER, INTENT_DATA_KEY_ACTION to INTENT_ACTION_SET_WALLPAPER,
MainActivity.INTENT_DATA_KEY_MIME_TYPE to type, INTENT_DATA_KEY_MIME_TYPE to type,
MainActivity.INTENT_DATA_KEY_URI to uri.toString(), INTENT_DATA_KEY_URI to uri.toString(),
) )
} }
}
Intent.ACTION_RUN -> { // if the media URI is not provided we need to pick one first
// flutter run originalIntent = intent.action
} intent.action = Intent.ACTION_PICK
else -> {
Log.w(LOG_TAG, "unhandled intent action=${intent?.action}")
} }
} }
return HashMap()
} }
companion object { return super.extractIntentData(intent)
private val LOG_TAG = LogUtils.createTag<WallpaperActivity>() }
override fun submitPickedItems(call: MethodCall) {
if (originalIntent != null) {
val pickedUris = call.argument<List<String>>("uris")
if (!pickedUris.isNullOrEmpty()) {
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) }
onNewIntent(Intent().apply {
action = originalIntent
data = toUri(pickedUris.first())
})
} else {
setResult(RESULT_CANCELED)
finish()
}
} else {
super.submitPickedItems(call)
}
} }
} }

View file

@ -61,6 +61,7 @@ class _HomePageState extends State<HomePage> {
int? _widgetId; int? _widgetId;
String? _initialRouteName, _initialSearchQuery; String? _initialRouteName, _initialSearchQuery;
Set<CollectionFilter>? _initialFilters; Set<CollectionFilter>? _initialFilters;
List<String>? _secureUris;
static const allowedShortcutRoutes = [ static const allowedShortcutRoutes = [
CollectionPage.routeName, CollectionPage.routeName,
@ -91,6 +92,7 @@ class _HomePageState extends State<HomePage> {
final safeMode = intentData[IntentDataKeys.safeMode] ?? false; final safeMode = intentData[IntentDataKeys.safeMode] ?? false;
final intentAction = intentData[IntentDataKeys.action]; final intentAction = intentData[IntentDataKeys.action];
_initialFilters = null; _initialFilters = null;
_secureUris = null;
await androidFileUtils.init(); await androidFileUtils.init();
if (!{ if (!{
@ -127,6 +129,7 @@ class _HomePageState extends State<HomePage> {
uri = intentData[IntentDataKeys.uri]; uri = intentData[IntentDataKeys.uri];
mimeType = intentData[IntentDataKeys.mimeType]; mimeType = intentData[IntentDataKeys.mimeType];
} }
_secureUris = intentData[IntentDataKeys.secureUris];
if (uri != null) { if (uri != null) {
_viewerEntry = await _initViewerEntry( _viewerEntry = await _initViewerEntry(
uri: uri, uri: uri,
@ -208,7 +211,7 @@ class _HomePageState extends State<HomePage> {
canAnalyze: false, canAnalyze: false,
); );
case AppMode.view: case AppMode.view:
if (_isViewerSourceable(_viewerEntry)) { if (_isViewerSourceable(_viewerEntry) && _secureUris == null) {
final directory = _viewerEntry?.directory; final directory = _viewerEntry?.directory;
if (directory != null) { if (directory != null) {
unawaited(AnalysisService.registerCallback()); unawaited(AnalysisService.registerCallback());

View file

@ -14,11 +14,13 @@ class IntentActions {
class IntentDataKeys { class IntentDataKeys {
static const action = 'action'; static const action = 'action';
static const allowMultiple = 'allowMultiple'; static const allowMultiple = 'allowMultiple';
static const brightness = 'brightness';
static const filters = 'filters'; static const filters = 'filters';
static const mimeType = 'mimeType'; static const mimeType = 'mimeType';
static const page = 'page'; static const page = 'page';
static const query = 'query'; static const query = 'query';
static const safeMode = 'safeMode'; static const safeMode = 'safeMode';
static const secureUris = 'secureUris';
static const uri = 'uri'; static const uri = 'uri';
static const widgetId = 'widgetId'; static const widgetId = 'widgetId';
} }