add storage abstraction, work on moving to StorageFile

external-storage-support
Floens 7 years ago
parent 97a15fdb85
commit 5e09cd398b
  1. 10
      Clover/app/src/main/java/org/floens/chan/controller/Controller.java
  2. 56
      Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java
  3. 35
      Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaver.java
  4. 2
      Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
  5. 38
      Clover/app/src/main/java/org/floens/chan/core/storage/BaseStorageImpl.java
  6. 106
      Clover/app/src/main/java/org/floens/chan/core/storage/LollipopStorageImpl.java
  7. 80
      Clover/app/src/main/java/org/floens/chan/core/storage/Storage.java
  8. 93
      Clover/app/src/main/java/org/floens/chan/core/storage/StorageFile.java
  9. 16
      Clover/app/src/main/java/org/floens/chan/core/storage/StorageImpl.java
  10. 5
      Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java
  11. 7
      Clover/app/src/main/java/org/floens/chan/ui/controller/AlbumDownloadController.java
  12. 5
      Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java
  13. 15
      Clover/app/src/main/java/org/floens/chan/ui/controller/SaveLocationController.java
  14. 79
      Clover/app/src/main/java/org/floens/chan/ui/controller/StorageSetupController.java
  15. 12
      Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java
  16. 25
      Clover/app/src/main/res/layout/controller_storage_setup.xml
  17. 4
      Clover/app/src/main/res/values/strings.xml

@ -18,6 +18,7 @@
package org.floens.chan.controller; package org.floens.chan.controller;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -135,6 +136,9 @@ public abstract class Controller {
controller.navigationController = navigationController; controller.navigationController = navigationController;
} }
controller.onCreate(); controller.onCreate();
if (controller.view == null) {
throw new IllegalArgumentException("Controller has no view");
}
} }
public boolean removeChildController(Controller controller) { public boolean removeChildController(Controller controller) {
@ -186,6 +190,12 @@ public abstract class Controller {
return false; return false;
} }
public void onActivityResult(int requestCode, int resultCode, Intent data) {
for (Controller childController : childControllers) {
childController.onActivityResult(requestCode, resultCode, data);
}
}
public void presentController(Controller controller) { public void presentController(Controller controller) {
presentController(controller, true); presentController(controller, true);
} }

@ -19,15 +19,14 @@ package org.floens.chan.core.saver;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.media.MediaScannerConnection;
import android.net.Uri; import android.net.Uri;
import org.floens.chan.core.cache.FileCacheListener;
import org.floens.chan.core.cache.FileCache; import org.floens.chan.core.cache.FileCache;
import org.floens.chan.core.cache.FileCacheDownloader; import org.floens.chan.core.cache.FileCacheDownloader;
import org.floens.chan.core.cache.FileCacheListener;
import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.storage.StorageFile;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.IOUtils;
import org.floens.chan.utils.ImageDecoder; import org.floens.chan.utils.ImageDecoder;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
@ -38,7 +37,6 @@ import javax.inject.Inject;
import static org.floens.chan.Chan.inject; import static org.floens.chan.Chan.inject;
import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.dp;
import static org.floens.chan.utils.AndroidUtils.getAppContext;
public class ImageSaveTask extends FileCacheListener implements Runnable { public class ImageSaveTask extends FileCacheListener implements Runnable {
private static final String TAG = "ImageSaveTask"; private static final String TAG = "ImageSaveTask";
@ -48,7 +46,7 @@ public class ImageSaveTask extends FileCacheListener implements Runnable {
private PostImage postImage; private PostImage postImage;
private ImageSaveTaskCallback callback; private ImageSaveTaskCallback callback;
private File destination; private StorageFile destination;
private boolean share; private boolean share;
private boolean makeBitmap; private boolean makeBitmap;
private Bitmap bitmap; private Bitmap bitmap;
@ -78,11 +76,11 @@ public class ImageSaveTask extends FileCacheListener implements Runnable {
return postImage; return postImage;
} }
public void setDestination(File destination) { public void setDestination(StorageFile destination) {
this.destination = destination; this.destination = destination;
} }
public File getDestination() { public StorageFile getDestination() {
return destination; return destination;
} }
@ -165,46 +163,32 @@ public class ImageSaveTask extends FileCacheListener implements Runnable {
success = true; success = true;
scanDestination(); scanDestination();
if (makeBitmap) { if (makeBitmap) {
bitmap = ImageDecoder.decodeFile(destination, dp(512), dp(256)); try {
bitmap = ImageDecoder.decodeFile(destination.inputStream(), dp(512), dp(256));
} catch (IOException e) {
Logger.e(TAG, "onDestination decodeFile", e);
bitmap = null;
}
} }
} }
private boolean copyToDestination(File source) { private boolean copyToDestination(File source) {
boolean result = false;
try { try {
File parent = destination.getParentFile(); destination.copyFrom(source);
if (!parent.mkdirs() && !parent.isDirectory()) { return true;
throw new IOException("Could not create parent directory");
}
if (destination.isDirectory()) {
throw new IOException("Destination file is already a directory");
}
IOUtils.copyFile(source, destination);
result = true;
} catch (IOException e) { } catch (IOException e) {
Logger.e(TAG, "Error writing to file", e); Logger.e(TAG, "copyToDestination copyFrom", e);
} }
return result; return false;
} }
private void scanDestination() { private void scanDestination() {
MediaScannerConnection.scanFile(getAppContext(), new String[]{destination.getAbsolutePath()}, null, new MediaScannerConnection.OnScanCompletedListener() { // TODO
@Override // MediaScannerConnection.scanFile(getAppContext(), new String[]{destination.getAbsolutePath()}, null, (path, uri) -> {
public void onScanCompleted(String path, final Uri uri) { // Runs on a binder thread
// Runs on a binder thread // AndroidUtils.runOnUiThread(() -> afterScan(uri));
AndroidUtils.runOnUiThread(new Runnable() { // });
@Override
public void run() {
afterScan(uri);
}
});
}
});
} }
private void afterScan(final Uri uri) { private void afterScan(final Uri uri) {

@ -27,6 +27,8 @@ import android.widget.Toast;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.storage.Storage;
import org.floens.chan.core.storage.StorageFile;
import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.activity.StartActivity;
import org.floens.chan.ui.helper.RuntimePermissionsHelper; import org.floens.chan.ui.helper.RuntimePermissionsHelper;
import org.floens.chan.ui.service.SavingNotification; import org.floens.chan.ui.service.SavingNotification;
@ -37,11 +39,15 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
import de.greenrobot.event.EventBus; import de.greenrobot.event.EventBus;
import static org.floens.chan.utils.AndroidUtils.getAppContext; import static org.floens.chan.utils.AndroidUtils.getAppContext;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
@Singleton
public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback { public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback {
private static final String TAG = "ImageSaver"; private static final String TAG = "ImageSaver";
private static final int MAX_RENAME_TRIES = 500; private static final int MAX_RENAME_TRIES = 500;
@ -49,18 +55,18 @@ public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback {
private static final int MAX_NAME_LENGTH = 50; private static final int MAX_NAME_LENGTH = 50;
private static final Pattern REPEATED_UNDERSCORES_PATTERN = Pattern.compile("_+"); private static final Pattern REPEATED_UNDERSCORES_PATTERN = Pattern.compile("_+");
private static final Pattern SAFE_CHARACTERS_PATTERN = Pattern.compile("[^a-zA-Z0-9._]"); private static final Pattern SAFE_CHARACTERS_PATTERN = Pattern.compile("[^a-zA-Z0-9._]");
private static final ImageSaver instance = new ImageSaver();
private NotificationManager notificationManager; private NotificationManager notificationManager;
private ExecutorService executor = Executors.newSingleThreadExecutor(); private ExecutorService executor = Executors.newSingleThreadExecutor();
private int doneTasks = 0; private int doneTasks = 0;
private int totalTasks = 0; private int totalTasks = 0;
private Toast toast; private Toast toast;
public static ImageSaver getInstance() { private Storage storage;
return instance;
} @Inject
public ImageSaver(Storage storage) {
this.storage = storage;
private ImageSaver() {
EventBus.getDefault().register(this); EventBus.getDefault().register(this);
notificationManager = (NotificationManager) getAppContext().getSystemService(Context.NOTIFICATION_SERVICE); notificationManager = (NotificationManager) getAppContext().getSystemService(Context.NOTIFICATION_SERVICE);
} }
@ -68,8 +74,13 @@ public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback {
public void startDownloadTask(Context context, final ImageSaveTask task) { public void startDownloadTask(Context context, final ImageSaveTask task) {
PostImage postImage = task.getPostImage(); PostImage postImage = task.getPostImage();
String name = ChanSettings.saveOriginalFilename.get() ? postImage.originalName : postImage.filename; String name = ChanSettings.saveOriginalFilename.get() ? postImage.originalName : postImage.filename;
String fileName = filterName(name + "." + postImage.extension); // String fileName = filterName(name + "." + postImage.extension);
task.setDestination(findUnusedFileName(new File(getSaveLocation(task), fileName), false));
// TODO
StorageFile file = storage.obtainStorageFileForName(name + "." + postImage.extension);
task.setDestination(file);
// task.setDestination(findUnusedFileName(new File(getSaveLocation(task), fileName), false));
// task.setMakeBitmap(true); // task.setMakeBitmap(true);
task.setShowToast(true); task.setShowToast(true);
@ -163,9 +174,9 @@ public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback {
for (ImageSaveTask task : tasks) { for (ImageSaveTask task : tasks) {
PostImage postImage = task.getPostImage(); PostImage postImage = task.getPostImage();
String fileName = filterName(postImage.originalName + "." + postImage.extension); String fileName = filterName(postImage.originalName + "." + postImage.extension);
task.setDestination(new File(getSaveLocation(task) + File.separator + subFolder + File.separator + fileName)); // TODO
// task.setDestination(new File(getSaveLocation(task) + File.separator + subFolder + File.separator + fileName));
startTask(task); // startTask(task);
} }
updateNotification(); updateNotification();
} }
@ -194,7 +205,7 @@ public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback {
NotificationCompat.Builder builder = new NotificationCompat.Builder(getAppContext()); NotificationCompat.Builder builder = new NotificationCompat.Builder(getAppContext());
builder.setSmallIcon(R.drawable.ic_stat_notify); builder.setSmallIcon(R.drawable.ic_stat_notify);
builder.setContentTitle(getString(R.string.image_save_saved)); builder.setContentTitle(getString(R.string.image_save_saved));
String savedAs = getAppContext().getString(R.string.image_save_as, task.getDestination().getName()); String savedAs = getAppContext().getString(R.string.image_save_as, task.getDestination().name());
builder.setContentText(savedAs); builder.setContentText(savedAs);
builder.setPriority(NotificationCompat.PRIORITY_HIGH); builder.setPriority(NotificationCompat.PRIORITY_HIGH);
builder.setStyle(new NotificationCompat.BigPictureStyle() builder.setStyle(new NotificationCompat.BigPictureStyle()
@ -210,7 +221,7 @@ public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback {
} }
String text = success ? String text = success ?
getAppContext().getString(R.string.image_save_as, task.getDestination().getName()) : getAppContext().getString(R.string.image_save_as, task.getDestination().name()) :
getString(R.string.image_save_failed); getString(R.string.image_save_failed);
toast = Toast.makeText(getAppContext(), text, Toast.LENGTH_LONG); toast = Toast.makeText(getAppContext(), text, Toast.LENGTH_LONG);
toast.show(); toast.show();

@ -115,6 +115,7 @@ public class ChanSettings {
public static final BooleanSetting developer; public static final BooleanSetting developer;
public static final StringSetting saveLocation; public static final StringSetting saveLocation;
public static final StringSetting saveLocationTreeUri;
public static final BooleanSetting saveOriginalFilename; public static final BooleanSetting saveOriginalFilename;
public static final BooleanSetting shareUrl; public static final BooleanSetting shareUrl;
public static final BooleanSetting enableReplyFab; public static final BooleanSetting enableReplyFab;
@ -197,6 +198,7 @@ public class ChanSettings {
saveLocation = new StringSetting(p, "preference_image_save_location", Environment.getExternalStorageDirectory() + File.separator + "Clover"); saveLocation = new StringSetting(p, "preference_image_save_location", Environment.getExternalStorageDirectory() + File.separator + "Clover");
saveLocation.addCallback((setting, value) -> saveLocation.addCallback((setting, value) ->
EventBus.getDefault().post(new SettingChanged<>(saveLocation))); EventBus.getDefault().post(new SettingChanged<>(saveLocation)));
saveLocationTreeUri = new StringSetting(p, "preference_image_save_tree_uri", "");
saveOriginalFilename = new BooleanSetting(p, "preference_image_save_original", false); saveOriginalFilename = new BooleanSetting(p, "preference_image_save_original", false);
shareUrl = new BooleanSetting(p, "preference_image_share_url", false); shareUrl = new BooleanSetting(p, "preference_image_share_url", false);
enableReplyFab = new BooleanSetting(p, "preference_enable_reply_fab", true); enableReplyFab = new BooleanSetting(p, "preference_enable_reply_fab", true);

@ -0,0 +1,38 @@
package org.floens.chan.core.storage;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
public class BaseStorageImpl implements StorageImpl {
protected Context applicationContext;
public BaseStorageImpl(Context applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public boolean supportsExternalStorage() {
return false;
}
@Override
public Intent getOpenTreeIntent() {
throw new UnsupportedOperationException();
}
@Override
public void handleOpenTreeIntent(Uri uri) {
throw new UnsupportedOperationException();
}
@Override
public StorageFile obtainStorageFileForName(String name) {
throw new UnsupportedOperationException();
}
@Override
public String currentStorageName() {
throw new UnsupportedOperationException();
}
}

@ -0,0 +1,106 @@
package org.floens.chan.core.storage;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.support.annotation.RequiresApi;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.utils.IOUtils;
import org.floens.chan.utils.Logger;
import java.io.FileNotFoundException;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class LollipopStorageImpl extends BaseStorageImpl {
private static final String TAG = "LollipopStorageImpl";
public LollipopStorageImpl(Context applicationContext) {
super(applicationContext);
}
@Override
public boolean supportsExternalStorage() {
return true;
}
@Override
public Intent getOpenTreeIntent() {
return new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
}
@Override
public void handleOpenTreeIntent(Uri uri) {
String documentId = DocumentsContract.getTreeDocumentId(uri);
Uri treeDocumentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId);
ChanSettings.saveLocationTreeUri.set(treeDocumentUri.toString());
}
@Override
public StorageFile obtainStorageFileForName(String name) {
String uriString = ChanSettings.saveLocationTreeUri.get();
if (uriString.isEmpty()) {
return null;
}
Uri treeUri = Uri.parse(uriString);
ContentResolver contentResolver = applicationContext.getContentResolver();
String documentId = DocumentsContract.getTreeDocumentId(treeUri);
Uri treeDocumentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId);
Uri docUri;
try {
docUri = DocumentsContract.createDocument(contentResolver, treeDocumentUri,
"text", name);
} catch (FileNotFoundException e) {
Logger.e(TAG, "obtainStorageFileForName createDocument", e);
return null;
}
return StorageFile.fromUri(contentResolver, docUri);
}
@Override
public String currentStorageName() {
String uriString = ChanSettings.saveLocationTreeUri.get();
if (uriString.isEmpty()) {
return null;
}
Uri treeUri = Uri.parse(uriString);
return queryTreeName(treeUri);
}
private String queryTreeName(Uri uri) {
ContentResolver contentResolver = applicationContext.getContentResolver();
Cursor c = null;
String name = null;
try {
c = contentResolver.query(uri, new String[]{
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE
}, null, null, null);
if (c != null && c.moveToNext()) {
name = c.getString(0);
// mime = c.getString(1);
}
return name;
} catch (Exception e) {
Logger.e(TAG, "queryTreeName", e);
} finally {
IOUtils.closeQuietly(c);
}
return null;
}
}

@ -1,66 +1,58 @@
package org.floens.chan.core.storage; package org.floens.chan.core.storage;
import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Abstraction of the storage APIs available in Android.
* <p>
* This is used primarily for saving images, especially on removable storage.
* <p>
* First, a good read:
* https://commonsware.com/blog/2017/11/13/storage-situation-internal-storage.html
* https://commonsware.com/blog/2017/11/14/storage-situation-external-storage.html
* https://commonsware.com/blog/2017/11/15/storage-situation-removable-storage.html
* <p>
* The Android Storage Access Framework can be used from Android 5.0 and higher. Since Android 5.0
* it has support for granting permissions for a directory, which we want to save our files to.
* <p>
* Otherwise a fallback is provided for only saving on the primary volume with the older APIs.
*/
@Singleton
public class Storage { public class Storage {
private static final Storage instance; private StorageImpl impl;
static { @Inject
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { public Storage(Context applicationContext) {
instance = new Storage(new BaseStorageImpl()); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
impl = new BaseStorageImpl(applicationContext);
} else { } else {
instance = new Storage(new NougatStorageImpl()); impl = new LollipopStorageImpl(applicationContext);
}
}
public static Storage getInstance() {
return instance;
} }
private StorageImpl impl;
public Storage(StorageImpl impl) {
this.impl = impl;
} }
public Intent requestExternalPermission(Context applicationContext) { public boolean supportsExternalStorage() {
return impl.requestExternalPermission(applicationContext); return impl.supportsExternalStorage();
} }
public interface StorageImpl { public Intent getOpenTreeIntent() {
Intent requestExternalPermission(Context applicationContext); return impl.getOpenTreeIntent();
} }
public static class BaseStorageImpl implements StorageImpl { public void handleOpenTreeIntent(Uri uri) {
@Override impl.handleOpenTreeIntent(uri);
public Intent requestExternalPermission(Context applicationContext) {
throw new UnsupportedOperationException();
}
} }
@TargetApi(Build.VERSION_CODES.N) public StorageFile obtainStorageFileForName(String name) {
public static class NougatStorageImpl extends BaseStorageImpl { return impl.obtainStorageFileForName(name);
@Override
public Intent requestExternalPermission(Context applicationContext) {
StorageManager sm = (StorageManager)
applicationContext.getSystemService(Context.STORAGE_SERVICE);
Objects.requireNonNull(sm);
for (StorageVolume storageVolume : sm.getStorageVolumes()) {
if (!storageVolume.isPrimary()) {
Intent accessIntent = storageVolume.createAccessIntent(null);
return accessIntent;
}
} }
return null; public String currentStorageName() {
} return impl.currentStorageName();
} }
} }

@ -0,0 +1,93 @@
package org.floens.chan.core.storage;
import android.content.ContentResolver;
import android.net.Uri;
import org.floens.chan.utils.IOUtils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class StorageFile {
private final ContentResolver contentResolver;
private final Uri uriOpenableByContentResolvers;
private final File file;
public static StorageFile fromFile(File file) {
return new StorageFile(file);
}
public static StorageFile fromUri(ContentResolver contentResolver, Uri uriOpenableByContentResolvers) {
return new StorageFile(contentResolver, uriOpenableByContentResolvers);
}
private StorageFile(ContentResolver contentResolver, Uri uriOpenableByContentResolvers) {
this.contentResolver = contentResolver;
this.uriOpenableByContentResolvers = uriOpenableByContentResolvers;
this.file = null;
}
private StorageFile(File file) {
this.contentResolver = null;
this.uriOpenableByContentResolvers = null;
this.file = file;
}
public InputStream inputStream() throws IOException {
if (file != null) {
return new FileInputStream(file);
} else {
return contentResolver.openInputStream(uriOpenableByContentResolvers);
}
}
public OutputStream outputStream() throws IOException {
if (file != null) {
File parent = file.getParentFile();
if (!parent.mkdirs() && !parent.isDirectory()) {
throw new IOException("Could not create parent directory");
}
if (file.isDirectory()) {
throw new IOException("Destination not a file");
}
return new FileOutputStream(file);
} else {
return contentResolver.openOutputStream(uriOpenableByContentResolvers);
}
}
public String name() {
return "dummy name";
}
public boolean exists() {
// TODO
return false;
}
public boolean delete() {
// TODO
return true;
}
public void copyFrom(File source) throws IOException {
InputStream is = null;
OutputStream os = null;
try {
is = new BufferedInputStream(new FileInputStream(source));
os = new BufferedOutputStream(outputStream());
IOUtils.copy(is, os);
} finally {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(os);
}
}
}

@ -0,0 +1,16 @@
package org.floens.chan.core.storage;
import android.content.Intent;
import android.net.Uri;
public interface StorageImpl {
boolean supportsExternalStorage();
Intent getOpenTreeIntent();
void handleOpenTreeIntent(Uri uri);
StorageFile obtainStorageFileForName(String name);
String currentStorageName();
}

@ -51,6 +51,7 @@ import org.floens.chan.ui.controller.BrowseController;
import org.floens.chan.ui.controller.DoubleNavigationController; import org.floens.chan.ui.controller.DoubleNavigationController;
import org.floens.chan.ui.controller.DrawerController; import org.floens.chan.ui.controller.DrawerController;
import org.floens.chan.ui.controller.SplitNavigationController; import org.floens.chan.ui.controller.SplitNavigationController;
import org.floens.chan.ui.controller.StorageSetupController;
import org.floens.chan.ui.controller.StyledToolbarNavigationController; import org.floens.chan.ui.controller.StyledToolbarNavigationController;
import org.floens.chan.ui.controller.ThreadSlideController; import org.floens.chan.ui.controller.ThreadSlideController;
import org.floens.chan.ui.controller.ViewThreadController; import org.floens.chan.ui.controller.ViewThreadController;
@ -164,6 +165,8 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat
} else { } else {
browseController.loadWithDefaultBoard(); browseController.loadWithDefaultBoard();
} }
mainNavigationController.pushController(new StorageSetupController(this), false);
} }
private boolean restoreFromUrl() { private boolean restoreFromUrl() {
@ -487,6 +490,8 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
imagePickDelegate.onActivityResult(requestCode, resultCode, data); imagePickDelegate.onActivityResult(requestCode, resultCode, data);
drawerController.onActivityResult(requestCode, resultCode, data);
} }
private Controller stackTop() { private Controller stackTop() {

@ -46,6 +46,9 @@ import org.floens.chan.utils.RecyclerUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.inject.Inject;
import static org.floens.chan.Chan.inject;
import static org.floens.chan.ui.theme.ThemeHelper.theme; import static org.floens.chan.ui.theme.ThemeHelper.theme;
import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.dp;
@ -59,6 +62,8 @@ public class AlbumDownloadController extends Controller implements View.OnClickL
private boolean allChecked = true; private boolean allChecked = true;
private AlbumAdapter adapter; private AlbumAdapter adapter;
@Inject
private ImageSaver imageSaver; private ImageSaver imageSaver;
public AlbumDownloadController(Context context) { public AlbumDownloadController(Context context) {
@ -69,7 +74,7 @@ public class AlbumDownloadController extends Controller implements View.OnClickL
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
imageSaver = ImageSaver.getInstance(); inject(this);
view = inflateRes(R.layout.controller_album_download); view = inflateRes(R.layout.controller_album_download);

@ -92,6 +92,9 @@ public class ImageViewerController extends Controller implements ImageViewerPres
@Inject @Inject
ImageLoader imageLoader; ImageLoader imageLoader;
@Inject
ImageSaver imageSaver;
private int statusBarColorPrevious; private int statusBarColorPrevious;
private AnimatorSet startAnimation; private AnimatorSet startAnimation;
private AnimatorSet endAnimation; private AnimatorSet endAnimation;
@ -226,7 +229,7 @@ public class ImageViewerController extends Controller implements ImageViewerPres
File.separator + File.separator +
presenter.getLoadable().boardCode); presenter.getLoadable().boardCode);
} }
ImageSaver.getInstance().startDownloadTask(context, task); imageSaver.startDownloadTask(context, task);
} }
} }

@ -18,9 +18,7 @@
package org.floens.chan.ui.controller; package org.floens.chan.ui.controller;
import android.Manifest; import android.Manifest;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.support.design.widget.FloatingActionButton; import android.support.design.widget.FloatingActionButton;
import android.view.View; import android.view.View;
@ -28,7 +26,6 @@ import org.floens.chan.R;
import org.floens.chan.controller.Controller; import org.floens.chan.controller.Controller;
import org.floens.chan.core.saver.FileWatcher; import org.floens.chan.core.saver.FileWatcher;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.storage.Storage;
import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.activity.StartActivity;
import org.floens.chan.ui.adapter.FilesAdapter; import org.floens.chan.ui.adapter.FilesAdapter;
import org.floens.chan.ui.helper.RuntimePermissionsHelper; import org.floens.chan.ui.helper.RuntimePermissionsHelper;
@ -77,18 +74,6 @@ public class SaveLocationController extends Controller implements FileWatcher.Fi
} else { } else {
requestPermission(); requestPermission();
} }
test();
}
private void test() {
Storage storage = Storage.getInstance();
Intent intent = storage.requestExternalPermission(context.getApplicationContext());
if (intent != null) {
if (intent.resolveActivity(context.getPackageManager()) != null) {
((Activity) context).startActivityForResult(intent, 10024);
}
}
} }
@Override @Override

@ -0,0 +1,79 @@
package org.floens.chan.ui.controller;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import org.floens.chan.R;
import org.floens.chan.controller.Controller;
import org.floens.chan.core.storage.Storage;
import javax.inject.Inject;
import static org.floens.chan.Chan.inject;
public class StorageSetupController extends Controller implements View.OnClickListener {
private static final int OPEN_TREE_INTENT_RESULT_ID = 101;
private static final String TAG = "StorageSetupController";
@Inject
private Storage storage;
private TextView text;
private Button button;
public StorageSetupController(Context context) {
super(context);
}
@Override
public void onCreate() {
super.onCreate();
inject(this);
// Navigation
navigation.setTitle(R.string.storage_setup_screen);
// View inflation
view = inflateRes(R.layout.controller_storage_setup);
// View binding
text = view.findViewById(R.id.text);
button = view.findViewById(R.id.button);
// View setup
button.setOnClickListener(this);
updateName();
}
@Override
public void onClick(View v) {
if (v == button) {
requestTree();
}
}
private void requestTree() {
Intent i = storage.getOpenTreeIntent();
((Activity) context).startActivityForResult(i, OPEN_TREE_INTENT_RESULT_ID);
updateName();
}
private void updateName() {
text.setText(storage.currentStorageName());
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == OPEN_TREE_INTENT_RESULT_ID && resultCode == Activity.RESULT_OK) {
storage.handleOpenTreeIntent(data.getData());
updateName();
}
}
}

@ -25,6 +25,7 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
/** /**
* Simple ImageDecoder. Taken from Volley ImageRequest. * Simple ImageDecoder. Taken from Volley ImageRequest.
@ -55,26 +56,27 @@ public class ImageDecoder {
if (!file.exists()) if (!file.exists())
return null; return null;
FileInputStream fis;
try { try {
fis = new FileInputStream(file); InputStream fis = new FileInputStream(file);
return decodeFile(fis, maxWidth, maxHeight);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
e.printStackTrace(); e.printStackTrace();
return null; return null;
} }
}
public static Bitmap decodeFile(InputStream is, int maxWidth, int maxHeight) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
Bitmap bitmap = null; Bitmap bitmap = null;
try { try {
IOUtils.copy(fis, baos); IOUtils.copy(is, baos);
bitmap = decode(baos.toByteArray(), maxWidth, maxHeight); bitmap = decode(baos.toByteArray(), maxWidth, maxHeight);
} catch (IOException | OutOfMemoryError e) { } catch (IOException | OutOfMemoryError e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
IOUtils.closeQuietly(fis); IOUtils.closeQuietly(is);
IOUtils.closeQuietly(baos); IOUtils.closeQuietly(baos);
} }

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="open" />
</LinearLayout>
</ScrollView>

@ -546,6 +546,10 @@ Re-enable this permission in the app settings if you permanently disabled it."
<string name="setting_folder_navigate_up">Up</string> <string name="setting_folder_navigate_up">Up</string>
<!-- Storage setup settings -->
<string name="storage_setup_screen">Setup storage</string>
<!-- Theme settings --> <!-- Theme settings -->
<string name="setting_theme_explanation"> <string name="setting_theme_explanation">
"Swipe to change the theme. "Swipe to change the theme.

Loading…
Cancel
Save