From ee98b5c3cf365c4e3afc87adf67268cf366215f2 Mon Sep 17 00:00:00 2001 From: Floens Date: Sun, 15 Nov 2015 23:15:07 +0100 Subject: [PATCH] Android 6: Ask for storage permission when saving images Fix success toast always showing, even when the task was not successful --- .../floens/chan/core/saver/ImageSaveTask.java | 7 +- .../floens/chan/core/saver/ImageSaver.java | 102 ++++++++++++++---- .../chan/ui/activity/StartActivity.java | 14 +++ .../controller/AlbumDownloadController.java | 5 +- .../ui/controller/ImageViewerController.java | 2 +- .../ui/helper/RuntimePermissionsHelper.java | 82 ++++++++++++++ 6 files changed, 181 insertions(+), 31 deletions(-) create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/helper/RuntimePermissionsHelper.java diff --git a/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java b/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java index d7ea8132..20098422 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java +++ b/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java @@ -100,8 +100,6 @@ public class ImageSaveTask implements Runnable, FileCache.DownloadedCallback { @Override public void run() { try { - Logger.test("Start! " + toString()); - if (destination.exists()) { onDestination(); } else { @@ -113,8 +111,6 @@ public class ImageSaveTask implements Runnable, FileCache.DownloadedCallback { fileCacheDownloader.getFuture().get(); } } - - Logger.test("End! " + toString()); } catch (InterruptedException e) { onInterrupted(); } catch (Exception e) { @@ -169,13 +165,14 @@ public class ImageSaveTask implements Runnable, FileCache.DownloadedCallback { is = new FileInputStream(source); os = new FileOutputStream(destination); IOUtils.copy(is, os); + + success = true; } catch (IOException e) { Logger.e(TAG, "Error writing to file", e); } finally { IOUtils.closeQuietly(is); IOUtils.closeQuietly(os); } - success = true; } private void scanDestination() { diff --git a/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaver.java b/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaver.java index d3625e32..11200193 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaver.java +++ b/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaver.java @@ -17,6 +17,7 @@ */ package org.floens.chan.core.saver; +import android.Manifest; import android.app.NotificationManager; import android.content.Context; import android.content.Intent; @@ -26,6 +27,8 @@ import android.widget.Toast; import org.floens.chan.R; import org.floens.chan.core.model.PostImage; import org.floens.chan.core.settings.ChanSettings; +import org.floens.chan.ui.activity.StartActivity; +import org.floens.chan.ui.helper.RuntimePermissionsHelper; import org.floens.chan.ui.service.SavingNotification; import java.io.File; @@ -51,6 +54,7 @@ public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback { private ExecutorService executor = Executors.newSingleThreadExecutor(); private int doneTasks = 0; private int totalTasks = 0; + private Toast toast; public static ImageSaver getInstance() { return instance; @@ -61,15 +65,54 @@ public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback { notificationManager = (NotificationManager) getAppContext().getSystemService(Context.NOTIFICATION_SERVICE); } - public void startBundledTask(String subFolder, List tasks) { - for (ImageSaveTask task : tasks) { - PostImage postImage = task.getPostImage(); - String fileName = filterName(postImage.originalName + "." + postImage.extension); - task.setDestination(new File(getSaveLocation() + File.separator + subFolder + File.separator + fileName)); + public void startDownloadTask(Context context, final ImageSaveTask task) { + PostImage postImage = task.getPostImage(); + String name = ChanSettings.saveOriginalFilename.get() ? postImage.originalName : postImage.filename; + String fileName = filterName(name + "." + postImage.extension); + task.setDestination(findUnusedFileName(new File(getSaveLocation(), fileName), false)); + +// task.setMakeBitmap(true); + task.setShowToast(true); + if (!hasPermission(context)) { + // This does not request the permission when another request is pending. + // This is ok and will drop the task. + requestPermission(context, new RuntimePermissionsHelper.Callback() { + @Override + public void onRuntimePermissionResult(boolean granted) { + if (granted) { + startTask(task); + updateNotification(); + } else { + showToast(null, false); + } + } + }); + } else { startTask(task); + updateNotification(); + } + } + + public boolean startBundledTask(Context context, final String subFolder, final List tasks) { + if (!hasPermission(context)) { + // This does not request the permission when another request is pending. + // This is ok and will drop the tasks. + requestPermission(context, new RuntimePermissionsHelper.Callback() { + @Override + public void onRuntimePermissionResult(boolean granted) { + if (granted) { + startBundledTaskInternal(subFolder, tasks); + } else { + showToast(null, false); + } + } + }); + return false; + } else { + startBundledTaskInternal(subFolder, tasks); + return true; } - updateNotification(); } public String getSubFolder(String name) { @@ -78,19 +121,6 @@ public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback { return filtered; } - public void startDownloadTask(ImageSaveTask task) { - PostImage postImage = task.getPostImage(); - String name = ChanSettings.saveOriginalFilename.get() ? postImage.originalName : postImage.filename; - String fileName = filterName(name + "." + postImage.extension); - task.setDestination(findUnusedFileName(new File(getSaveLocation(), fileName), false)); - -// task.setMakeBitmap(true); - task.setShowToast(true); - - startTask(task); - updateNotification(); - } - public File getSaveLocation() { return new File(ChanSettings.saveLocation.get()); } @@ -108,7 +138,7 @@ public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback { showImageSaved(task); } if (task.isShowToast()) { - showToast(task); + showToast(task, success); } } @@ -123,6 +153,17 @@ public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback { executor.execute(task); } + private void startBundledTaskInternal(String subFolder, List tasks) { + for (ImageSaveTask task : tasks) { + PostImage postImage = task.getPostImage(); + String fileName = filterName(postImage.originalName + "." + postImage.extension); + task.setDestination(new File(getSaveLocation() + File.separator + subFolder + File.separator + fileName)); + + startTask(task); + } + updateNotification(); + } + private void cancelAll() { executor.shutdownNow(); executor = Executors.newSingleThreadExecutor(); @@ -157,9 +198,16 @@ public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback { notificationManager.notify(NOTIFICATION_ID, builder.build()); } - private void showToast(ImageSaveTask task) { - String savedAs = getAppContext().getString(R.string.image_save_as, task.getDestination().getName()); - Toast.makeText(getAppContext(), savedAs, Toast.LENGTH_LONG).show(); + private void showToast(ImageSaveTask task, boolean success) { + if (toast != null) { + toast.cancel(); + } + + String text = success ? + getAppContext().getString(R.string.image_save_as, task.getDestination().getName()) : + getString(R.string.image_save_failed); + toast = Toast.makeText(getAppContext(), text, Toast.LENGTH_LONG); + toast.show(); } private String filterName(String name) { @@ -210,4 +258,12 @@ public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback { return test; } + + private boolean hasPermission(Context context) { + return ((StartActivity) context).getRuntimePermissionsHelper().hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); + } + + private void requestPermission(Context context, RuntimePermissionsHelper.Callback callback) { + ((StartActivity) context).getRuntimePermissionsHelper().requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, callback); + } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java index a4f800a4..141eac5e 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java @@ -50,6 +50,7 @@ import org.floens.chan.ui.controller.StyledToolbarNavigationController; import org.floens.chan.ui.controller.ViewThreadController; import org.floens.chan.ui.helper.ImagePickDelegate; import org.floens.chan.ui.helper.PreviousVersionHandler; +import org.floens.chan.ui.helper.RuntimePermissionsHelper; import org.floens.chan.ui.state.ChanState; import org.floens.chan.ui.theme.ThemeHelper; import org.floens.chan.utils.AndroidUtils; @@ -72,6 +73,7 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat private BrowseController browseController; private ImagePickDelegate imagePickDelegate; + private RuntimePermissionsHelper runtimePermissionsHelper; public StartActivity() { boardManager = Chan.getBoardManager(); @@ -84,6 +86,7 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat ThemeHelper.getInstance().setupContext(this); imagePickDelegate = new ImagePickDelegate(this); + runtimePermissionsHelper = new RuntimePermissionsHelper(this); contentView = (ViewGroup) findViewById(android.R.id.content); @@ -277,6 +280,10 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat return imagePickDelegate; } + public RuntimePermissionsHelper getRuntimePermissionsHelper() { + return runtimePermissionsHelper; + } + @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -337,6 +344,13 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat imagePickDelegate.onActivityResult(requestCode, resultCode, data); } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + runtimePermissionsHelper.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + private Controller stackTop() { return stack.get(stack.size() - 1); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/AlbumDownloadController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/AlbumDownloadController.java index 2207f06b..52d7a688 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/AlbumDownloadController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/AlbumDownloadController.java @@ -122,8 +122,9 @@ public class AlbumDownloadController extends Controller implements ToolbarMenuIt } } - imageSaver.startBundledTask(folderForAlbum, tasks); - navigationController.popController(); + if (imageSaver.startBundledTask(context, folderForAlbum, tasks)) { + navigationController.popController(); + } } }) .show(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java index be872cca..db3d6cef 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java @@ -201,7 +201,7 @@ public class ImageViewerController extends Controller implements ImageViewerPres } else { ImageSaveTask task = new ImageSaveTask(postImage); task.setShare(share); - ImageSaver.getInstance().startDownloadTask(task); + ImageSaver.getInstance().startDownloadTask(context, task); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/helper/RuntimePermissionsHelper.java b/Clover/app/src/main/java/org/floens/chan/ui/helper/RuntimePermissionsHelper.java new file mode 100644 index 00000000..da71210d --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/helper/RuntimePermissionsHelper.java @@ -0,0 +1,82 @@ +/* + * Clover - 4chan browser https://github.com/Floens/Clover/ + * Copyright (C) 2014 Floens + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.floens.chan.ui.helper; + +import android.app.Activity; +import android.content.pm.PackageManager; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; + +import static org.floens.chan.utils.AndroidUtils.getAppContext; + +public class RuntimePermissionsHelper { + private static final int RUNTIME_PERMISSION_RESULT_ID = 3; + + private ActivityCompat.OnRequestPermissionsResultCallback callbackActvity; + + private CallbackHolder pendingCallback; + + public RuntimePermissionsHelper(ActivityCompat.OnRequestPermissionsResultCallback callbackActvity) { + this.callbackActvity = callbackActvity; + } + + public boolean hasPermission(String permission) { + return ContextCompat.checkSelfPermission(getAppContext(), permission) == PackageManager.PERMISSION_GRANTED; + } + + public boolean requestPermission(String permission, Callback callback) { + if (pendingCallback == null) { + pendingCallback = new CallbackHolder(); + pendingCallback.callback = callback; + pendingCallback.permission = permission; + + ActivityCompat.requestPermissions((Activity) callbackActvity, new String[]{permission}, RUNTIME_PERMISSION_RESULT_ID); + + return true; + } else { + return false; + } + } + + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == RUNTIME_PERMISSION_RESULT_ID && pendingCallback != null) { + boolean granted = false; + + for (int i = 0; i < permissions.length; i++) { + String permission = permissions[i]; + if (permission.equals(pendingCallback.permission) && grantResults[i] == PackageManager.PERMISSION_GRANTED) { + granted = true; + break; + } + } + + pendingCallback.callback.onRuntimePermissionResult(granted); + pendingCallback = null; + } + } + + private class CallbackHolder { + private Callback callback; + private String permission; + } + + public interface Callback { + void onRuntimePermissionResult(boolean granted); + } +}