use contextwrapper instead of activity

This commit is contained in:
Thibault Deckers 2022-06-24 16:06:56 +09:00
parent 3b17aa4149
commit a0eb5caa78
21 changed files with 374 additions and 155 deletions

View file

@ -0,0 +1,2 @@
<dream xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="com.example.app/.ScreenSaverSettingsActivity" />

View file

@ -5,7 +5,10 @@ import android.app.SearchManager
import android.content.ClipData import android.content.ClipData
import android.content.Intent import android.content.Intent
import android.net.Uri 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 android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
@ -13,6 +16,8 @@ import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import app.loup.streams_channel.StreamsChannel import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.calls.* 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.channel.streams.*
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat import deckers.thibault.aves.utils.getParcelableExtraCompat
@ -66,11 +71,12 @@ class MainActivity : FlutterActivity() {
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this)) MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
// - need Activity // - need ContextWrapper
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this)) MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this)) MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this))
MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(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 // result streaming: dart -> platform ->->-> dart
// - need Context // - need Context

View file

@ -0,0 +1,4 @@
package deckers.thibault.aves
class ScreenSaverService {
}

View file

@ -0,0 +1,4 @@
package deckers.thibault.aves
class ScreenSaverSettingsActivity {
}

View file

@ -2,10 +2,15 @@ package deckers.thibault.aves
import android.content.Intent import android.content.Intent
import android.net.Uri 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 android.util.Log
import app.loup.streams_channel.StreamsChannel import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.calls.* 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.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat import deckers.thibault.aves.utils.getParcelableExtraCompat
@ -30,11 +35,12 @@ class WallpaperActivity : FlutterActivity() {
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this)) MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this)) MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
// - need Activity // - need ContextWrapper
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this)) MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this)) MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this))
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(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 // result streaming: dart -> platform ->->-> dart
// - need Context // - need Context

View file

@ -1,8 +1,8 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.util.Log 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
import io.flutter.plugin.common.MethodChannel.MethodCallHandler 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) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"areAnimationsRemoved" -> safe(call, result, ::areAnimationsRemoved) "areAnimationsRemoved" -> safe(call, result, ::areAnimationsRemoved)
@ -28,7 +28,7 @@ class AccessibilityHandler(private val activity: Activity) : MethodCallHandler {
@SuppressLint("ObsoleteSdkInt") @SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
try { 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) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null) Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
} }
@ -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) { if (am == null) {
result.error("getRecommendedTimeoutMillis-service", "failed to get accessibility manager", null) result.error("getRecommendedTimeoutMillis-service", "failed to get accessibility manager", null)
return return

View file

@ -1,6 +1,6 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.app.Activity import android.content.ContextWrapper
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
@ -21,14 +21,17 @@ import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlin.math.roundToInt import kotlin.math.roundToInt
class MediaFileHandler(private val activity: Activity) : MethodCallHandler { class MediaFileHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val density = activity.resources.displayMetrics.density private val density = contextWrapper.resources.displayMetrics.density
private val regionFetcher = RegionFetcher(activity) private val regionFetcher = RegionFetcher(contextWrapper)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
@ -56,7 +59,7 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
return return
} }
provider.fetchSingle(activity, uri, mimeType, object : ImageOpCallback { provider.fetchSingle(contextWrapper, uri, mimeType, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri", throwable.message) override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri", throwable.message)
}) })
@ -80,7 +83,7 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
// convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter // convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter
ThumbnailFetcher( ThumbnailFetcher(
activity, contextWrapper,
uri, uri,
mimeType, mimeType,
dateModifiedSecs, dateModifiedSecs,
@ -113,14 +116,14 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
val regionRect = Rect(x, y, x + width, y + height) val regionRect = Rect(x, y, x + width, y + height)
when (mimeType) { when (mimeType) {
MimeTypes.SVG -> SvgRegionFetcher(activity).fetch( MimeTypes.SVG -> SvgRegionFetcher(contextWrapper).fetch(
uri = uri, uri = uri,
regionRect = regionRect, regionRect = regionRect,
imageWidth = imageWidth, imageWidth = imageWidth,
imageHeight = imageHeight, imageHeight = imageHeight,
result = result, result = result,
) )
MimeTypes.TIFF -> TiffRegionFetcher(activity).fetch( MimeTypes.TIFF -> TiffRegionFetcher(contextWrapper).fetch(
uri = uri, uri = uri,
page = pageId ?: 0, page = pageId ?: 0,
sampleSize = sampleSize, sampleSize = sampleSize,
@ -172,14 +175,14 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
} }
destinationDir = ensureTrailingSeparator(destinationDir) destinationDir = ensureTrailingSeparator(destinationDir)
provider.captureFrame(activity, desiredName, exifFields, bytes, destinationDir, nameConflictStrategy, object : ImageOpCallback { provider.captureFrame(contextWrapper, desiredName, exifFields, bytes, destinationDir, nameConflictStrategy, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("captureFrame-failure", "failed to capture frame for uri=$uri", throwable.message) override fun onFailure(throwable: Throwable) = result.error("captureFrame-failure", "failed to capture frame for uri=$uri", throwable.message)
}) })
} }
private fun clearSizedThumbnailDiskCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun clearSizedThumbnailDiskCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
Glide.get(activity).clearDiskCache() Glide.get(contextWrapper).clearDiskCache()
result.success(null) result.success(null)
} }

View file

@ -1,6 +1,6 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.app.Activity import android.content.ContextWrapper
import android.net.Uri import android.net.Uri
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.model.ExifOrientationOp import deckers.thibault.aves.model.ExifOrientationOp
@ -15,7 +15,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
@ -66,7 +66,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
return return
} }
provider.editOrientation(activity, path, uri, mimeType, op, object : ImageOpCallback { provider.editOrientation(contextWrapper, path, uri, mimeType, op, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("editOrientation-failure", "failed to change orientation for mimeType=$mimeType uri=$uri", throwable.message) override fun onFailure(throwable: Throwable) = result.error("editOrientation-failure", "failed to change orientation for mimeType=$mimeType uri=$uri", throwable.message)
}) })
@ -96,7 +96,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
return return
} }
provider.editDate(activity, path, uri, mimeType, dateMillis, shiftMinutes, fields, object : ImageOpCallback { provider.editDate(contextWrapper, path, uri, mimeType, dateMillis, shiftMinutes, fields, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("editDate-failure", "failed to edit date for mimeType=$mimeType uri=$uri", throwable.message) override fun onFailure(throwable: Throwable) = result.error("editDate-failure", "failed to edit date for mimeType=$mimeType uri=$uri", throwable.message)
}) })
@ -125,7 +125,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
return return
} }
provider.editMetadata(activity, path, uri, mimeType, metadata, autoCorrectTrailerOffset, callback = object : ImageOpCallback { provider.editMetadata(contextWrapper, path, uri, mimeType, metadata, autoCorrectTrailerOffset, callback = object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("editMetadata-failure", "failed to edit metadata for mimeType=$mimeType uri=$uri", throwable.message) override fun onFailure(throwable: Throwable) = result.error("editMetadata-failure", "failed to edit metadata for mimeType=$mimeType uri=$uri", throwable.message)
}) })
@ -152,7 +152,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
return return
} }
provider.removeTrailerVideo(activity, path, uri, mimeType, object : ImageOpCallback { provider.removeTrailerVideo(contextWrapper, path, uri, mimeType, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("removeTrailerVideo-failure", "failed to remove trailer video for mimeType=$mimeType uri=$uri", throwable.message) override fun onFailure(throwable: Throwable) = result.error("removeTrailerVideo-failure", "failed to remove trailer video for mimeType=$mimeType uri=$uri", throwable.message)
}) })
@ -180,7 +180,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
return return
} }
provider.removeMetadataTypes(activity, path, uri, mimeType, types.toSet(), object : ImageOpCallback { provider.removeMetadataTypes(contextWrapper, path, uri, mimeType, types.toSet(), object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("removeTypes-failure", "failed to remove metadata for mimeType=$mimeType uri=$uri", throwable.message) override fun onFailure(throwable: Throwable) = result.error("removeTypes-failure", "failed to remove metadata for mimeType=$mimeType uri=$uri", throwable.message)
}) })

View file

@ -1,9 +1,9 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.app.Activity
import android.app.WallpaperManager import android.app.WallpaperManager
import android.app.WallpaperManager.FLAG_LOCK import android.app.WallpaperManager.FLAG_LOCK
import android.app.WallpaperManager.FLAG_SYSTEM import android.app.WallpaperManager.FLAG_SYSTEM
import android.content.ContextWrapper
import android.os.Build import android.os.Build
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@ -14,7 +14,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class WallpaperHandler(private val activity: Activity) : MethodCallHandler { class WallpaperHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
@ -33,7 +33,7 @@ class WallpaperHandler(private val activity: Activity) : MethodCallHandler {
return return
} }
val manager = WallpaperManager.getInstance(activity) val manager = WallpaperManager.getInstance(contextWrapper)
val supported = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || manager.isWallpaperSupported val supported = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || manager.isWallpaperSupported
val allowed = Build.VERSION.SDK_INT < Build.VERSION_CODES.N || manager.isSetWallpaperAllowed val allowed = Build.VERSION.SDK_INT < Build.VERSION_CODES.N || manager.isSetWallpaperAllowed
if (!supported || !allowed) { if (!supported || !allowed) {

View file

@ -1,89 +0,0 @@
package deckers.thibault.aves.channel.calls
import android.app.Activity
import android.os.Build
import android.provider.Settings
import android.util.Log
import android.view.WindowManager
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
class WindowHandler(private val activity: Activity) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"keepScreenOn" -> safe(call, result, ::keepScreenOn)
"isRotationLocked" -> safe(call, result, ::isRotationLocked)
"requestOrientation" -> safe(call, result, ::requestOrientation)
"canSetCutoutMode" -> safe(call, result, ::canSetCutoutMode)
"setCutoutMode" -> safe(call, result, ::setCutoutMode)
else -> result.notImplemented()
}
}
private fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) {
val on = call.argument<Boolean>("on")
if (on == null) {
result.error("keepOn-args", "failed because of missing arguments", null)
return
}
val window = activity.window
val flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
if (on) {
window.addFlags(flag)
} else {
window.clearFlags(flag)
}
result.success(null)
}
private fun isRotationLocked(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
var locked = false
try {
locked = Settings.System.getInt(activity.contentResolver, Settings.System.ACCELEROMETER_ROTATION) == 0
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
}
result.success(locked)
}
private fun requestOrientation(call: MethodCall, result: MethodChannel.Result) {
val orientation = call.argument<Int>("orientation")
if (orientation == null) {
result.error("requestOrientation-args", "failed because of missing arguments", null)
return
}
activity.requestedOrientation = orientation
result.success(true)
}
private fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
}
private fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
val use = call.argument<Boolean>("use")
if (use == null) {
result.error("setCutoutMode-args", "failed because of missing arguments", null)
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val mode = if (use) {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
} else {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
activity.window.attributes.layoutInDisplayCutoutMode = mode
}
result.success(true)
}
companion object {
private val LOG_TAG = LogUtils.createTag<WindowHandler>()
const val CHANNEL = "deckers.thibault/aves/window"
}
}

View file

@ -0,0 +1,67 @@
package deckers.thibault.aves.channel.calls.window
import android.app.Activity
import android.os.Build
import android.view.WindowManager
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class ActivityWindowHandler(private val activity: Activity) : WindowHandler(activity) {
override fun isActivity(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(true)
}
override fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) {
val on = call.argument<Boolean>("on")
if (on == null) {
result.error("keepOn-args", "failed because of missing arguments", null)
return
}
val window = activity.window
val flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
if (on) {
window.addFlags(flag)
} else {
window.clearFlags(flag)
}
result.success(null)
}
override fun requestOrientation(call: MethodCall, result: MethodChannel.Result) {
val orientation = call.argument<Int>("orientation")
if (orientation == null) {
result.error("requestOrientation-args", "failed because of missing arguments", null)
return
}
activity.requestedOrientation = orientation
result.success(true)
}
override fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
}
override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
val use = call.argument<Boolean>("use")
if (use == null) {
result.error("setCutoutMode-args", "failed because of missing arguments", null)
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val mode = if (use) {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
} else {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
activity.window.attributes.layoutInDisplayCutoutMode = mode
}
result.success(true)
}
companion object {
private val LOG_TAG = LogUtils.createTag<ActivityWindowHandler>()
}
}

View file

@ -0,0 +1,27 @@
package deckers.thibault.aves.channel.calls.window
import android.app.Service
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class ServiceWindowHandler(service: Service) : WindowHandler(service) {
override fun isActivity(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
override fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) {
result.success(null)
}
override fun requestOrientation(call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
override fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
}

View file

@ -0,0 +1,48 @@
package deckers.thibault.aves.channel.calls.window
import android.content.ContextWrapper
import android.provider.Settings
import android.util.Log
import deckers.thibault.aves.channel.calls.Coresult
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
abstract class WindowHandler(private val contextWrapper: ContextWrapper) : MethodChannel.MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"isActivity" -> Coresult.safe(call, result, ::isActivity)
"keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn)
"isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked)
"requestOrientation" -> Coresult.safe(call, result, ::requestOrientation)
"canSetCutoutMode" -> Coresult.safe(call, result, ::canSetCutoutMode)
"setCutoutMode" -> Coresult.safe(call, result, ::setCutoutMode)
else -> result.notImplemented()
}
}
abstract fun isActivity(call: MethodCall, result: MethodChannel.Result)
abstract fun keepScreenOn(call: MethodCall, result: MethodChannel.Result)
private fun isRotationLocked(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
var locked = false
try {
locked = Settings.System.getInt(contextWrapper.contentResolver, Settings.System.ACCELEROMETER_ROTATION) == 0
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
}
result.success(locked)
}
abstract fun requestOrientation(call: MethodCall, result: MethodChannel.Result)
abstract fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result)
abstract fun setCutoutMode(call: MethodCall, result: MethodChannel.Result)
companion object {
private val LOG_TAG = LogUtils.createTag<WindowHandler>()
const val CHANNEL = "deckers.thibault/aves/window"
}
}

View file

@ -1,7 +1,7 @@
package deckers.thibault.aves.model.provider package deckers.thibault.aves.model.provider
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import deckers.thibault.aves.model.SourceEntry import deckers.thibault.aves.model.SourceEntry
@ -37,7 +37,7 @@ internal class FileImageProvider : ImageProvider() {
} }
} }
override suspend fun delete(activity: Activity, uri: Uri, path: String?, mimeType: String) { override suspend fun delete(contextWrapper: ContextWrapper, uri: Uri, path: String?, mimeType: String) {
val file = File(File(uri.path!!).path) val file = File(File(uri.path!!).path)
if (!file.exists()) return if (!file.exists()) return

View file

@ -2,6 +2,7 @@ package deckers.thibault.aves.model.provider
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
@ -45,7 +46,7 @@ abstract class ImageProvider {
callback.onFailure(UnsupportedOperationException("`fetchSingle` is not supported by this image provider")) callback.onFailure(UnsupportedOperationException("`fetchSingle` is not supported by this image provider"))
} }
open suspend fun delete(activity: Activity, uri: Uri, path: String?, mimeType: String) { open suspend fun delete(contextWrapper: ContextWrapper, uri: Uri, path: String?, mimeType: String) {
throw UnsupportedOperationException("`delete` is not supported by this image provider") throw UnsupportedOperationException("`delete` is not supported by this image provider")
} }
@ -151,7 +152,7 @@ abstract class ImageProvider {
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}" desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
} }
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
activity = activity, contextWrapper = activity,
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = exportMimeType, mimeType = exportMimeType,
@ -242,7 +243,7 @@ abstract class ImageProvider {
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
suspend fun captureFrame( suspend fun captureFrame(
activity: Activity, contextWrapper: ContextWrapper,
desiredNameWithoutExtension: String, desiredNameWithoutExtension: String,
exifFields: FieldMap, exifFields: FieldMap,
bytes: ByteArray, bytes: ByteArray,
@ -250,7 +251,7 @@ abstract class ImageProvider {
nameConflictStrategy: NameConflictStrategy, nameConflictStrategy: NameConflictStrategy,
callback: ImageOpCallback, callback: ImageOpCallback,
) { ) {
val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir) val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(contextWrapper, targetDir)
if (!File(targetDir).exists()) { if (!File(targetDir).exists()) {
callback.onFailure(Exception("failed to create directory at path=$targetDir")) callback.onFailure(Exception("failed to create directory at path=$targetDir"))
return return
@ -265,7 +266,7 @@ abstract class ImageProvider {
val captureMimeType = MimeTypes.JPEG val captureMimeType = MimeTypes.JPEG
val targetNameWithoutExtension = try { val targetNameWithoutExtension = try {
resolveTargetFileNameWithoutExtension( resolveTargetFileNameWithoutExtension(
activity = activity, contextWrapper = contextWrapper,
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = captureMimeType, mimeType = captureMimeType,
@ -287,7 +288,7 @@ abstract class ImageProvider {
// through a document URI, not a tree URI // through a document URI, not a tree URI
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
val targetTreeFile = targetDirDocFile.createFile(captureMimeType, targetNameWithoutExtension) val targetTreeFile = targetDirDocFile.createFile(captureMimeType, targetNameWithoutExtension)
val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri) val targetDocFile = DocumentFileCompat.fromSingleUri(contextWrapper, targetTreeFile.uri)
try { try {
if (exifFields.isEmpty()) { if (exifFields.isEmpty()) {
@ -355,7 +356,7 @@ abstract class ImageProvider {
val fileName = targetDocFile.name val fileName = targetDocFile.name
val targetFullPath = targetDir + fileName val targetFullPath = targetDir + fileName
val newFields = MediaStoreImageProvider().scanNewPath(activity, targetFullPath, captureMimeType) val newFields = MediaStoreImageProvider().scanNewPath(contextWrapper, targetFullPath, captureMimeType)
callback.onSuccess(newFields) callback.onSuccess(newFields)
} catch (e: Exception) { } catch (e: Exception) {
callback.onFailure(e) callback.onFailure(e)
@ -364,7 +365,7 @@ abstract class ImageProvider {
// returns available name to use, or `null` to skip it // returns available name to use, or `null` to skip it
suspend fun resolveTargetFileNameWithoutExtension( suspend fun resolveTargetFileNameWithoutExtension(
activity: Activity, contextWrapper: ContextWrapper,
dir: String, dir: String,
desiredNameWithoutExtension: String, desiredNameWithoutExtension: String,
mimeType: String, mimeType: String,
@ -386,9 +387,9 @@ abstract class ImageProvider {
if (targetFile.exists()) { if (targetFile.exists()) {
val path = targetFile.path val path = targetFile.path
MediaStoreImageProvider().apply { MediaStoreImageProvider().apply {
val uri = getContentUriForPath(activity, path) val uri = getContentUriForPath(contextWrapper, path)
uri ?: throw Exception("failed to find content URI for path=$path") uri ?: throw Exception("failed to find content URI for path=$path")
delete(activity, uri, path, mimeType) delete(contextWrapper, uri, path, mimeType)
} }
} }
desiredNameWithoutExtension desiredNameWithoutExtension

View file

@ -3,10 +3,7 @@ package deckers.thibault.aves.model.provider
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.RecoverableSecurityException import android.app.RecoverableSecurityException
import android.content.ContentResolver import android.content.*
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -280,7 +277,7 @@ class MediaStoreImageProvider : ImageProvider() {
private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType
// `uri` is a media URI, not a document URI // `uri` is a media URI, not a document URI
override suspend fun delete(activity: Activity, uri: Uri, path: String?, mimeType: String) { override suspend fun delete(contextWrapper: ContextWrapper, uri: Uri, path: String?, mimeType: String) {
path ?: throw Exception("failed to delete file because path is null") path ?: throw Exception("failed to delete file because path is null")
// the following situations are possible: // the following situations are possible:
@ -291,10 +288,10 @@ class MediaStoreImageProvider : ImageProvider() {
val fileExists = file.exists() val fileExists = file.exists()
if (fileExists) { if (fileExists) {
if (StorageUtils.canEditByFile(activity, path)) { if (StorageUtils.canEditByFile(contextWrapper, path)) {
if (hasEntry(activity, uri)) { if (hasEntry(contextWrapper, uri)) {
Log.d(LOG_TAG, "delete [permission:file, file exists, content exists] content at uri=$uri path=$path") Log.d(LOG_TAG, "delete [permission:file, file exists, content exists] content at uri=$uri path=$path")
activity.contentResolver.delete(uri, null, null) contextWrapper.contentResolver.delete(uri, null, null)
} }
// in theory, deleting via content resolver should remove the file on storage // in theory, deleting via content resolver should remove the file on storage
// in practice, the file may still be there afterwards // in practice, the file may still be there afterwards
@ -303,31 +300,31 @@ class MediaStoreImageProvider : ImageProvider() {
if (file.delete()) { if (file.delete()) {
// in theory, scanning an obsolete path should remove the entry from the Media Store // in theory, scanning an obsolete path should remove the entry from the Media Store
// in practice, the entry may still be there afterwards // in practice, the entry may still be there afterwards
scanObsoletePath(activity, uri, path, mimeType) scanObsoletePath(contextWrapper, uri, path, mimeType)
return return
} }
} else { } else {
return return
} }
} else if (!isMediaUriPermissionGranted(activity, uri, mimeType) } else if (!isMediaUriPermissionGranted(contextWrapper, uri, mimeType)
&& StorageUtils.requireAccessPermission(activity, path) && StorageUtils.requireAccessPermission(contextWrapper, path)
) { ) {
// the delete request may yield a `RecoverableSecurityException` when using scoped storage, // the delete request may yield a `RecoverableSecurityException` when using scoped storage,
// even if we have permissions on the tree document via SAF // even if we have permissions on the tree document via SAF
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && hasEntry(activity, uri)) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && hasEntry(contextWrapper, uri)) {
Log.d(LOG_TAG, "delete [permission:doc, file exists, content exists] content at uri=$uri path=$path") Log.d(LOG_TAG, "delete [permission:doc, file exists, content exists] content at uri=$uri path=$path")
activity.contentResolver.delete(uri, null, null) contextWrapper.contentResolver.delete(uri, null, null)
} }
// in theory, deleting via content resolver should remove the file on storage // in theory, deleting via content resolver should remove the file on storage
// in practice, the file may still be there afterwards // in practice, the file may still be there afterwards
if (file.exists()) { if (file.exists()) {
Log.d(LOG_TAG, "delete [permission:doc, file exists after content delete] document at uri=$uri path=$path") Log.d(LOG_TAG, "delete [permission:doc, file exists after content delete] document at uri=$uri path=$path")
val df = StorageUtils.getDocumentFile(activity, path, uri) val df = StorageUtils.getDocumentFile(contextWrapper, path, uri)
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
if (df != null && df.delete()) { if (df != null && df.delete()) {
scanObsoletePath(activity, uri, path, mimeType) scanObsoletePath(contextWrapper, uri, path, mimeType)
return return
} }
throw Exception("failed to delete document with df=$df") throw Exception("failed to delete document with df=$df")
@ -343,28 +340,28 @@ class MediaStoreImageProvider : ImageProvider() {
try { try {
Log.d(LOG_TAG, "delete [file exists=$fileExists] content at uri=$uri path=$path") Log.d(LOG_TAG, "delete [file exists=$fileExists] content at uri=$uri path=$path")
if (activity.contentResolver.delete(uri, null, null) > 0) return if (contextWrapper.contentResolver.delete(uri, null, null) > 0) return
if (hasEntry(activity, uri) || file.exists()) { if (hasEntry(contextWrapper, uri) || file.exists()) {
throw Exception("failed to delete row from content provider") throw Exception("failed to delete row from content provider")
} }
} catch (securityException: SecurityException) { } catch (securityException: SecurityException) {
// even if the app has access permission granted on the containing directory, // even if the app has access permission granted on the containing directory,
// the delete request may yield a `RecoverableSecurityException` on Android 10+ // the delete request may yield a `RecoverableSecurityException` on Android 10+
// when the underlying file no longer exists and this is an orphaned entry in the Media Store // when the underlying file no longer exists and this is an orphaned entry in the Media Store
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && contextWrapper is Activity) {
Log.w(LOG_TAG, "caught a security exception when attempting to delete uri=$uri", securityException) Log.w(LOG_TAG, "caught a security exception when attempting to delete uri=$uri", securityException)
val rse = securityException as? RecoverableSecurityException ?: throw securityException val rse = securityException as? RecoverableSecurityException ?: throw securityException
val intentSender = rse.userAction.actionIntent.intentSender val intentSender = rse.userAction.actionIntent.intentSender
// request user permission for this item // request user permission for this item
MainActivity.pendingScopedStoragePermissionCompleter = CompletableFuture<Boolean>() MainActivity.pendingScopedStoragePermissionCompleter = CompletableFuture<Boolean>()
activity.startIntentSenderForResult(intentSender, DELETE_SINGLE_PERMISSION_REQUEST, null, 0, 0, 0, null) contextWrapper.startIntentSenderForResult(intentSender, DELETE_SINGLE_PERMISSION_REQUEST, null, 0, 0, 0, null)
val granted = MainActivity.pendingScopedStoragePermissionCompleter!!.join() val granted = MainActivity.pendingScopedStoragePermissionCompleter!!.join()
MainActivity.pendingScopedStoragePermissionCompleter = null MainActivity.pendingScopedStoragePermissionCompleter = null
if (granted) { if (granted) {
delete(activity, uri, path, mimeType) delete(contextWrapper, uri, path, mimeType)
} else { } else {
throw Exception("failed to get delete permission") throw Exception("failed to get delete permission")
} }
@ -494,7 +491,7 @@ class MediaStoreImageProvider : ImageProvider() {
val desiredNameWithoutExtension = desiredName.substringBeforeLast(".") val desiredNameWithoutExtension = desiredName.substringBeforeLast(".")
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
activity = activity, contextWrapper = activity,
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType, mimeType = mimeType,
@ -641,7 +638,7 @@ class MediaStoreImageProvider : ImageProvider() {
val dir = oldFile.parent ?: return skippedFieldMap val dir = oldFile.parent ?: return skippedFieldMap
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
activity = activity, contextWrapper = activity,
dir = dir, dir = dir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType, mimeType = mimeType,

View file

@ -24,7 +24,6 @@ fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): Ap
@Suppress("deprecation") @Suppress("deprecation")
getApplicationInfo(packageName, flags) getApplicationInfo(packageName, flags)
} }
} }
fun PackageManager.queryIntentActivitiesCompat(intent: Intent, flags: Int): List<ResolveInfo> { fun PackageManager.queryIntentActivitiesCompat(intent: Intent, flags: Int): List<ResolveInfo> {

View file

@ -0,0 +1,2 @@
<dream xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="com.example.app/.ScreenSaverSettingsActivity" />

View file

@ -0,0 +1,142 @@
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/slideshow_actions.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/slideshow_interval.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves/widgets/viewer/entry_viewer_stack.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class SlideshowPage extends StatefulWidget {
static const routeName = '/collection/slideshow';
final CollectionLens collection;
const SlideshowPage({
super.key,
required this.collection,
});
@override
State<SlideshowPage> createState() => _SlideshowPageState();
}
class _SlideshowPageState extends State<SlideshowPage> {
late final CollectionLens _slideshowCollection;
late final ViewerController _viewerController;
@override
void initState() {
super.initState();
final originalCollection = widget.collection;
var entries = originalCollection.sortedEntries;
if (settings.slideshowVideoPlayback == SlideshowVideoPlayback.skip) {
entries = entries.where((entry) => !MimeFilter.video.test(entry)).toList();
}
if (settings.slideshowShuffle) {
entries.shuffle();
}
_slideshowCollection = CollectionLens(
source: originalCollection.source,
listenToSource: false,
fixedSort: true,
fixedSelection: entries,
);
_viewerController = ViewerController(
transition: settings.slideshowTransition,
repeat: settings.slideshowRepeat,
autopilot: true,
autopilotInterval: settings.slideshowInterval.getDuration(),
);
}
@override
void dispose() {
_viewerController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final entries = _slideshowCollection.sortedEntries;
return ListenableProvider<ValueNotifier<AppMode>>.value(
value: ValueNotifier(AppMode.slideshow),
child: MediaQueryDataProvider(
child: Scaffold(
body: entries.isEmpty
? EmptyContent(
icon: AIcons.image,
text: context.l10n.collectionEmptyImages,
alignment: Alignment.center,
)
: ViewStateConductorProvider(
child: VideoConductorProvider(
child: MultiPageConductorProvider(
child: NotificationListener<SlideshowActionNotification>(
onNotification: (notification) {
_onActionSelected(notification.action);
return true;
},
child: EntryViewerStack(
collection: _slideshowCollection,
initialEntry: entries.first,
viewerController: _viewerController,
),
),
),
),
),
),
),
);
}
void _onActionSelected(SlideshowAction action) {
switch (action) {
case SlideshowAction.resume:
_viewerController.autopilot = true;
break;
case SlideshowAction.showInCollection:
_showInCollection();
break;
}
}
void _showInCollection() {
final entry = _viewerController.entryNotifier.value;
if (entry == null) return;
final source = _slideshowCollection.source;
final album = entry.directory;
final uri = entry.uri;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
settings: const RouteSettings(name: CollectionPage.routeName),
builder: (context) => CollectionPage(
source: source,
filters: album != null ? {AlbumFilter(album, source.getAlbumDisplayName(context, album))} : null,
highlightTest: (entry) => entry.uri == uri,
),
),
(route) => false,
);
}
}
class SlideshowActionNotification extends Notification {
final SlideshowAction action;
SlideshowActionNotification(this.action);
}