diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3462b4f2b..e1fe5cc9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
### Added
+- Vaults
- Viewer: overlay details expand/collapse on tap
- Viewer: export actions available as quick actions
- Slideshow: added settings quick action
@@ -14,6 +15,7 @@ All notable changes to this project will be documented in this file.
### Changed
+- disabling the recycle bin will delete forever items in it
- remember pin status of albums becoming empty
- upgraded Flutter to stable v3.7.3
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 051231057..b974a5ee6 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -18,6 +18,9 @@ if (localPropertiesFile.exists()) {
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
def flutterVersionName = localProperties.getProperty('flutter.versionName')
def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
// Keys
@@ -181,10 +184,11 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.core:core-ktx:1.9.0'
- implementation 'androidx.exifinterface:exifinterface:1.3.5'
+ implementation 'androidx.exifinterface:exifinterface:1.3.6'
implementation 'androidx.lifecycle:lifecycle-process:2.5.1'
implementation 'androidx.media:media:1.6.0'
implementation 'androidx.multidex:multidex:2.0.1'
+ implementation 'androidx.security:security-crypto:1.1.0-alpha04'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.commonsware.cwac:document:0.5.0'
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5d8bc1ce9..7553cb54f 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -19,7 +19,7 @@ This change eventually prevents building the app with Flutter v3.3.3.
android:required="false" />
@@ -32,28 +32,35 @@ This change eventually prevents building the app with Flutter v3.3.3.
android:maxSdkVersion="29"
tools:ignore="ScopedStorage" />
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
-
-
+
+
@@ -75,12 +82,14 @@ This change eventually prevents building the app with Flutter v3.3.3.
android:allowBackup="true"
android:appCategory="image"
android:banner="@drawable/banner"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ android:fullBackupContent="@xml/full_backup_content"
android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
- tools:targetApi="o">
+ tools:targetApi="s">
when (call.method) {
"configure" -> {
@@ -42,9 +47,9 @@ class HomeWidgetSettingsActivity : MainActivity() {
}
private fun saveWidget() {
- val appWidgetManager = AppWidgetManager.getInstance(context)
+ val appWidgetManager = AppWidgetManager.getInstance(this)
val widgetInfo = appWidgetManager.getAppWidgetOptions(appWidgetId)
- HomeWidgetProvider().onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, widgetInfo)
+ HomeWidgetProvider().onAppWidgetOptionsChanged(this, appWidgetManager, appWidgetId, widgetInfo)
val intent = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
setResult(RESULT_OK, intent)
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 0f1975177..cfd60ec3e 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
@@ -25,14 +25,15 @@ import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
import deckers.thibault.aves.utils.FlutterUtils.isSoftwareRenderingRequired
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat
-import io.flutter.embedding.android.FlutterActivity
+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.MethodChannel
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
-open class MainActivity : FlutterActivity() {
+open class MainActivity : FlutterFragmentActivity() {
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler
private lateinit var intentStreamHandler: IntentStreamHandler
@@ -68,8 +69,12 @@ open class MainActivity : FlutterActivity() {
// .build()
// )
super.onCreate(savedInstanceState)
+ }
- val messenger = flutterEngine!!.dartExecutor
+ override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+ super.configureFlutterEngine(flutterEngine)
+
+ val messenger = flutterEngine.dartExecutor
// notification: platform -> dart
analysisStreamHandler = AnalysisStreamHandler().apply {
@@ -99,6 +104,7 @@ open class MainActivity : FlutterActivity() {
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler)
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
+ MethodChannel(messenger, SecurityHandler.CHANNEL).setMethodCallHandler(SecurityHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
// - need ContextWrapper
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
@@ -193,6 +199,7 @@ open class MainActivity : FlutterActivity() {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(requestCode, resultCode, data)
DELETE_SINGLE_PERMISSION_REQUEST,
@@ -255,7 +262,7 @@ open class MainActivity : FlutterActivity() {
Intent.ACTION_VIEW, Intent.ACTION_SEND, "com.android.camera.action.REVIEW" -> {
(intent.data ?: intent.getParcelableExtraCompat(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional
- val type = intent.type ?: intent.resolveType(context)
+ val type = intent.type ?: intent.resolveType(this)
return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW,
INTENT_DATA_KEY_MIME_TYPE to type,
@@ -325,7 +332,7 @@ open class MainActivity : FlutterActivity() {
private fun submitPickedItems(call: MethodCall) {
val pickedUris = call.argument>("uris")
if (pickedUris != null && pickedUris.isNotEmpty()) {
- val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(context, Uri.parse(uriString)) }
+ val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) }
val intent = Intent().apply {
val firstUri = toUri(pickedUris.first())
if (pickedUris.size == 1) {
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 a4285c3cf..202e73090 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt
@@ -1,6 +1,5 @@
package deckers.thibault.aves
-import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
@@ -18,11 +17,12 @@ import deckers.thibault.aves.utils.FlutterUtils
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat
-import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.android.FlutterFragmentActivity
+import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
-class WallpaperActivity : FlutterActivity() {
+class WallpaperActivity : FlutterFragmentActivity() {
private lateinit var intentDataMap: MutableMap
override fun onCreate(savedInstanceState: Bundle?) {
@@ -36,8 +36,33 @@ class WallpaperActivity : FlutterActivity() {
Log.i(LOG_TAG, "onCreate intent extras=$it")
}
intentDataMap = extractIntentData(intent)
+ }
- initChannels(this)
+ override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+ super.configureFlutterEngine(flutterEngine)
+ val messenger = flutterEngine.dartExecutor
+
+ // dart -> platform -> dart
+ // - need Context
+ MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
+ MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
+ MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
+ MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
+ 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() {
@@ -54,32 +79,6 @@ class WallpaperActivity : FlutterActivity() {
}
}
- private fun initChannels(activity: Activity) {
- val messenger = flutterEngine!!.dartExecutor
-
- // dart -> platform -> dart
- // - need Context
- MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(activity))
- MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(activity))
- MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(activity))
- MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(activity))
- MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(activity))
- MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(activity))
- // - need ContextWrapper
- MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(activity))
- MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(activity))
- // - need Activity
- MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(activity))
-
- // result streaming: dart -> platform ->->-> dart
- // - need Context
- StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(activity, args) }
-
- // intent handling
- // detail fetch: dart -> platform
- MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result -> onMethodCall(call, result) }
- }
-
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getIntentData" -> {
@@ -94,7 +93,7 @@ class WallpaperActivity : FlutterActivity() {
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
(intent.data ?: intent.getParcelableExtraCompat(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional
- val type = intent.type ?: intent.resolveType(context)
+ val type = intent.type ?: intent.resolveType(this)
return hashMapOf(
MainActivity.INTENT_DATA_KEY_ACTION to MainActivity.INTENT_ACTION_SET_WALLPAPER,
MainActivity.INTENT_DATA_KEY_MIME_TYPE to type,
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt
index d64a01db9..f35eaaa39 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt
@@ -21,7 +21,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
when (call.method) {
"canManageMedia" -> safe(call, result, ::canManageMedia)
"getCapabilities" -> safe(call, result, ::getCapabilities)
- "getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone)
+ "getDefaultTimeZoneRawOffsetMillis" -> safe(call, result, ::getDefaultTimeZoneRawOffsetMillis)
"getLocales" -> safe(call, result, ::getLocales)
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
"isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled)
@@ -44,6 +44,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
"canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.M),
"canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S),
"canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N),
+ "canUseCrypto" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
"hasGeocoder" to Geocoder.isPresent(),
"isDynamicColorAvailable" to (sdkInt >= Build.VERSION_CODES.S),
"showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O),
@@ -52,8 +53,8 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
)
}
- private fun getDefaultTimeZone(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
- result.success(TimeZone.getDefault().id)
+ private fun getDefaultTimeZoneRawOffsetMillis(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
+ result.success(TimeZone.getDefault().rawOffset)
}
private fun getLocales(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/SecurityHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/SecurityHandler.kt
new file mode 100644
index 000000000..05a6ba398
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/SecurityHandler.kt
@@ -0,0 +1,79 @@
+package deckers.thibault.aves.channel.calls
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKey
+import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler
+
+class SecurityHandler(private val context: Context) : MethodCallHandler {
+ private var sharedPreferences: SharedPreferences? = null
+
+ override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
+ when (call.method) {
+ "writeValue" -> safe(call, result, ::writeValue)
+ "readValue" -> safe(call, result, ::readValue)
+ else -> result.notImplemented()
+ }
+ }
+
+ private fun getStore(): SharedPreferences {
+ if (sharedPreferences == null) {
+ val mainKey = MasterKey.Builder(context)
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ .build()
+ sharedPreferences = EncryptedSharedPreferences.create(
+ context,
+ FILENAME,
+ mainKey,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ )
+ }
+ return sharedPreferences!!
+ }
+
+ private fun writeValue(call: MethodCall, result: MethodChannel.Result) {
+ val key = call.argument("key")
+ val value = call.argument("value")
+ if (key == null) {
+ result.error("writeValue-args", "missing arguments", null)
+ return
+ }
+
+ with(getStore().edit()) {
+ when (value) {
+ is Boolean -> putBoolean(key, value)
+ is Float -> putFloat(key, value)
+ is Int -> putInt(key, value)
+ is Long -> putLong(key, value)
+ is String -> putString(key, value)
+ null -> remove(key)
+ else -> {
+ result.error("writeValue-type", "unsupported type for value=$value", null)
+ return
+ }
+ }
+ apply()
+ }
+ result.success(true)
+ }
+
+ private fun readValue(call: MethodCall, result: MethodChannel.Result) {
+ val key = call.argument("key")
+ if (key == null) {
+ result.error("readValue-args", "missing arguments", null)
+ return
+ }
+
+ result.success(getStore().all[key])
+ }
+
+ companion object {
+ const val CHANNEL = "deckers.thibault/aves/security"
+ const val FILENAME = "secret_shared_prefs"
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
index 435705285..1dcc4bd32 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
@@ -8,6 +8,7 @@ import androidx.core.os.EnvironmentCompat
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.PermissionManager
+import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.StorageUtils.getPrimaryVolumePath
import deckers.thibault.aves.utils.StorageUtils.getVolumePaths
import io.flutter.plugin.common.MethodCall
@@ -25,6 +26,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getStorageVolumes" -> ioScope.launch { safe(call, result, ::getStorageVolumes) }
+ "getVaultRoot" -> ioScope.launch { safe(call, result, ::getVaultRoot) }
"getFreeSpace" -> ioScope.launch { safe(call, result, ::getFreeSpace) }
"getGrantedDirectories" -> ioScope.launch { safe(call, result, ::getGrantedDirectories) }
"getInaccessibleDirectories" -> ioScope.launch { safe(call, result, ::getInaccessibleDirectories) }
@@ -88,6 +90,10 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
result.success(volumes)
}
+ private fun getVaultRoot(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
+ result.success(StorageUtils.getVaultRoot(context))
+ }
+
private fun getFreeSpace(call: MethodCall, result: MethodChannel.Result) {
val path = call.argument("path")
if (path == null) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt
index 3086708ad..752633094 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt
@@ -109,7 +109,7 @@ class ThumbnailFetcher internal constructor(
} else {
@Suppress("deprecation")
var bitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null)
- // from Android 10, returned thumbnail is already rotated according to EXIF orientation
+ // from Android 10 (API 29), returned thumbnail is already rotated according to EXIF orientation
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null) {
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
}
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
index 258682b00..1f6590dd6 100644
--- 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
@@ -12,7 +12,7 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
result.success(true)
}
- override fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) {
+ private fun setWindowFlag(call: MethodCall, result: MethodChannel.Result, flag: Int) {
val on = call.argument("on")
if (on == null) {
result.error("keepOn-args", "missing arguments", null)
@@ -20,8 +20,6 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
}
val window = activity.window
- val flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
-
val old = (window.attributes.flags and flag) != 0
if (old != on) {
if (on) {
@@ -33,6 +31,14 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
result.success(null)
}
+ override fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) {
+ setWindowFlag(call, result, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ }
+
+ override fun secureScreen(call: MethodCall, result: MethodChannel.Result) {
+ setWindowFlag(call, result, WindowManager.LayoutParams.FLAG_SECURE)
+ }
+
override fun requestOrientation(call: MethodCall, result: MethodChannel.Result) {
val orientation = call.argument("orientation")
if (orientation == null) {
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
index 46d1e43b8..3a50fd324 100644
--- 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
@@ -13,6 +13,10 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) {
result.success(null)
}
+ override fun secureScreen(call: MethodCall, result: MethodChannel.Result) {
+ result.success(null)
+ }
+
override fun requestOrientation(call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
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
index 0a6f41249..492c6deeb 100644
--- 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
@@ -13,6 +13,7 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
when (call.method) {
"isActivity" -> Coresult.safe(call, result, ::isActivity)
"keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn)
+ "secureScreen" -> Coresult.safe(call, result, ::secureScreen)
"isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked)
"requestOrientation" -> Coresult.safe(call, result, ::requestOrientation)
"isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware)
@@ -25,6 +26,8 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
abstract fun keepScreenOn(call: MethodCall, result: MethodChannel.Result)
+ abstract fun secureScreen(call: MethodCall, result: MethodChannel.Result)
+
private fun isRotationLocked(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
var locked = false
try {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt
index 0b7ced26e..f618c0df1 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt
@@ -170,6 +170,11 @@ object XMP {
}
}
// creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device`
+
+ // TODO TLAD [mp4] `IsoFile` init may fail if a skipped box has a `org.mp4parser.boxes.iso14496.part12.MetaBox` as parent,
+ // because `MetaBox.parse()` changes the argument `dataSource` to a `RewindableReadableByteChannel`,
+ // so it is no longer a seekable `FileChannel`, which is a requirement to skip boxes.
+
IsoFile(channel, boxParser).use { isoFile ->
isoFile.processBoxes(UserBox::class.java, true) { box, _ ->
val boxSize = box.size
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt
index 9d5749561..e858c03e4 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt
@@ -33,6 +33,7 @@ import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import java.io.IOException
class SourceEntry {
+ private val origin: Int
val uri: Uri // content or file URI
var path: String? = null // best effort to get local path
private val sourceMimeType: String
@@ -48,12 +49,14 @@ class SourceEntry {
private var foundExif: Boolean = false
- constructor(uri: Uri, sourceMimeType: String) {
+ constructor(origin: Int, uri: Uri, sourceMimeType: String) {
+ this.origin = origin
this.uri = uri
this.sourceMimeType = sourceMimeType
}
constructor(map: FieldMap) {
+ origin = map["origin"] as Int
uri = Uri.parse(map["uri"] as String)
path = map["path"] as String?
sourceMimeType = map["sourceMimeType"] as String
@@ -77,6 +80,7 @@ class SourceEntry {
fun toMap(): FieldMap {
return hashMapOf(
+ "origin" to origin,
"uri" to uri.toString(),
"path" to path,
"sourceMimeType" to sourceMimeType,
@@ -249,13 +253,15 @@ class SourceEntry {
private fun fillByTiffDecode(context: Context) {
try {
- val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd() ?: return
- val options = TiffBitmapFactory.Options().apply {
- inJustDecodeBounds = true
+ context.contentResolver.openFileDescriptor(uri, "r")?.use { pfd ->
+ val fd = pfd.detachFd()
+ val options = TiffBitmapFactory.Options().apply {
+ inJustDecodeBounds = true
+ }
+ TiffBitmapFactory.decodeFileDescriptor(fd, options)
+ width = options.outWidth
+ height = options.outHeight
}
- TiffBitmapFactory.decodeFileDescriptor(fd, options)
- width = options.outWidth
- height = options.outHeight
} catch (e: Exception) {
// ignore
}
@@ -267,5 +273,11 @@ class SourceEntry {
is Int -> o.toLong()
else -> o as? Long
}
+
+ // should match `EntryOrigins` on the Dart side
+ const val ORIGIN_MEDIA_STORE_CONTENT = 0
+ const val ORIGIN_UNKNOWN_CONTENT = 1
+ const val ORIGIN_FILE = 2
+ const val ORIGIN_VAULT = 3
}
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt
index 7ff773d21..368035721 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt
@@ -44,6 +44,7 @@ internal class ContentImageProvider : ImageProvider() {
}
val fields: FieldMap = hashMapOf(
+ "origin" to SourceEntry.ORIGIN_UNKNOWN_CONTENT,
"uri" to uri.toString(),
"sourceMimeType" to mimeType,
)
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 3c3a624d5..6a1b0725e 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
@@ -4,6 +4,7 @@ import android.content.Context
import android.content.ContextWrapper
import android.net.Uri
import android.util.Log
+import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.LogUtils
import java.io.File
@@ -15,7 +16,7 @@ internal class FileImageProvider : ImageProvider() {
return
}
- val entry = SourceEntry(uri, sourceMimeType)
+ val entry = SourceEntry(SourceEntry.ORIGIN_FILE, uri, sourceMimeType)
val path = uri.path
if (path != null) {
@@ -52,6 +53,19 @@ internal class FileImageProvider : ImageProvider() {
throw Exception("failed to delete entry with uri=$uri path=$path")
}
+ override fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: FieldMap, callback: ImageOpCallback) {
+ try {
+ val file = File(path)
+ if (file.exists()) {
+ newFields["dateModifiedSecs"] = file.lastModified() / 1000
+ newFields["sizeBytes"] = file.length()
+ }
+ callback.onSuccess(newFields)
+ } catch (e: SecurityException) {
+ callback.onFailure(e)
+ }
+ }
+
companion object {
private val LOG_TAG = LogUtils.createTag()
}
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 440b45edf..5e568be50 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
@@ -9,6 +9,7 @@ import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Log
+import androidx.core.net.toUri
import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.MainActivity.Companion.DELETE_SINGLE_PERMISSION_REQUEST
@@ -220,6 +221,7 @@ class MediaStoreImageProvider : ImageProvider() {
Log.w(LOG_TAG, "failed to make entry from uri=$itemUri because of null MIME type")
} else {
var entryMap: FieldMap = hashMapOf(
+ "origin" to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
"uri" to itemUri.toString(),
"path" to cursor.getString(pathColumn),
"sourceMimeType" to mimeType,
@@ -350,7 +352,7 @@ class MediaStoreImageProvider : ImageProvider() {
}
} 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
+ // the delete request may yield a `RecoverableSecurityException` on API >=29
// 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 && contextWrapper is Activity) {
Log.w(LOG_TAG, "caught a security exception when attempting to delete uri=$uri", securityException)
@@ -387,10 +389,12 @@ class MediaStoreImageProvider : ImageProvider() {
val entries = kv.value
val toBin = targetDir == StorageUtils.TRASH_PATH_PLACEHOLDER
+ val toVault = StorageUtils.isInVault(activity, targetDir)
+ val toAppDir = toBin || toVault
var effectiveTargetDir: String? = null
var targetDirDocFile: DocumentFileCompat? = null
- if (!toBin) {
+ if (!toAppDir) {
effectiveTargetDir = targetDir
targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir)
if (!File(targetDir).exists()) {
@@ -438,13 +442,20 @@ class MediaStoreImageProvider : ImageProvider() {
// - there is no documentation regarding support for usage with removable storage
// - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage
try {
- if (toBin) {
- val trashDir = StorageUtils.trashDirFor(activity, sourcePath)
- if (trashDir != null) {
- effectiveTargetDir = ensureTrailingSeparator(trashDir.path)
- targetDirDocFile = DocumentFileCompat.fromFile(trashDir)
+ val appDir = when {
+ toBin -> StorageUtils.trashDirFor(activity, sourcePath)
+ toVault -> File(targetDir)
+ else -> null
+ }
+ if (appDir != null) {
+ effectiveTargetDir = ensureTrailingSeparator(appDir.path)
+ targetDirDocFile = DocumentFileCompat.fromFile(appDir)
+
+ if (toVault) {
+ appDir.mkdirs()
}
}
+
if (effectiveTargetDir != null) {
val newFields = if (isCancelledOp()) skippedFieldMap else {
val sourceFile = File(sourcePath)
@@ -463,6 +474,7 @@ class MediaStoreImageProvider : ImageProvider() {
mimeType = mimeType,
copy = copy,
toBin = toBin,
+ toVault = toVault,
)
}
}
@@ -489,6 +501,7 @@ class MediaStoreImageProvider : ImageProvider() {
mimeType: String,
copy: Boolean,
toBin: Boolean,
+ toVault: Boolean,
): FieldMap {
val sourcePath = sourceFile.path
val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) }
@@ -532,13 +545,21 @@ class MediaStoreImageProvider : ImageProvider() {
Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e)
}
}
- if (toBin) {
- return hashMapOf(
+ return if (toBin) {
+ hashMapOf(
"trashed" to true,
"trashPath" to targetPath,
)
+ } else if (toVault) {
+ hashMapOf(
+ "uri" to File(targetPath).toUri().toString(),
+ "contentId" to null,
+ "path" to targetPath,
+ "origin" to SourceEntry.ORIGIN_VAULT,
+ )
+ } else {
+ scanNewPath(activity, targetPath, mimeType)
}
- return scanNewPath(activity, targetPath, mimeType)
}
// `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry
@@ -920,7 +941,7 @@ class MediaStoreImageProvider : ImageProvider() {
private val VIDEO_PROJECTION = arrayOf(
*BASE_PROJECTION,
MediaColumns.DURATION,
- // `ORIENTATION` was only available for images before Android 10
+ // `ORIENTATION` was only available for images before Android 10 (API 29)
*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) arrayOf(
MediaStore.MediaColumns.ORIENTATION,
) else emptyArray()
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt
index df4ea5f75..1215d7ab7 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt
@@ -1,5 +1,6 @@
package deckers.thibault.aves.utils
+import android.webkit.MimeTypeMap
import androidx.exifinterface.media.ExifInterface
object MimeTypes {
@@ -153,47 +154,11 @@ object MimeTypes {
// among other refs:
// - https://android.googlesource.com/platform/external/mime-support/+/refs/heads/master/mime.types
fun extensionFor(mimeType: String): String? = when (mimeType) {
- ARW -> ".arw"
AVI, AVI_VND -> ".avi"
- AVIF -> ".avif"
- BMP -> ".bmp"
- CR2 -> ".cr2"
- CRW -> ".crw"
- DCR -> ".dcr"
- DJVU -> ".djvu"
- DNG -> ".dng"
- ERF -> ".erf"
- GIF -> ".gif"
HEIC, HEIF -> ".heif"
- ICO -> ".ico"
- JPEG -> ".jpg"
- K25 -> ".k25"
- KDC -> ".kdc"
- MKV -> ".mkv"
- MOV -> ".mov"
MP2T, MP2TS -> ".m2ts"
- MP4 -> ".mp4"
- MRW -> ".mrw"
- NEF -> ".nef"
- NRW -> ".nrw"
- OGV -> ".ogv"
- ORF -> ".orf"
- PEF -> ".pef"
- PNG -> ".png"
PSD_VND, PSD_X -> ".psd"
- RAF -> ".raf"
- RAW -> ".raw"
- RW2 -> ".rw2"
- SR2 -> ".sr2"
- SRF -> ".srf"
- SRW -> ".srw"
- SVG -> ".svg"
- TIFF -> ".tiff"
- WBMP -> ".wbmp"
- WEBM -> ".webm"
- WEBP -> ".webp"
- X3F -> ".x3f"
- else -> null
+ else -> MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" }
}
val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt
index 08355a5de..2bad51da4 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt
@@ -119,7 +119,7 @@ object PermissionManager {
dirSet.add("")
}
} else {
- // request volume root until Android 10
+ // request volume root until Android 10 (API 29)
dirSet.add("")
}
dirsPerVolume[volumePath] = dirSet
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
index 962c28ce7..457a36e0e 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
@@ -45,23 +45,23 @@ object StorageUtils {
const val TRASH_PATH_PLACEHOLDER = "#trash"
private fun isAppFile(context: Context, path: String): Boolean {
- val filesDirs = context.getExternalFilesDirs(null).filterNotNull()
- return filesDirs.any { path.startsWith(it.path) }
+ val dirs = context.getExternalFilesDirs(null).filterNotNull()
+ return dirs.any { path.startsWith(it.path) }
}
private fun appExternalFilesDirFor(context: Context, path: String): File? {
- val filesDirs = context.getExternalFilesDirs(null).filterNotNull()
+ val dirs = context.getExternalFilesDirs(null).filterNotNull()
val volumePath = getVolumePath(context, path)
- return volumePath?.let { filesDirs.firstOrNull { it.startsWith(volumePath) } } ?: filesDirs.firstOrNull()
+ return volumePath?.let { dirs.firstOrNull { it.startsWith(volumePath) } } ?: dirs.firstOrNull()
}
fun trashDirFor(context: Context, path: String): File? {
- val filesDir = appExternalFilesDirFor(context, path)
- if (filesDir == null) {
+ val externalFilesDir = appExternalFilesDirFor(context, path)
+ if (externalFilesDir == null) {
Log.e(LOG_TAG, "failed to find external files dir for path=$path")
return null
}
- val trashDir = File(filesDir, "trash")
+ val trashDir = File(externalFilesDir, "trash")
if (!trashDir.exists() && !trashDir.mkdirs()) {
Log.e(LOG_TAG, "failed to create directories at path=$trashDir")
return null
@@ -69,6 +69,10 @@ object StorageUtils {
return trashDir
}
+ fun getVaultRoot(context: Context) = ensureTrailingSeparator(File(context.filesDir, "vault").path)
+
+ fun isInVault(context: Context, path: String) = path.startsWith(getVaultRoot(context))
+
/**
* Volume paths
*/
@@ -545,7 +549,7 @@ object StorageUtils {
}
// As of Glide v4.12.0, a special loader `QMediaStoreUriLoader` is automatically used
- // to work around a bug from Android 10 where metadata redaction corrupts HEIC images.
+ // to work around a bug from Android 10 (API 29) where metadata redaction corrupts HEIC images.
// This loader relies on `MediaStore.setRequireOriginal` but this yields a `SecurityException`
// for some non image/video content URIs (e.g. `downloads`, `file`)
fun getGlideSafeUri(context: Context, uri: Uri, mimeType: String, sizeBytes: Long? = null): Uri {
diff --git a/android/app/src/main/res/xml/data_extraction_rules.xml b/android/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 000000000..9f7329404
--- /dev/null
+++ b/android/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/xml/full_backup_content.xml b/android/app/src/main/res/xml/full_backup_content.xml
new file mode 100644
index 000000000..5e5ca09e7
--- /dev/null
+++ b/android/app/src/main/res/xml/full_backup_content.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index e0e447a3d..3a528264c 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
diff --git a/lib/app_mode.dart b/lib/app_mode.dart
index de3cd6455..925c3d967 100644
--- a/lib/app_mode.dart
+++ b/lib/app_mode.dart
@@ -26,6 +26,11 @@ extension ExtraAppMode on AppMode {
bool get canSelectFilter => this == AppMode.main;
+ bool get canCreateFilter => {
+ AppMode.main,
+ AppMode.pickFilterInternal,
+ }.contains(this);
+
bool get isPickingMedia => {
AppMode.pickSingleMediaExternal,
AppMode.pickMultipleMediaExternal,
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index cabefa0be..7104192c1 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -78,11 +78,14 @@
"chipActionFilterOut": "Filter out",
"chipActionFilterIn": "Filter in",
"chipActionHide": "Hide",
+ "chipActionLock": "Lock",
"chipActionPin": "Pin to top",
"chipActionUnpin": "Unpin from top",
"chipActionRename": "Rename",
"chipActionSetCover": "Set cover",
"chipActionCreateAlbum": "Create album",
+ "chipActionCreateVault": "Create vault",
+ "chipActionConfigureVault": "Configure vault",
"entryActionCopyToClipboard": "Copy to clipboard",
"entryActionDelete": "Delete",
@@ -158,6 +161,16 @@
"filterMimeImageLabel": "Image",
"filterMimeVideoLabel": "Video",
+ "accessibilityAnimationsRemove": "Prevent screen effects",
+ "accessibilityAnimationsKeep": "Keep screen effects",
+
+ "albumTierNew": "New",
+ "albumTierPinned": "Pinned",
+ "albumTierSpecial": "Common",
+ "albumTierApps": "Apps",
+ "albumTierVaults": "Vaults",
+ "albumTierRegular": "Others",
+
"coordinateFormatDms": "DMS",
"coordinateFormatDecimal": "Decimal degrees",
"coordinateDms": "{coordinate} {direction}",
@@ -178,17 +191,13 @@
"coordinateDmsEast": "E",
"coordinateDmsWest": "W",
- "unitSystemMetric": "Metric",
- "unitSystemImperial": "Imperial",
+ "displayRefreshRatePreferHighest": "Highest rate",
+ "displayRefreshRatePreferLowest": "Lowest rate",
- "videoLoopModeNever": "Never",
- "videoLoopModeShortOnly": "Short videos only",
- "videoLoopModeAlways": "Always",
-
- "videoControlsPlay": "Play",
- "videoControlsPlaySeek": "Play & seek backward/forward",
- "videoControlsPlayOutside": "Open with other player",
- "videoControlsNone": "None",
+ "keepScreenOnNever": "Never",
+ "keepScreenOnVideoPlayback": "During video playback",
+ "keepScreenOnViewerOnly": "Viewer page only",
+ "keepScreenOnAlways": "Always",
"mapStyleGoogleNormal": "Google Maps",
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
@@ -203,28 +212,32 @@
"nameConflictStrategyReplace": "Replace",
"nameConflictStrategySkip": "Skip",
- "keepScreenOnNever": "Never",
- "keepScreenOnVideoPlayback": "During video playback",
- "keepScreenOnViewerOnly": "Viewer page only",
- "keepScreenOnAlways": "Always",
-
- "accessibilityAnimationsRemove": "Prevent screen effects",
- "accessibilityAnimationsKeep": "Keep screen effects",
-
- "displayRefreshRatePreferHighest": "Highest rate",
- "displayRefreshRatePreferLowest": "Lowest rate",
-
"subtitlePositionTop": "Top",
"subtitlePositionBottom": "Bottom",
- "videoPlaybackSkip": "Skip",
- "videoPlaybackMuted": "Play muted",
- "videoPlaybackWithSound": "Play with sound",
-
"themeBrightnessLight": "Light",
"themeBrightnessDark": "Dark",
"themeBrightnessBlack": "Black",
+ "unitSystemMetric": "Metric",
+ "unitSystemImperial": "Imperial",
+
+ "vaultLockTypePin": "Pin",
+ "vaultLockTypePassword": "Password",
+
+ "videoControlsPlay": "Play",
+ "videoControlsPlaySeek": "Play & seek backward/forward",
+ "videoControlsPlayOutside": "Open with other player",
+ "videoControlsNone": "None",
+
+ "videoLoopModeNever": "Never",
+ "videoLoopModeShortOnly": "Short videos only",
+ "videoLoopModeAlways": "Always",
+
+ "videoPlaybackSkip": "Skip",
+ "videoPlaybackMuted": "Play muted",
+ "videoPlaybackWithSound": "Play with sound",
+
"viewerTransitionSlide": "Slide",
"viewerTransitionParallax": "Parallax",
"viewerTransitionFade": "Fade",
@@ -242,12 +255,6 @@
"widgetOpenPageCollection": "Open collection",
"widgetOpenPageViewer": "Open viewer",
- "albumTierNew": "New",
- "albumTierPinned": "Pinned",
- "albumTierSpecial": "Common",
- "albumTierApps": "Apps",
- "albumTierRegular": "Others",
-
"storageVolumeDescriptionFallbackPrimary": "Internal storage",
"storageVolumeDescriptionFallbackNonPrimary": "SD card",
"rootDirectoryDescription": "root directory",
@@ -367,6 +374,23 @@
"newAlbumDialogNameLabelAlreadyExistsHelper": "Directory already exists",
"newAlbumDialogStorageLabel": "Storage:",
+ "newVaultWarningDialogMessage": "Items in vaults are only available to this app and no others.\n\nIf you uninstall this app, or clear this app data, you will lose all these items.",
+ "newVaultDialogTitle": "New Vault",
+ "configureVaultDialogTitle": "Configure Vault",
+ "vaultDialogLockModeWhenScreenOff": "Lock when screen turns off",
+ "vaultDialogLockTypeLabel": "Lock type",
+
+ "pinDialogEnter": "Enter pin",
+ "pinDialogConfirm": "Confirm pin",
+
+ "passwordDialogEnter": "Enter password",
+ "passwordDialogConfirm": "Confirm password",
+
+ "authenticateToConfigureVault": "Authenticate to configure vault",
+ "authenticateToUnlockVault": "Authenticate to unlock vault",
+
+ "vaultBinUsageDialogMessage": "Some vaults are using the recycle bin.",
+
"renameAlbumDialogLabel": "New name",
"renameAlbumDialogLabelAlreadyExistsHelper": "Directory already exists",
@@ -635,7 +659,6 @@
"albumPageTitle": "Albums",
"albumEmpty": "No albums",
- "createAlbumTooltip": "Create album",
"createAlbumButtonLabel": "CREATE",
"newFilterBanner": "new",
@@ -688,6 +711,7 @@
"settingsConfirmationBeforeMoveToBinItems": "Ask before moving items to the recycle bin",
"settingsConfirmationBeforeMoveUndatedItems": "Ask before moving undated items",
"settingsConfirmationAfterMoveToBinItems": "Show message after moving items to the recycle bin",
+ "settingsConfirmationVaultDataLoss": "Show vault data loss warning",
"settingsNavigationDrawerTile": "Navigation menu",
"settingsNavigationDrawerEditorPageTitle": "Navigation Menu",
@@ -791,6 +815,7 @@
"settingsSaveSearchHistory": "Save search history",
"settingsEnableBin": "Use recycle bin",
"settingsEnableBinSubtitle": "Keep deleted items for 30 days",
+ "settingsDisablingBinWarningDialogMessage": "Items in the recycle bin will be deleted forever.",
"settingsAllowMediaManagement": "Allow media management",
"settingsHiddenItemsTile": "Hidden items",
diff --git a/lib/model/actions/chip_actions.dart b/lib/model/actions/chip_actions.dart
index 28b596579..bb7dc079a 100644
--- a/lib/model/actions/chip_actions.dart
+++ b/lib/model/actions/chip_actions.dart
@@ -8,6 +8,7 @@ enum ChipAction {
goToTagPage,
reverse,
hide,
+ lockVault,
}
extension ExtraChipAction on ChipAction {
@@ -24,6 +25,8 @@ extension ExtraChipAction on ChipAction {
return context.l10n.chipActionFilterOut;
case ChipAction.hide:
return context.l10n.chipActionHide;
+ case ChipAction.lockVault:
+ return context.l10n.chipActionLock;
}
}
@@ -41,6 +44,8 @@ extension ExtraChipAction on ChipAction {
return AIcons.reverse;
case ChipAction.hide:
return AIcons.hide;
+ case ChipAction.lockVault:
+ return AIcons.vaultLock;
}
}
}
diff --git a/lib/model/actions/chip_set_actions.dart b/lib/model/actions/chip_set_actions.dart
index a1e919801..7e2efa418 100644
--- a/lib/model/actions/chip_set_actions.dart
+++ b/lib/model/actions/chip_set_actions.dart
@@ -12,6 +12,7 @@ enum ChipSetAction {
search,
toggleTitleSearch,
createAlbum,
+ createVault,
// browsing or selecting
map,
slideshow,
@@ -21,9 +22,11 @@ enum ChipSetAction {
hide,
pin,
unpin,
+ lockVault,
// selecting (single filter)
rename,
setCover,
+ configureVault,
}
class ChipSetActions {
@@ -34,15 +37,20 @@ class ChipSetActions {
ChipSetAction.selectNone,
];
+ // `null` items are converted to dividers
static const browsing = [
ChipSetAction.search,
ChipSetAction.toggleTitleSearch,
- ChipSetAction.createAlbum,
+ null,
ChipSetAction.map,
ChipSetAction.slideshow,
ChipSetAction.stats,
+ null,
+ ChipSetAction.createAlbum,
+ ChipSetAction.createVault,
];
+ // `null` items are converted to dividers
static const selection = [
ChipSetAction.setCover,
ChipSetAction.pin,
@@ -50,9 +58,13 @@ class ChipSetActions {
ChipSetAction.delete,
ChipSetAction.rename,
ChipSetAction.hide,
+ null,
ChipSetAction.map,
ChipSetAction.slideshow,
ChipSetAction.stats,
+ null,
+ ChipSetAction.configureVault,
+ ChipSetAction.lockVault,
];
}
@@ -76,6 +88,8 @@ extension ExtraChipSetAction on ChipSetAction {
return context.l10n.collectionActionShowTitleSearch;
case ChipSetAction.createAlbum:
return context.l10n.chipActionCreateAlbum;
+ case ChipSetAction.createVault:
+ return context.l10n.chipActionCreateVault;
// browsing or selecting
case ChipSetAction.map:
return context.l10n.menuActionMap;
@@ -92,11 +106,15 @@ extension ExtraChipSetAction on ChipSetAction {
return context.l10n.chipActionPin;
case ChipSetAction.unpin:
return context.l10n.chipActionUnpin;
+ case ChipSetAction.lockVault:
+ return context.l10n.chipActionLock;
// selecting (single filter)
case ChipSetAction.rename:
return context.l10n.chipActionRename;
case ChipSetAction.setCover:
return context.l10n.chipActionSetCover;
+ case ChipSetAction.configureVault:
+ return context.l10n.chipActionConfigureVault;
}
}
@@ -121,6 +139,8 @@ extension ExtraChipSetAction on ChipSetAction {
return AIcons.filter;
case ChipSetAction.createAlbum:
return AIcons.add;
+ case ChipSetAction.createVault:
+ return AIcons.vaultAdd;
// browsing or selecting
case ChipSetAction.map:
return AIcons.map;
@@ -137,11 +157,15 @@ extension ExtraChipSetAction on ChipSetAction {
return AIcons.pin;
case ChipSetAction.unpin:
return AIcons.unpin;
+ case ChipSetAction.lockVault:
+ return AIcons.vaultLock;
// selecting (single filter)
case ChipSetAction.rename:
return AIcons.name;
case ChipSetAction.setCover:
return AIcons.setCover;
+ case ChipSetAction.configureVault:
+ return AIcons.vaultConfigure;
}
}
}
diff --git a/lib/model/covers.dart b/lib/model/covers.dart
index dfd07b8d3..601a07bd5 100644
--- a/lib/model/covers.dart
+++ b/lib/model/covers.dart
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
+import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart';
@@ -38,6 +39,8 @@ class Covers {
Set get all => Set.unmodifiable(_rows);
Tuple3? of(CollectionFilter filter) {
+ if (filter is AlbumFilter && vaults.isLocked(filter.album)) return null;
+
final row = _rows.firstWhereOrNull((row) => row.filter == filter);
return row != null ? Tuple3(row.entryId, row.packageName, row.color) : null;
}
diff --git a/lib/model/db/db_metadata.dart b/lib/model/db/db_metadata.dart
index cc23707bf..bf4f677ae 100644
--- a/lib/model/db/db_metadata.dart
+++ b/lib/model/db/db_metadata.dart
@@ -5,6 +5,7 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/trash.dart';
+import 'package:aves/model/vaults/details.dart';
import 'package:aves/model/video_playback.dart';
abstract class MetadataDb {
@@ -16,17 +17,17 @@ abstract class MetadataDb {
Future reset();
- Future removeIds(Iterable ids, {Set? dataTypes});
+ Future removeIds(Set ids, {Set? dataTypes});
// entries
Future clearEntries();
- Future> loadEntries({String? directory});
+ Future> loadEntries({int? origin, String? directory});
- Future> loadEntriesById(Iterable ids);
+ Future> loadEntriesById(Set ids);
- Future saveEntries(Iterable entries);
+ Future saveEntries(Set entries);
Future updateEntry(int id, AvesEntry entry);
@@ -44,7 +45,7 @@ abstract class MetadataDb {
Future> loadCatalogMetadata();
- Future> loadCatalogMetadataById(Iterable ids);
+ Future> loadCatalogMetadataById(Set ids);
Future saveCatalogMetadata(Set metadataEntries);
@@ -56,12 +57,24 @@ abstract class MetadataDb {
Future> loadAddresses();
- Future> loadAddressesById(Iterable ids);
+ Future> loadAddressesById(Set ids);
Future saveAddresses(Set addresses);
Future updateAddress(int id, AddressDetails? address);
+ // vaults
+
+ Future clearVaults();
+
+ Future> loadAllVaults();
+
+ Future addVaults(Set rows);
+
+ Future updateVault(String oldName, VaultDetails row);
+
+ Future removeVaults(Set rows);
+
// trash
Future clearTrashDetails();
@@ -76,11 +89,11 @@ abstract class MetadataDb {
Future> loadAllFavourites();
- Future addFavourites(Iterable rows);
+ Future addFavourites(Set rows);
Future updateFavouriteId(int id, FavouriteRow row);
- Future removeFavourites(Iterable rows);
+ Future removeFavourites(Set rows);
// covers
@@ -88,7 +101,7 @@ abstract class MetadataDb {
Future> loadAllCovers();
- Future addCovers(Iterable rows);
+ Future addCovers(Set rows);
Future updateCoverEntryId(int id, CoverRow row);
@@ -104,5 +117,5 @@ abstract class MetadataDb {
Future addVideoPlayback(Set rows);
- Future removeVideoPlayback(Iterable ids);
+ Future removeVideoPlayback(Set ids);
}
diff --git a/lib/model/db/db_metadata_sqflite.dart b/lib/model/db/db_metadata_sqflite.dart
index 2e0e3e11d..9bc6a8885 100644
--- a/lib/model/db/db_metadata_sqflite.dart
+++ b/lib/model/db/db_metadata_sqflite.dart
@@ -9,6 +9,7 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/trash.dart';
+import 'package:aves/model/vaults/details.dart';
import 'package:aves/model/video_playback.dart';
import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart';
@@ -26,6 +27,7 @@ class SqfliteMetadataDb implements MetadataDb {
static const addressTable = 'address';
static const favouriteTable = 'favourites';
static const coverTable = 'covers';
+ static const vaultTable = 'vaults';
static const trashTable = 'trash';
static const videoPlaybackTable = 'videoPlayback';
@@ -55,6 +57,7 @@ class SqfliteMetadataDb implements MetadataDb {
', sourceDateTakenMillis INTEGER'
', durationMillis INTEGER'
', trashed INTEGER DEFAULT 0'
+ ', origin INTEGER DEFAULT 0'
')');
await db.execute('CREATE TABLE $dateTakenTable('
'id INTEGER PRIMARY KEY'
@@ -89,6 +92,12 @@ class SqfliteMetadataDb implements MetadataDb {
', packageName TEXT'
', color INTEGER'
')');
+ await db.execute('CREATE TABLE $vaultTable('
+ 'name TEXT PRIMARY KEY'
+ ', autoLock INTEGER'
+ ', useBin INTEGER'
+ ', lockType TEXT'
+ ')');
await db.execute('CREATE TABLE $trashTable('
'id INTEGER PRIMARY KEY'
', path TEXT'
@@ -100,7 +109,7 @@ class SqfliteMetadataDb implements MetadataDb {
')');
},
onUpgrade: MetadataDbUpgrader.upgradeDb,
- version: 10,
+ version: 11,
);
final maxIdRows = await _db.rawQuery('SELECT max(id) AS maxId FROM $entryTable');
@@ -122,7 +131,7 @@ class SqfliteMetadataDb implements MetadataDb {
}
@override
- Future removeIds(Iterable ids, {Set? dataTypes}) async {
+ Future removeIds(Set ids, {Set? dataTypes}) async {
if (ids.isEmpty) return;
final _dataTypes = dataTypes ?? EntryDataType.values.toSet();
@@ -162,15 +171,23 @@ class SqfliteMetadataDb implements MetadataDb {
}
@override
- Future> loadEntries({String? directory}) async {
+ Future> loadEntries({int? origin, String? directory}) async {
+ String? where;
+ final whereArgs =