Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
deckerst committed Jul 26, 2020
2 parents 0c0c8cc + 19314cc commit ce0808f
Show file tree
Hide file tree
Showing 84 changed files with 1,020 additions and 811 deletions.
12 changes: 7 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Release an APK and an App Bundle on tagging
name: Release on tag

on:
push:
Expand Down Expand Up @@ -33,9 +33,11 @@ jobs:
working-directory: ${{ github.workspace }}/scripts
run: ./update_flutter_version.sh

# `flutter test` fails if test directory is missing
#- name: Run the unit tests.
# run: flutter test
- name: Static analysis.
run: flutter analyze

- name: Unit tests.
run: flutter test

- name: Build signed artifacts.
# `KEY_JKS` should contain the result of:
Expand Down Expand Up @@ -86,4 +88,4 @@ jobs:
packageName: deckers.thibault.aves
releaseFile: app-release.aab
track: beta
whatsNewDirectory: whatsnew
whatsNewDirectory: whatsnew
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
![Aves logo][] [<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" alt='Get it on Google Play' width="200">](https://play.google.com/store/apps/details?id=deckers.thibault.aves&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1)
![Version badge][Version badge]
![Build badge][Build badge]

<br />
<img src="https://raw.githubusercontent.com/deckerst/aves/develop/assets/aves_logo.svg" alt='Aves logo' width="200" />

[<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" alt='Get it on Google Play' width="200">](https://play.google.com/store/apps/details?id=deckers.thibault.aves&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1)

Aves is a gallery and metadata explorer app. It is built for Android, with Flutter.

<img src="https://raw.githubusercontent.com/deckerst/aves/develop/extra/play/screenshots%20v1.0.0/S10/1-S10-collection.jpg" alt='Collection screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves/develop/extra/play/screenshots%20v1.0.0/S10/2-S10-image.jpg" alt='Image screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves/develop/extra/play/screenshots%20v1.0.0/S10/5-S10-stats.jpg" alt='Stats screenshot' height="400" />

## Features

- support raster images: JPEG, PNG, GIF, WEBP, BMP, WBMP, HEIC (from Android Pie)
Expand All @@ -13,7 +21,7 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt
- favorites
- statistics
- handle intents to view or pick images
- support Android API 24 ~ 29 (Nougat ~ Android 10)
- support Android API 24 ~ 30 (Nougat ~ R)

## Roadmap

Expand All @@ -25,7 +33,6 @@ If time permits, I intend to eventually add these:
- gesture: long press and drag thumbnails to select multiple items
- gesture: double tap and drag image to zoom in/out (aka quick scale, one finger zoom)
- support: burst groups
- support: Android R
- subsampling/tiling

## Known Issues
Expand All @@ -48,4 +55,5 @@ If time permits, I intend to eventually add these:

Create a file named `<app dir>/android/key.properties`. It should contain a reference to a keystore for app signing, and other necessary credentials. See `<app dir>/android/key_template.properties` for the expected keys.

[Aves logo]: https://github.com/deckerst/aves/blob/master/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
[Version badge]: https://img.shields.io/github/v/release/deckerst/aves?include_prereleases&sort=semver
[Build badge]: https://img.shields.io/github/workflow/status/deckerst/aves/Release%20on%20tag
29 changes: 25 additions & 4 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,30 @@ analyzer:
exclude:
- lib/generated_plugin_registrant.dart

# strong-mode:
# implicit-casts: false
# implicit-dynamic: false

linter:
rules:
- always_declare_return_types
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
# from 'effective dart', excluded
avoid_function_literals_in_foreach_calls: false # benefit?
lines_longer_than_80_chars: false # nope
avoid_classes_with_only_static_members: false # too strict

# from 'effective dart', undecided
prefer_relative_imports: false # check IDE support (auto import, file move)
public_member_api_docs: false # maybe?

# from 'effective dart', included
avoid_types_on_closure_parameters: true
constant_identifier_names: true
prefer_function_declarations_over_variables: true
prefer_interpolation_to_compose_strings: true
unnecessary_brace_in_string_interps: true
unnecessary_lambdas: true

# misc
prefer_const_constructors: false # too noisy
prefer_const_constructors_in_immutables: true
prefer_const_declarations: true
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ if (keystorePropertiesFile.exists()) {
}

android {
compileSdkVersion 29 // latest (or latest-1 if the sources of latest SDK are unavailable)
compileSdkVersion 30 // latest (or latest-1 if the sources of latest SDK are unavailable)

lintOptions {
disable 'InvalidPackage'
Expand All @@ -54,7 +54,7 @@ android {
// but Flutter (as of v1.17.3) fails to run in release mode when using Gradle plugin 4.0:
// https://github.com/flutter/flutter/issues/58247
minSdkVersion 24
targetSdkVersion 29 // same as compileSdkVersion
targetSdkVersion 30 // same as compileSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
manifestPlaceholders = [googleApiKey:keystoreProperties['googleApiKey']]
Expand Down
8 changes: 8 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@
-->
<uses-permission android:name="android.permission.WAKE_LOCK" />

<!-- from Android R, we should define <queries> to make other apps visible to this app -->
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
</queries>

<application
android:name="io.flutter.app.FlutterApplication"
android:icon="@mipmap/ic_launcher"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ private void handleIntent(Intent intent) {
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == PermissionManager.VOLUME_ROOT_PERMISSION_REQUEST_CODE) {
if (resultCode != RESULT_OK || data.getData() == null) {
PermissionManager.onPermissionResult(this, requestCode, false, null);
PermissionManager.onPermissionResult(requestCode, null);
return;
}

Expand All @@ -113,7 +113,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);

// resume pending action
PermissionManager.onPermissionResult(this, requestCode, true, treeUri);
PermissionManager.onPermissionResult(requestCode, treeUri);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package deckers.thibault.aves.channel.calls;

import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
Expand All @@ -22,10 +22,10 @@
public class StorageHandler implements MethodChannel.MethodCallHandler {
public static final String CHANNEL = "deckers.thibault/aves/storage";

private Activity activity;
private Context context;

public StorageHandler(Activity activity) {
this.activity = activity;
public StorageHandler(Context context) {
this.context = context;
}

@Override
Expand All @@ -42,12 +42,12 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
result.success(volumes);
break;
}
case "requireVolumeAccessDialog": {
String path = call.argument("path");
if (path == null) {
result.success(true);
case "getInaccessibleDirectories": {
List<String> dirPaths = call.argument("dirPaths");
if (dirPaths == null) {
result.error("getInaccessibleDirectories-args", "failed because of missing arguments", null);
} else {
result.success(PermissionManager.requireVolumeAccessDialog(activity, path));
result.success(PermissionManager.getInaccessibleDirectories(context, dirPaths));
}
break;
}
Expand All @@ -60,15 +60,15 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
@RequiresApi(api = Build.VERSION_CODES.N)
private List<Map<String, Object>> getStorageVolumes() {
List<Map<String, Object>> volumes = new ArrayList<>();
StorageManager sm = activity.getSystemService(StorageManager.class);
StorageManager sm = context.getSystemService(StorageManager.class);
if (sm != null) {
for (String volumePath : StorageUtils.getVolumePaths(activity)) {
for (String volumePath : StorageUtils.getVolumePaths(context)) {
try {
StorageVolume volume = sm.getStorageVolume(new File(volumePath));
if (volume != null) {
Map<String, Object> volumeMap = new HashMap<>();
volumeMap.put("path", volumePath);
volumeMap.put("description", volume.getDescription(activity));
volumeMap.put("description", volume.getDescription(context));
volumeMap.put("isPrimary", volume.isPrimary());
volumeMap.put("isRemovable", volume.isRemovable());
volumeMap.put("isEmulated", volume.isEmulated());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package deckers.thibault.aves.channel.streams;

import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
Expand All @@ -25,16 +25,16 @@ public class ImageOpStreamHandler implements EventChannel.StreamHandler {

public static final String CHANNEL = "deckers.thibault/aves/imageopstream";

private Activity activity;
private Context context;
private EventChannel.EventSink eventSink;
private Handler handler;
private Map<String, Object> argMap;
private List<Map<String, Object>> entryMapList;
private String op;

@SuppressWarnings("unchecked")
public ImageOpStreamHandler(Activity activity, Object arguments) {
this.activity = activity;
public ImageOpStreamHandler(Context context, Object arguments) {
this.context = context;
if (arguments instanceof Map) {
argMap = (Map<String, Object>) arguments;
this.op = (String) argMap.get("op");
Expand Down Expand Up @@ -100,7 +100,7 @@ private void move() {
}

List<AvesImageEntry> entries = entryMapList.stream().map(AvesImageEntry::new).collect(Collectors.toList());
provider.moveMultiple(activity, copy, destinationDir, entries, new ImageProvider.ImageOpCallback() {
provider.moveMultiple(context, copy, destinationDir, entries, new ImageProvider.ImageOpCallback() {
@Override
public void onSuccess(Map<String, Object> fields) {
success(fields);
Expand Down Expand Up @@ -138,7 +138,7 @@ private void delete() {
put("uri", uriString);
}};
try {
provider.delete(activity, path, uri).get();
provider.delete(context, path, uri).get();
result.put("success", true);
} catch (ExecutionException | InterruptedException e) {
Log.w(LOG_TAG, "failed to delete entry with path=" + path, e);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package deckers.thibault.aves.channel.streams;

import android.app.Activity;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;

Expand All @@ -12,14 +12,14 @@
public class MediaStoreStreamHandler implements EventChannel.StreamHandler {
public static final String CHANNEL = "deckers.thibault/aves/mediastorestream";

private Activity activity;
private Context context;
private EventChannel.EventSink eventSink;
private Handler handler;
private Map<Integer, Integer> knownEntries;

@SuppressWarnings("unchecked")
public MediaStoreStreamHandler(Activity activity, Object arguments) {
this.activity = activity;
public MediaStoreStreamHandler(Context context, Object arguments) {
this.context = context;
if (arguments instanceof Map) {
Map<String, Object> argMap = (Map<String, Object>) arguments;
this.knownEntries = (Map<Integer, Integer>) argMap.get("knownEntries");
Expand Down Expand Up @@ -47,7 +47,7 @@ private void endOfStream() {
}

void fetchAll() {
new MediaStoreImageProvider().fetchAll(activity, knownEntries, this::success); // 350ms
new MediaStoreImageProvider().fetchAll(context, knownEntries, this::success); // 350ms
endOfStream();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,24 @@ public class StorageAccessStreamHandler implements EventChannel.StreamHandler {
private Activity activity;
private EventChannel.EventSink eventSink;
private Handler handler;
private String volumePath;
private String path;

@SuppressWarnings("unchecked")
public StorageAccessStreamHandler(Activity activity, Object arguments) {
this.activity = activity;
if (arguments instanceof Map) {
Map<String, Object> argMap = (Map<String, Object>) arguments;
this.volumePath = (String) argMap.get("path");
this.path = (String) argMap.get("path");
}
}

@Override
public void onListen(Object o, final EventChannel.EventSink eventSink) {
this.eventSink = eventSink;
this.handler = new Handler(Looper.getMainLooper());
Runnable onGranted = () -> success(!PermissionManager.requireVolumeAccessDialog(activity, volumePath));
Runnable onDenied = () -> success(false);
PermissionManager.requestVolumeAccess(activity, volumePath, onGranted, onDenied);
Runnable onGranted = () -> success(true); // user gave access to a directory, with no guarantee that it matches the specified `path`
Runnable onDenied = () -> success(false); // user cancelled
PermissionManager.requestVolumeAccess(activity, path, onGranted, onDenied);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import android.net.Uri;
import android.os.Build;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.drew.imaging.ImageMetadataReader;
Expand Down Expand Up @@ -53,7 +54,7 @@ public class SourceImageEntry {
public SourceImageEntry() {
}

public SourceImageEntry(Map<String, Object> map) {
public SourceImageEntry(@NonNull Map<String, Object> map) {
this.uri = Uri.parse((String) map.get("uri"));
this.path = (String) map.get("path");
this.sourceMimeType = (String) map.get("sourceMimeType");
Expand Down Expand Up @@ -121,7 +122,7 @@ private boolean isVideo() {

// expects entry with: uri, mimeType
// finds: width, height, orientation/rotation, date, title, duration
public SourceImageEntry fillPreCatalogMetadata(Context context) {
public SourceImageEntry fillPreCatalogMetadata(@NonNull Context context) {
fillByMediaMetadataRetriever(context);
if (hasSize() && (!isVideo() || hasDuration())) return this;
fillByMetadataExtractor(context);
Expand All @@ -132,7 +133,7 @@ public SourceImageEntry fillPreCatalogMetadata(Context context) {

// expects entry with: uri, mimeType
// finds: width, height, orientation/rotation, date, title, duration
private void fillByMediaMetadataRetriever(Context context) {
private void fillByMediaMetadataRetriever(@NonNull Context context) {
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
try {
String width = null, height = null, rotation = null, durationMillis = null;
Expand Down Expand Up @@ -182,7 +183,7 @@ private void fillByMediaMetadataRetriever(Context context) {

// expects entry with: uri, mimeType
// finds: width, height, orientation, date
private void fillByMetadataExtractor(Context context) {
private void fillByMetadataExtractor(@NonNull Context context) {
if (isSvg()) return;

try (InputStream is = StorageUtils.openInputStream(context, uri)) {
Expand Down Expand Up @@ -244,7 +245,7 @@ private void fillByMetadataExtractor(Context context) {

// expects entry with: uri
// finds: width, height
private void fillByBitmapDecode(Context context) {
private void fillByBitmapDecode(@NonNull Context context) {
if (isSvg()) return;

try (InputStream is = StorageUtils.openInputStream(context, uri)) {
Expand All @@ -260,7 +261,7 @@ private void fillByBitmapDecode(Context context) {

// convenience method

private static Long toLong(Object o) {
private static Long toLong(@Nullable Object o) {
if (o == null) return null;
if (o instanceof Integer) return Long.valueOf((Integer) o);
return (long) o;
Expand Down
Loading

0 comments on commit ce0808f

Please sign in to comment.