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..91be344f3
--- /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/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
index f7f1a5b4b..8617fd4b5 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
@@ -5,7 +5,10 @@ import android.app.SearchManager
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 +16,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
@@ -66,11 +71,12 @@ class MainActivity : FlutterActivity() {
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, 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
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..ad949c2de
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverService.kt
@@ -0,0 +1,4 @@
+package deckers.thibault.aves
+
+class ScreenSaverService {
+}
\ 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..5f6549f7c
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverSettingsActivity.kt
@@ -0,0 +1,4 @@
+package deckers.thibault.aves
+
+class ScreenSaverSettingsActivity {
+}
\ 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..20f89511c 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
@@ -30,11 +35,12 @@ class WallpaperActivity : FlutterActivity() {
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(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
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..55925791a 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)
}
@@ -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/MediaFileHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFileHandler.kt
index 44c0cc405..0d4dc3c37 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFileHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFileHandler.kt
@@ -1,6 +1,6 @@
package deckers.thibault.aves.channel.calls
-import android.app.Activity
+import android.content.ContextWrapper
import android.graphics.Rect
import android.net.Uri
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.MethodChannel
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
-class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
+class MediaFileHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler {
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) {
when (call.method) {
@@ -56,7 +59,7 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
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 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
ThumbnailFetcher(
- activity,
+ contextWrapper,
uri,
mimeType,
dateModifiedSecs,
@@ -113,14 +116,14 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
val regionRect = Rect(x, y, x + width, y + height)
when (mimeType) {
- MimeTypes.SVG -> SvgRegionFetcher(activity).fetch(
+ MimeTypes.SVG -> SvgRegionFetcher(contextWrapper).fetch(
uri = uri,
regionRect = regionRect,
imageWidth = imageWidth,
imageHeight = imageHeight,
result = result,
)
- MimeTypes.TIFF -> TiffRegionFetcher(activity).fetch(
+ MimeTypes.TIFF -> TiffRegionFetcher(contextWrapper).fetch(
uri = uri,
page = pageId ?: 0,
sampleSize = sampleSize,
@@ -172,14 +175,14 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
}
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 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) {
- Glide.get(activity).clearDiskCache()
+ Glide.get(contextWrapper).clearDiskCache()
result.success(null)
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt
index 7b6d7ecfb..da5124483 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt
@@ -1,6 +1,6 @@
package deckers.thibault.aves.channel.calls
-import android.app.Activity
+import android.content.ContextWrapper
import android.net.Uri
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.model.ExifOrientationOp
@@ -15,7 +15,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
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)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
@@ -66,7 +66,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
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 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
}
- 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 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
}
- 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 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
}
- 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 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
}
- 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 onFailure(throwable: Throwable) = result.error("removeTypes-failure", "failed to remove metadata for mimeType=$mimeType uri=$uri", throwable.message)
})
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WallpaperHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WallpaperHandler.kt
index c4f1eab9d..2935a8c14 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WallpaperHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WallpaperHandler.kt
@@ -1,9 +1,9 @@
package deckers.thibault.aves.channel.calls
-import android.app.Activity
import android.app.WallpaperManager
import android.app.WallpaperManager.FLAG_LOCK
import android.app.WallpaperManager.FLAG_SYSTEM
+import android.content.ContextWrapper
import android.os.Build
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall
@@ -14,7 +14,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
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)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
@@ -33,7 +33,7 @@ class WallpaperHandler(private val activity: Activity) : MethodCallHandler {
return
}
- val manager = WallpaperManager.getInstance(activity)
+ val manager = WallpaperManager.getInstance(contextWrapper)
val supported = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || manager.isWallpaperSupported
val allowed = Build.VERSION.SDK_INT < Build.VERSION_CODES.N || manager.isSetWallpaperAllowed
if (!supported || !allowed) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WindowHandler.kt
deleted file mode 100644
index 528e70941..000000000
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WindowHandler.kt
+++ /dev/null
@@ -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("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("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("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()
- const val CHANNEL = "deckers.thibault/aves/window"
- }
-}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt
new file mode 100644
index 000000000..11905270e
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt
@@ -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("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("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("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()
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt
new file mode 100644
index 000000000..55794ade4
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt
new file mode 100644
index 000000000..184d2398d
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt
@@ -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()
+ const val CHANNEL = "deckers.thibault/aves/window"
+ }
+}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt
index 7a6cfae94..66d6f1035 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt
@@ -1,7 +1,7 @@
package deckers.thibault.aves.model.provider
-import android.app.Activity
import android.content.Context
+import android.content.ContextWrapper
import android.net.Uri
import android.util.Log
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)
if (!file.exists()) return
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
index 12ce8e4a5..679e96b9a 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
@@ -2,6 +2,7 @@ package deckers.thibault.aves.model.provider
import android.app.Activity
import android.content.Context
+import android.content.ContextWrapper
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
@@ -45,7 +46,7 @@ abstract class ImageProvider {
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")
}
@@ -151,7 +152,7 @@ abstract class ImageProvider {
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
}
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
- activity = activity,
+ contextWrapper = activity,
dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = exportMimeType,
@@ -242,7 +243,7 @@ abstract class ImageProvider {
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun captureFrame(
- activity: Activity,
+ contextWrapper: ContextWrapper,
desiredNameWithoutExtension: String,
exifFields: FieldMap,
bytes: ByteArray,
@@ -250,7 +251,7 @@ abstract class ImageProvider {
nameConflictStrategy: NameConflictStrategy,
callback: ImageOpCallback,
) {
- val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir)
+ val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(contextWrapper, targetDir)
if (!File(targetDir).exists()) {
callback.onFailure(Exception("failed to create directory at path=$targetDir"))
return
@@ -265,7 +266,7 @@ abstract class ImageProvider {
val captureMimeType = MimeTypes.JPEG
val targetNameWithoutExtension = try {
resolveTargetFileNameWithoutExtension(
- activity = activity,
+ contextWrapper = contextWrapper,
dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = captureMimeType,
@@ -287,7 +288,7 @@ abstract class ImageProvider {
// through a document URI, not a tree URI
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
val targetTreeFile = targetDirDocFile.createFile(captureMimeType, targetNameWithoutExtension)
- val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
+ val targetDocFile = DocumentFileCompat.fromSingleUri(contextWrapper, targetTreeFile.uri)
try {
if (exifFields.isEmpty()) {
@@ -355,7 +356,7 @@ abstract class ImageProvider {
val fileName = targetDocFile.name
val targetFullPath = targetDir + fileName
- val newFields = MediaStoreImageProvider().scanNewPath(activity, targetFullPath, captureMimeType)
+ val newFields = MediaStoreImageProvider().scanNewPath(contextWrapper, targetFullPath, captureMimeType)
callback.onSuccess(newFields)
} catch (e: Exception) {
callback.onFailure(e)
@@ -364,7 +365,7 @@ abstract class ImageProvider {
// returns available name to use, or `null` to skip it
suspend fun resolveTargetFileNameWithoutExtension(
- activity: Activity,
+ contextWrapper: ContextWrapper,
dir: String,
desiredNameWithoutExtension: String,
mimeType: String,
@@ -386,9 +387,9 @@ abstract class ImageProvider {
if (targetFile.exists()) {
val path = targetFile.path
MediaStoreImageProvider().apply {
- val uri = getContentUriForPath(activity, path)
+ val uri = getContentUriForPath(contextWrapper, path)
uri ?: throw Exception("failed to find content URI for path=$path")
- delete(activity, uri, path, mimeType)
+ delete(contextWrapper, uri, path, mimeType)
}
}
desiredNameWithoutExtension
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
index c8f38a340..e3e9ff736 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
@@ -3,10 +3,7 @@ package deckers.thibault.aves.model.provider
import android.annotation.SuppressLint
import android.app.Activity
import android.app.RecoverableSecurityException
-import android.content.ContentResolver
-import android.content.ContentUris
-import android.content.ContentValues
-import android.content.Context
+import android.content.*
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
@@ -280,7 +277,7 @@ class MediaStoreImageProvider : ImageProvider() {
private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType
// `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")
// the following situations are possible:
@@ -291,10 +288,10 @@ class MediaStoreImageProvider : ImageProvider() {
val fileExists = file.exists()
if (fileExists) {
- if (StorageUtils.canEditByFile(activity, path)) {
- if (hasEntry(activity, uri)) {
+ if (StorageUtils.canEditByFile(contextWrapper, path)) {
+ if (hasEntry(contextWrapper, uri)) {
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 practice, the file may still be there afterwards
@@ -303,31 +300,31 @@ class MediaStoreImageProvider : ImageProvider() {
if (file.delete()) {
// in theory, scanning an obsolete path should remove the entry from the Media Store
// in practice, the entry may still be there afterwards
- scanObsoletePath(activity, uri, path, mimeType)
+ scanObsoletePath(contextWrapper, uri, path, mimeType)
return
}
} else {
return
}
- } else if (!isMediaUriPermissionGranted(activity, uri, mimeType)
- && StorageUtils.requireAccessPermission(activity, path)
+ } else if (!isMediaUriPermissionGranted(contextWrapper, uri, mimeType)
+ && StorageUtils.requireAccessPermission(contextWrapper, path)
) {
// the delete request may yield a `RecoverableSecurityException` when using scoped storage,
// 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")
- 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 practice, the file may still be there afterwards
if (file.exists()) {
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")
if (df != null && df.delete()) {
- scanObsoletePath(activity, uri, path, mimeType)
+ scanObsoletePath(contextWrapper, uri, path, mimeType)
return
}
throw Exception("failed to delete document with df=$df")
@@ -343,28 +340,28 @@ class MediaStoreImageProvider : ImageProvider() {
try {
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")
}
} catch (securityException: SecurityException) {
// even if the app has access permission granted on the containing directory,
// 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
- 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)
val rse = securityException as? RecoverableSecurityException ?: throw securityException
val intentSender = rse.userAction.actionIntent.intentSender
// request user permission for this item
MainActivity.pendingScopedStoragePermissionCompleter = CompletableFuture()
- 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()
MainActivity.pendingScopedStoragePermissionCompleter = null
if (granted) {
- delete(activity, uri, path, mimeType)
+ delete(contextWrapper, uri, path, mimeType)
} else {
throw Exception("failed to get delete permission")
}
@@ -494,7 +491,7 @@ class MediaStoreImageProvider : ImageProvider() {
val desiredNameWithoutExtension = desiredName.substringBeforeLast(".")
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
- activity = activity,
+ contextWrapper = activity,
dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType,
@@ -641,7 +638,7 @@ class MediaStoreImageProvider : ImageProvider() {
val dir = oldFile.parent ?: return skippedFieldMap
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
- activity = activity,
+ contextWrapper = activity,
dir = dir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType,
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt
index cd7dffcfc..9be2fbe67 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt
@@ -24,7 +24,6 @@ fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): Ap
@Suppress("deprecation")
getApplicationInfo(packageName, flags)
}
-
}
fun PackageManager.queryIntentActivitiesCompat(intent: Intent, flags: Int): List {
diff --git a/android/app/src/main/res/xml/screen_saver.xml b/android/app/src/main/res/xml/screen_saver.xml
new file mode 100644
index 000000000..e69de29bb
diff --git a/android/app/src/profile/res/xml/screen_saver.xml b/android/app/src/profile/res/xml/screen_saver.xml
new file mode 100644
index 000000000..91be344f3
--- /dev/null
+++ b/android/app/src/profile/res/xml/screen_saver.xml
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/lib/widgets/settings/screen_saver_settings_page.dart b/lib/widgets/settings/screen_saver_settings_page.dart
new file mode 100644
index 000000000..e69de29bb
diff --git a/lib/widgets/viewer/screen_saver_page.dart b/lib/widgets/viewer/screen_saver_page.dart
new file mode 100644
index 000000000..c21b238bf
--- /dev/null
+++ b/lib/widgets/viewer/screen_saver_page.dart
@@ -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 createState() => _SlideshowPageState();
+}
+
+class _SlideshowPageState extends State {
+ 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>.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(
+ 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);
+}