diff --git a/CHANGELOG.md b/CHANGELOG.md index 1208f5714..e409b9f58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file. ## [Unreleased] -## [v1.4.7] - 2021-08-06 +## [v1.4.8] - 2021-08-08 ### Added - Map - Viewer: action to copy to clipboard @@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file. - auto album identification and naming - opening HEIC images from downloads content URI on Android R+ +## [v1.4.7] - 2021-08-06 [YANKED] + ## [v1.4.6] - 2021-07-22 ### Added - Albums / Countries / Tags: multiple selection diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt index 04afd10b8..ef30b56ab 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -4,6 +4,8 @@ import android.content.* import android.content.pm.ApplicationInfo import android.content.res.Configuration import android.net.Uri +import android.os.Handler +import android.os.Looper import android.util.Log import androidx.core.content.FileProvider import com.bumptech.glide.Glide @@ -141,13 +143,21 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { return } - val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager - if (clipboard != null) { - val clip = ClipData.newUri(context.contentResolver, label, getShareableUri(uri)) - clipboard.setPrimaryClip(clip) - result.success(true) - } else { - result.success(false) + // on older devices, `ClipboardManager` initialization must happen on the main thread + // (e.g. Samsung S7 with Android 8.0 / API 26, but not on Tab A 10.1 with Android 8.1 / API 27) + Handler(Looper.getMainLooper()).post { + try { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager + if (clipboard != null) { + val clip = ClipData.newUri(context.contentResolver, label, getShareableUri(uri)) + clipboard.setPrimaryClip(clip) + result.success(true) + } else { + result.success(false) + } + } catch (e: Exception) { + result.error("copyToClipboard-exception", "failed to set clip", e.message) + } } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt index 59dfd47d7..8e9ae3001 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt @@ -140,7 +140,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen error("streamImage-image-decode-null", "failed to get image from uri=$uri", null) } } catch (e: Exception) { - error("streamImage-image-decode-exception", "failed to get image from uri=$uri", toErrorDetails(e)) + error("streamImage-image-decode-exception", "failed to get image from uri=$uri model=$model", toErrorDetails(e)) } finally { Glide.with(activity).clear(target) } 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 84d51ecf3..0692d7deb 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 @@ -429,11 +429,15 @@ object StorageUtils { // so we build a typical `images` or `videos` content URI from the original content ID. fun getGlideSafeUri(uri: Uri, mimeType: String): Uri { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isMediaStoreContentUri(uri)) { - uri.tryParseId()?.let { id -> - return when { - isImage(mimeType) -> ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) - isVideo(mimeType) -> ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id) - else -> uri + // we cannot safely apply this to a file content URI, as it may point to a file not indexed + // by the Media Store (via `.nomedia`), and therefore has no matching image/video content URI + if (uri.path?.contains("/downloads/") == true) { + uri.tryParseId()?.let { id -> + return when { + isImage(mimeType) -> ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) + isVideo(mimeType) -> ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id) + else -> uri + } } } } diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index 7ae61645c..8c557d716 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -14,6 +14,7 @@ class AndroidFileUtils { Set storageVolumes = {}; Set _packages = {}; List _potentialAppDirs = []; + bool _initialized = false; AChangeNotifier appNameChangeNotifier = AChangeNotifier(); @@ -22,6 +23,8 @@ class AndroidFileUtils { AndroidFileUtils._private(); Future init() async { + if (_initialized) return; + separator = pContext.separator; storageVolumes = await storageService.getStorageVolumes(); primaryStorage = storageVolumes.firstWhereOrNull((volume) => volume.isPrimary)?.path ?? separator; @@ -32,12 +35,16 @@ class AndroidFileUtils { picturesPath = pContext.join(primaryStorage, 'Pictures'); // from Aves videoCapturesPath = pContext.join(dcimPath, 'Video Captures'); + + _initialized = true; } Future initAppNames() async { - _packages = await AndroidAppService.getPackages(); - _potentialAppDirs = _launcherPackages.expand((package) => package.potentialDirs).toList(); - appNameChangeNotifier.notifyListeners(); + if (_packages.isEmpty) { + _packages = await AndroidAppService.getPackages(); + _potentialAppDirs = _launcherPackages.expand((package) => package.potentialDirs).toList(); + appNameChangeNotifier.notifyListeners(); + } } bool isCameraPath(String path) => path.startsWith(dcimPath) && (path.endsWith('${separator}Camera') || path.endsWith('${separator}100ANDRO')); diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 86e15c017..d16d53954 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -161,6 +161,7 @@ class _HomePageState extends State { return SearchPageRoute( delegate: CollectionSearchDelegate( source: source, + canPop: false, initialQuery: _shortcutSearchQuery, ), ); diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index 65d8f6dbe..2e5e8c438 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -28,6 +28,7 @@ class CollectionSearchDelegate { final CollectionSource source; final CollectionLens? parentCollection; final ValueNotifier expandedSectionNotifier = ValueNotifier(null); + final bool canPop; static const searchHistoryCount = 10; static final typeFilters = [ @@ -45,13 +46,17 @@ class CollectionSearchDelegate { CollectionSearchDelegate({ required this.source, this.parentCollection, + this.canPop = true, String? initialQuery, }) { query = initialQuery ?? ''; } Widget buildLeading(BuildContext context) { - return Navigator.canPop(context) + // use a property instead of checking `Navigator.canPop(context)` + // because the navigator state changes as soon as we press back + // so the leading may mistakenly switch to the close button + return canPop ? IconButton( icon: AnimatedIcon( icon: AnimatedIcons.menu_arrow, diff --git a/pubspec.yaml b/pubspec.yaml index 38da7420e..e8c22c1d3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: aves description: A visual media gallery and metadata explorer app. repository: https://github.com/deckerst/aves -version: 1.4.7+51 +version: 1.4.8+52 publish_to: none environment: diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index 6a41abb90..2eff3dbb6 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,5 +1,5 @@ Thanks for using Aves! -v1.4.7: +v1.4.8: - map page - viewer action to copy to clipboard - integration with OS global search