From b2f525ffd0aede7515f38afb0dc92ff00300c5ce Mon Sep 17 00:00:00 2001 From: Floens Date: Wed, 13 Jul 2016 14:46:14 +0200 Subject: [PATCH] Add a better save location picker Taken from another project, might as well use it here. This layout did allow switching between storage devices, but I found out how the storage framework works now, and it's too much work to get it to work right now, so the selection of storage is hidden. Reading works fine but writing requires a lot more code. Might pick it up later if there's a good library that does it for us. All the current code uses the normal File api so that has to be rewritten afaik. Closes #202 --- Clover/app/build.gradle | 14 +- Clover/app/src/main/AndroidManifest.xml | 17 +- .../org/floens/chan/core/model/FileItem.java | 29 +++ .../org/floens/chan/core/model/FileItems.java | 17 ++ .../floens/chan/core/saver/FileWatcher.java | 112 ++++++++++ .../floens/chan/core/saver/StorageHelper.java | 31 +++ .../chan/core/settings/ChanSettings.java | 16 ++ .../floens/chan/core/settings/Setting.java | 4 + .../floens/chan/ui/adapter/FilesAdapter.java | 129 +++++++++++ .../AdvancedSettingsController.java | 33 --- .../ui/controller/MainSettingsController.java | 18 ++ .../ui/controller/SaveLocationController.java | 136 ++++++++++++ .../chan/ui/fragment/FolderPickFragment.java | 201 ------------------ .../floens/chan/ui/layout/FilesLayout.java | 141 ++++++++++++ .../org/floens/chan/utils/RecyclerUtils.java | 13 ++ .../ic_chevron_left_black_24dp.xml | 9 + .../drawable-xxhdpi/ic_folder_black_24dp.xml | 9 + Clover/app/src/main/res/layout/cell_file.xml | 22 ++ .../res/layout/controller_save_location.xml | 67 ++++++ Clover/app/src/main/res/values/strings.xml | 10 + Clover/build.gradle | 2 +- 21 files changed, 780 insertions(+), 250 deletions(-) create mode 100644 Clover/app/src/main/java/org/floens/chan/core/model/FileItem.java create mode 100644 Clover/app/src/main/java/org/floens/chan/core/model/FileItems.java create mode 100644 Clover/app/src/main/java/org/floens/chan/core/saver/FileWatcher.java create mode 100644 Clover/app/src/main/java/org/floens/chan/core/saver/StorageHelper.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/adapter/FilesAdapter.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/controller/SaveLocationController.java delete mode 100644 Clover/app/src/main/java/org/floens/chan/ui/fragment/FolderPickFragment.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/layout/FilesLayout.java create mode 100644 Clover/app/src/main/res/drawable-mdpi/ic_chevron_left_black_24dp.xml create mode 100644 Clover/app/src/main/res/drawable-xxhdpi/ic_folder_black_24dp.xml create mode 100644 Clover/app/src/main/res/layout/cell_file.xml create mode 100644 Clover/app/src/main/res/layout/controller_save_location.xml diff --git a/Clover/app/build.gradle b/Clover/app/build.gradle index f7b1af74..39b0b97e 100644 --- a/Clover/app/build.gradle +++ b/Clover/app/build.gradle @@ -84,13 +84,13 @@ android { } dependencies { - compile 'com.android.support:support-v13:23.2.1' - compile 'com.android.support:appcompat-v7:23.2.1' - compile 'com.android.support:recyclerview-v7:23.2.1' - compile 'com.android.support:cardview-v7:23.2.1' - compile 'com.android.support:support-annotations:23.2.1' - compile 'com.android.support:design:23.2.1' - compile 'com.android.support:customtabs:23.2.1' + compile 'com.android.support:support-v13:23.4.0' + compile 'com.android.support:appcompat-v7:23.4.0' + compile 'com.android.support:recyclerview-v7:23.4.0' + compile 'com.android.support:cardview-v7:23.4.0' + compile 'com.android.support:support-annotations:23.4.0' + compile 'com.android.support:design:23.4.0' + compile 'com.android.support:customtabs:23.4.0' compile 'org.jsoup:jsoup:1.8.3' compile 'com.j256.ormlite:ormlite-core:4.48' diff --git a/Clover/app/src/main/AndroidManifest.xml b/Clover/app/src/main/AndroidManifest.xml index 4ec033dd..52e330fb 100644 --- a/Clover/app/src/main/AndroidManifest.xml +++ b/Clover/app/src/main/AndroidManifest.xml @@ -31,24 +31,25 @@ along with this program. If not, see . android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/Chan.Theme"> - + + android:value="true" /> + android:name="com.sec.android.app.multiwindow" + android:required="false" /> + + android:value="true" /> + android:value="632.0dip" /> - + android:value="598.0dip" /> + fileItems; + + public final boolean canNavigateUp; + + public FileItems(File path, List fileItems, boolean canNavigateUp) { + this.path = path; + this.fileItems = fileItems; + this.canNavigateUp = canNavigateUp; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/saver/FileWatcher.java b/Clover/app/src/main/java/org/floens/chan/core/saver/FileWatcher.java new file mode 100644 index 00000000..4ab84094 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/saver/FileWatcher.java @@ -0,0 +1,112 @@ +package org.floens.chan.core.saver; + +import android.os.FileObserver; +import android.util.Log; + +import org.floens.chan.core.model.FileItem; +import org.floens.chan.core.model.FileItems; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class FileWatcher { + private static final String TAG = "FileWatcher"; + + private static final Comparator FILE_COMPARATOR = new Comparator() { + @Override + public int compare(FileItem a, FileItem b) { + return a.file.getName().compareToIgnoreCase(b.file.getName()); + } + }; + + private final FileWatcherCallback callback; + boolean initialized = false; + private File startingPath; + + private File currentPath; + + private AFileObserver fileObserver; + + public FileWatcher(FileWatcherCallback callback, File startingPath) { + this.callback = callback; + this.startingPath = startingPath; + } + + public void initialize() { + initialized = true; + navigateTo(startingPath); + } + + public File getCurrentPath() { + return currentPath; + } + + public void navigateUp() { + File parentFile = currentPath.getParentFile(); + if (parentFile != null && StorageHelper.canNavigate(parentFile)) { + navigateTo(parentFile); + } + } + + public void navigateTo(File to) { + if (!StorageHelper.canNavigate(to)) { + throw new IllegalArgumentException("Cannot navigate to " + to.getAbsolutePath()); + } + + if (fileObserver != null) { + fileObserver.stopWatching(); + fileObserver = null; + } + + // TODO: fileobserver is broken +// int mask = FileObserver.CREATE | FileObserver.DELETE; +// fileObserver = new AFileObserver(to.getAbsolutePath(), mask); +// fileObserver = new AFileObserver("/sdcard/"); +// fileObserver.startWatching(); + + currentPath = to; + + File[] files = currentPath.listFiles(); + + List folderList = new ArrayList<>(); + List fileList = new ArrayList<>(); + for (File file : files) { + if (StorageHelper.canNavigate(file)) { + folderList.add(new FileItem(file)); + } else if (file.isFile()) { + fileList.add(new FileItem(file)); + } + } + Collections.sort(folderList, FILE_COMPARATOR); + Collections.sort(fileList, FILE_COMPARATOR); + List items = new ArrayList<>(folderList.size() + fileList.size()); + items.addAll(folderList); + items.addAll(fileList); + + boolean canNavigateUp = StorageHelper.canNavigate(currentPath.getParentFile()); + + callback.onFiles(new FileItems(currentPath, items, canNavigateUp)); + } + + private class AFileObserver extends FileObserver { + public AFileObserver(String path) { + super(path); + } + + public AFileObserver(String path, int mask) { + super(path, mask); + } + + @Override + public void onEvent(int event, String path) { + Log.d(TAG, "onEvent() called with: " + "event = [" + event + "], path = [" + path + "]"); + } + } + + public interface FileWatcherCallback { + void onFiles(FileItems fileItems); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/saver/StorageHelper.java b/Clover/app/src/main/java/org/floens/chan/core/saver/StorageHelper.java new file mode 100644 index 00000000..4118a97a --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/saver/StorageHelper.java @@ -0,0 +1,31 @@ +package org.floens.chan.core.saver; + +import java.io.File; + +public class StorageHelper { + private static final String TAG = "StorageHelper"; + + public static boolean canNavigate(File file) { + return file != null && !isDirectoryBlacklisted(file) && file.exists() + && file.isDirectory() && file.canRead(); + } + + public static boolean isDirectoryBlacklisted(File file) { + String absolutePath = file.getAbsolutePath(); + switch (absolutePath) { + case "/storage": + return true; + case "/storage/emulated": + return true; + case "/storage/emulated/0/0": + return true; + case "/storage/emulated/legacy": + return true; + } + return false; + } + + public static boolean canOpen(File file) { + return file != null && file.exists() && file.isFile() && file.canRead(); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java index 83f13a7e..d41e53bc 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java +++ b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java @@ -31,6 +31,8 @@ import java.io.File; import java.net.InetSocketAddress; import java.net.Proxy; +import de.greenrobot.event.EventBus; + public class ChanSettings { public enum MediaAutoLoadMode implements OptionSettingItem { // ALways auto load, either wifi or mobile @@ -200,6 +202,12 @@ public class ChanSettings { developer = new BooleanSetting(p, "preference_developer", false); saveLocation = new StringSetting(p, "preference_image_save_location", Environment.getExternalStorageDirectory() + File.separator + "Clover"); + saveLocation.addCallback(new Setting.SettingCallback() { + @Override + public void onValueChange(Setting setting, String value) { + EventBus.getDefault().post(new SettingChanged<>(saveLocation)); + } + }); saveOriginalFilename = new BooleanSetting(p, "preference_image_save_original", false); shareUrl = new BooleanSetting(p, "preference_image_share_url", false); networkHttps = new BooleanSetting(p, "preference_network_https", true); @@ -342,4 +350,12 @@ public class ChanSettings { this.accentColor = accentColor; } } + + public static class SettingChanged { + public final Setting setting; + + public SettingChanged(Setting setting) { + this.setting = setting; + } + } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/Setting.java b/Clover/app/src/main/java/org/floens/chan/core/settings/Setting.java index 6114df02..a3b0ad65 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/settings/Setting.java +++ b/Clover/app/src/main/java/org/floens/chan/core/settings/Setting.java @@ -42,6 +42,10 @@ public abstract class Setting { return def; } + public String getKey() { + return key; + } + public void addCallback(SettingCallback callback) { this.callbacks.add(callback); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/FilesAdapter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/FilesAdapter.java new file mode 100644 index 00000000..5c6cad11 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/FilesAdapter.java @@ -0,0 +1,129 @@ +package org.floens.chan.ui.adapter; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.floens.chan.R; +import org.floens.chan.core.model.FileItem; +import org.floens.chan.core.model.FileItems; + +import static org.floens.chan.utils.AndroidUtils.getAttrColor; + +public class FilesAdapter extends RecyclerView.Adapter { + private static final int ITEM_TYPE_FOLDER = 0; + private static final int ITEM_TYPE_FILE = 1; + + private FileItem highlightedItem; + private FileItems fileItems; + private Callback callback; + + public FilesAdapter(Callback callback) { + this.callback = callback; + } + + public void setFiles(FileItems fileItems) { + this.fileItems = fileItems; + notifyDataSetChanged(); + } + + public void setHighlightedItem(FileItem highlightedItem) { + this.highlightedItem = highlightedItem; + } + + @Override + public FileViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new FileViewHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.cell_file, parent, false)); + } + + @SuppressWarnings({"ConstantConditions", "deprecation"}) + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + int itemViewType = getItemViewType(position); + switch (itemViewType) { + case ITEM_TYPE_FILE: + case ITEM_TYPE_FOLDER: { + boolean isFile = itemViewType == ITEM_TYPE_FILE; + + FileItem item = getItem(position); + FileViewHolder fileViewHolder = ((FileViewHolder) holder); + fileViewHolder.text.setText(item.file.getName()); + + Context context = holder.itemView.getContext(); + + if (isFile) { + fileViewHolder.image.setVisibility(View.GONE); + } else { + fileViewHolder.image.setVisibility(View.VISIBLE); + Drawable drawable = DrawableCompat.wrap( + context.getResources().getDrawable(R.drawable.ic_folder_black_24dp)); + DrawableCompat.setTint(drawable, getAttrColor(context, R.attr.text_color_secondary)); + fileViewHolder.image.setImageDrawable(drawable); + } + + boolean highlighted = highlightedItem != null && highlightedItem.file.equals(item.file); + if (highlighted) { + fileViewHolder.itemView.setBackgroundColor(0x0e000000); + } else { + fileViewHolder.itemView.setBackgroundResource(R.drawable.item_background); + } + + break; + } + } + } + + @Override + public int getItemCount() { + return fileItems.fileItems.size(); + } + + @Override + public int getItemViewType(int position) { + FileItem item = getItem(position); + if (item.isFile()) { + return ITEM_TYPE_FILE; + } else if (item.isFolder()) { + return ITEM_TYPE_FOLDER; + } else { + return ITEM_TYPE_FILE; + } + } + + public FileItem getItem(int position) { + return fileItems.fileItems.get(position); + } + + private void onItemClicked(FileItem fileItem) { + callback.onFileItemClicked(fileItem); + } + + public class FileViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + private ImageView image; + private TextView text; + + public FileViewHolder(View itemView) { + super(itemView); + image = (ImageView) itemView.findViewById(R.id.image); + text = (TextView) itemView.findViewById(R.id.text); + itemView.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + FileItem item = getItem(getAdapterPosition()); + onItemClicked(item); + } + } + + public interface Callback { + void onFileItemClicked(FileItem fileItem); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/AdvancedSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/AdvancedSettingsController.java index 5611d6f1..5b62993b 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/AdvancedSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/AdvancedSettingsController.java @@ -17,34 +17,26 @@ */ package org.floens.chan.ui.controller; -import android.app.Activity; import android.content.Context; -import android.support.v7.app.AlertDialog; -import android.view.View; import android.widget.LinearLayout; import org.floens.chan.R; import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.ui.activity.StartActivity; -import org.floens.chan.ui.fragment.FolderPickFragment; import org.floens.chan.ui.helper.RefreshUIMessage; import org.floens.chan.ui.settings.BooleanSettingView; import org.floens.chan.ui.settings.IntegerSettingView; -import org.floens.chan.ui.settings.LinkSettingView; import org.floens.chan.ui.settings.SettingView; import org.floens.chan.ui.settings.SettingsController; import org.floens.chan.ui.settings.SettingsGroup; import org.floens.chan.ui.settings.StringSettingView; -import java.io.File; - import de.greenrobot.event.EventBus; public class AdvancedSettingsController extends SettingsController { private static final String TAG = "AdvancedSettingsController"; private boolean needRestart; - private LinkSettingView saveLocation; private SettingView newCaptcha; private SettingView enableReplyFab; private SettingView neverHideToolbar; @@ -93,27 +85,6 @@ public class AdvancedSettingsController extends SettingsController { private void populatePreferences() { SettingsGroup settings = new SettingsGroup(R.string.settings_group_advanced); - // TODO change this to a presenting controller - saveLocation = (LinkSettingView) settings.add(new LinkSettingView(this, R.string.setting_save_folder, 0, new View.OnClickListener() { - @Override - public void onClick(View v) { - File dir = new File(ChanSettings.saveLocation.get()); - if (!dir.mkdirs() && !dir.isDirectory()) { - new AlertDialog.Builder(context).setMessage(R.string.setting_save_folder_error_create_folder).show(); - } else { - FolderPickFragment frag = FolderPickFragment.newInstance(new FolderPickFragment.FolderPickListener() { - @Override - public void folderPicked(File path) { - ChanSettings.saveLocation.set(path.getAbsolutePath()); - setSaveLocationDescription(); - } - }, dir); - ((Activity) context).getFragmentManager().beginTransaction().add(frag, null).commit(); - } - } - })); - setSaveLocationDescription(); - newCaptcha = settings.add(new BooleanSettingView(this, ChanSettings.postNewCaptcha, R.string.setting_use_new_captcha, R.string.setting_use_new_captcha_description)); settings.add(new BooleanSettingView(this, ChanSettings.saveOriginalFilename, R.string.setting_save_original_filename, 0)); controllersSwipeable = settings.add(new BooleanSettingView(this, ChanSettings.controllerSwipeable, R.string.setting_controller_swipeable, 0)); @@ -142,8 +113,4 @@ public class AdvancedSettingsController extends SettingsController { proxy.add(new IntegerSettingView(this, ChanSettings.proxyPort, R.string.setting_proxy_port, R.string.setting_proxy_port)); groups.add(proxy); } - - private void setSaveLocationDescription() { - saveLocation.setDescription(ChanSettings.saveLocation.get()); - } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java index 49742ba4..1b7e06df 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java @@ -64,6 +64,7 @@ public class MainSettingsController extends SettingsController implements Toolba private ListSettingView videoAutoLoadView; private LinkSettingView boardEditorView; + private LinkSettingView saveLocation; private LinkSettingView watchLink; private LinkSettingView passLink; private int clickCount; @@ -144,6 +145,12 @@ public class MainSettingsController extends SettingsController implements Toolba updateBoardLinkDescription(); } + public void onEvent(ChanSettings.SettingChanged setting) { + if (setting.setting == ChanSettings.saveLocation) { + setSaveLocationDescription(); + } + } + @Override public void onMenuItemClicked(ToolbarMenuItem item) { } @@ -258,6 +265,13 @@ public class MainSettingsController extends SettingsController implements Toolba navigationController.pushController(new FiltersController(context)); } })); + saveLocation = (LinkSettingView) browsing.add(new LinkSettingView(this, R.string.save_location_screen, 0, new View.OnClickListener() { + @Override + public void onClick(View v) { + navigationController.pushController(new SaveLocationController(context)); + } + })); + setSaveLocationDescription(); browsing.add(new BooleanSettingView(this, ChanSettings.openLinkConfirmation, R.string.setting_open_link_confirmation, 0)); browsing.add(new BooleanSettingView(this, ChanSettings.autoRefreshThread, R.string.setting_auto_refresh_thread, 0)); @@ -410,6 +424,10 @@ public class MainSettingsController extends SettingsController implements Toolba boardEditorView.setDescription(context.getResources().getQuantityString(R.plurals.board, savedBoards.size(), savedBoards.size())); } + private void setSaveLocationDescription() { + saveLocation.setDescription(ChanSettings.saveLocation.get()); + } + private void updateVideoLoadModes() { ChanSettings.MediaAutoLoadMode currentImageLoadMode = ChanSettings.imageAutoLoadNetwork.get(); ChanSettings.MediaAutoLoadMode[] modes = ChanSettings.MediaAutoLoadMode.values(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/SaveLocationController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/SaveLocationController.java new file mode 100644 index 00000000..c4b8efb8 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/SaveLocationController.java @@ -0,0 +1,136 @@ +package org.floens.chan.ui.controller; + +import android.Manifest; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.provider.Settings; +import android.support.design.widget.FloatingActionButton; +import android.support.v7.app.AlertDialog; +import android.view.View; + +import org.floens.chan.R; +import org.floens.chan.controller.Controller; +import org.floens.chan.core.model.FileItem; +import org.floens.chan.core.model.FileItems; +import org.floens.chan.core.saver.FileWatcher; +import org.floens.chan.core.settings.ChanSettings; +import org.floens.chan.ui.activity.StartActivity; +import org.floens.chan.ui.adapter.FilesAdapter; +import org.floens.chan.ui.helper.RuntimePermissionsHelper; +import org.floens.chan.ui.layout.FilesLayout; +import org.floens.chan.utils.AndroidUtils; + +import java.io.File; + +public class SaveLocationController extends Controller implements FileWatcher.FileWatcherCallback, FilesAdapter.Callback, FilesLayout.Callback, View.OnClickListener { + private static final String TAG = "SaveLocationController"; + + private FilesLayout filesLayout; + private FloatingActionButton setButton; + + private RuntimePermissionsHelper runtimePermissionsHelper; + private boolean gotPermission = false; + + private FileWatcher fileWatcher; + private FileItems fileItems; + + public SaveLocationController(Context context) { + super(context); + } + + @Override + public void onCreate() { + super.onCreate(); + + navigationItem.setTitle(R.string.save_location_screen); + + view = inflateRes(R.layout.controller_save_location); + filesLayout = (FilesLayout) view.findViewById(R.id.files_layout); + filesLayout.setCallback(this); + setButton = (FloatingActionButton) view.findViewById(R.id.set_button); + setButton.setOnClickListener(this); + + File saveLocation = new File(ChanSettings.saveLocation.get()); + fileWatcher = new FileWatcher(this, saveLocation); + + runtimePermissionsHelper = ((StartActivity) context).getRuntimePermissionsHelper(); + gotPermission = hasPermission(); + if (gotPermission) { + initialize(); + } else { + requestPermission(); + } + } + + @Override + public void onClick(View v) { + if (v == setButton) { + File currentPath = fileWatcher.getCurrentPath(); + ChanSettings.saveLocation.set(currentPath.getAbsolutePath()); + navigationController.popController(); + } + } + + @Override + public void onFiles(FileItems fileItems) { + this.fileItems = fileItems; + filesLayout.setFiles(fileItems); + } + + @Override + public void onBackClicked() { + fileWatcher.navigateUp(); + } + + @Override + public void onFileItemClicked(FileItem fileItem) { + if (fileItem.canNavigate()) { + fileWatcher.navigateTo(fileItem.file); + } + // Else ignore, we only do folder selection here + } + + private boolean hasPermission() { + return runtimePermissionsHelper.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); + } + + private void requestPermission() { + runtimePermissionsHelper.requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, new RuntimePermissionsHelper.Callback() { + @Override + public void onRuntimePermissionResult(boolean granted) { + gotPermission = granted; + if (gotPermission) { + initialize(); + } else { + new AlertDialog.Builder(context) + .setTitle(R.string.write_permission_required_title) + .setMessage(R.string.write_permission_required) + .setCancelable(false) + .setNeutralButton(R.string.write_permission_app_settings, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + requestPermission(); + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.parse("package:" + context.getPackageName())); + AndroidUtils.openIntent(intent); + } + }) + .setPositiveButton(R.string.write_permission_grant, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + requestPermission(); + } + }) + .show(); + } + } + }); + } + + private void initialize() { + filesLayout.initialize(); + fileWatcher.initialize(); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/fragment/FolderPickFragment.java b/Clover/app/src/main/java/org/floens/chan/ui/fragment/FolderPickFragment.java deleted file mode 100644 index ad160752..00000000 --- a/Clover/app/src/main/java/org/floens/chan/ui/fragment/FolderPickFragment.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * 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.fragment; - -import android.app.DialogFragment; -import android.os.Bundle; -import android.os.Environment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.FrameLayout; -import android.widget.ListView; -import android.widget.TextView; - -import org.floens.chan.R; -import org.floens.chan.ui.theme.ThemeHelper; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -public class FolderPickFragment extends DialogFragment { - private TextView statusPath; - private ListView listView; - private ArrayAdapter adapter; - boolean hasParent = false; - - private FolderPickListener listener; - private File currentPath; - private List directories; - private FrameLayout okButton; - private TextView okButtonIcon; - - public static FolderPickFragment newInstance(FolderPickListener listener, File startingPath) { - FolderPickFragment fragment = new FolderPickFragment(); - fragment.listener = listener; - fragment.currentPath = startingPath; - return fragment; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (listener == null || currentPath == null) { - dismiss(); - } - - setStyle(STYLE_NO_TITLE, 0); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { - if (listener == null || currentPath == null) { - return null; - } - - View container = inflater.inflate(R.layout.fragment_folder_pick, parent); - - statusPath = (TextView) container.findViewById(R.id.folder_status); - listView = (ListView) container.findViewById(R.id.folder_list); - okButton = (FrameLayout) container.findViewById(R.id.pick_ok); - okButtonIcon = (TextView) container.findViewById(R.id.pick_ok_icon); - - container.findViewById(R.id.pick_back).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dismiss(); - } - }); - - okButton.findViewById(R.id.pick_ok).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - listener.folderPicked(currentPath); - dismiss(); - } - }); - - if (!ThemeHelper.getInstance().getTheme().isLightTheme) { - ((TextView) container.findViewById(R.id.pick_back_icon)).setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_arrow_back_white_24dp, 0, 0, 0); - ((TextView) container.findViewById(R.id.pick_ok_icon)).setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_done_white_24dp, 0, 0, 0); - } - - adapter = new ArrayAdapter(inflater.getContext(), 0) { - @Override - public View getView(final int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = LayoutInflater.from(getContext()).inflate(android.R.layout.simple_list_item_1, null); - } - TextView text = (TextView) convertView; - - String name = getItem(position); - - text.setText(name); - - text.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (hasParent) { - if (position == 0) { - File parent = currentPath.getParentFile(); - moveTo(parent); - } else if (position > 0 && position <= directories.size()) { - File dir = directories.get(position - 1); - moveTo(dir); - } - } else { - if (position >= 0 && position < directories.size()) { - File dir = directories.get(position); - moveTo(dir); - } - } - } - }); - - return text; - } - }; - - listView.setAdapter(adapter); - - if (currentPath == null || !currentPath.exists()) { - currentPath = Environment.getExternalStorageDirectory(); - } - - moveTo(currentPath); - - return container; - } - - private boolean validPath(File path) { - return path != null && path.isDirectory() && path.canRead() && path.canWrite(); - } - - private void moveTo(File path) { - if (path != null && path.isDirectory()) { - File[] listFiles = path.listFiles(); - if (listFiles != null) { - currentPath = path; - statusPath.setText(currentPath.getAbsolutePath()); - List dirs = new ArrayList<>(); - for (File file : path.listFiles()) { - if (file.isDirectory()) { - dirs.add(file); - } - } - - setDirs(dirs); - } - } - - validState(); - } - - private void validState() { - if (validPath(currentPath)) { - okButton.setEnabled(true); - okButtonIcon.setEnabled(true); - } else { - okButton.setEnabled(false); - okButtonIcon.setEnabled(false); - } - } - - private void setDirs(List dirs) { - directories = dirs; - adapter.clear(); - - if (currentPath.getParent() != null) { - adapter.add(".."); - hasParent = true; - } else { - hasParent = false; - } - for (File file : dirs) { - adapter.add(file.getName()); - } - adapter.notifyDataSetChanged(); - } - - public interface FolderPickListener { - void folderPicked(File path); - } -} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/FilesLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/FilesLayout.java new file mode 100644 index 00000000..9b679d6a --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/FilesLayout.java @@ -0,0 +1,141 @@ +package org.floens.chan.ui.layout; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.floens.chan.R; +import org.floens.chan.core.model.FileItem; +import org.floens.chan.core.model.FileItems; +import org.floens.chan.ui.adapter.FilesAdapter; +import org.floens.chan.utils.RecyclerUtils; + +import java.util.HashMap; +import java.util.Map; + +import static org.floens.chan.utils.AndroidUtils.getAttrColor; + +public class FilesLayout extends LinearLayout implements FilesAdapter.Callback, View.OnClickListener { + private ViewGroup backLayout; + private ImageView backImage; + private TextView backText; + private RecyclerView recyclerView; + + private LinearLayoutManager layoutManager; + private FilesAdapter filesAdapter; + + private Map history = new HashMap<>(); + private FileItemHistory currentHistory; + private FileItems currentFileItems; + + private Callback callback; + + public FilesLayout(Context context) { + this(context, null); + } + + public FilesLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FilesLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + backLayout = (ViewGroup) findViewById(R.id.back_layout); + backImage = (ImageView) backLayout.findViewById(R.id.back_image); + backImage.setImageDrawable(DrawableCompat.wrap(backImage.getDrawable())); + backText = (TextView) backLayout.findViewById(R.id.back_text); + recyclerView = (RecyclerView) findViewById(R.id.recycler); + + backLayout.setOnClickListener(this); + } + + public void initialize() { + layoutManager = new LinearLayoutManager(getContext()); + recyclerView.setLayoutManager(layoutManager); + + filesAdapter = new FilesAdapter(this); + recyclerView.setAdapter(filesAdapter); + } + + public void setCallback(Callback callback) { + this.callback = callback; + } + + public void setFiles(FileItems fileItems) { + // Save the associated list position + if (currentFileItems != null) { + int[] indexTop = RecyclerUtils.getIndexAndTop(recyclerView); + currentHistory.index = indexTop[0]; + currentHistory.top = indexTop[1]; + history.put(currentFileItems.path.getAbsolutePath(), currentHistory); + } + + filesAdapter.setFiles(fileItems); + currentFileItems = fileItems; + + // Restore any previous list position + currentHistory = history.get(fileItems.path.getAbsolutePath()); + if (currentHistory != null) { + layoutManager.scrollToPositionWithOffset(currentHistory.index, currentHistory.top); + filesAdapter.setHighlightedItem(currentHistory.clickedItem); + } else { + currentHistory = new FileItemHistory(); + filesAdapter.setHighlightedItem(null); + } + + boolean enabled = fileItems.canNavigateUp; + backLayout.setEnabled(enabled); + Drawable wrapped = DrawableCompat.wrap(backImage.getDrawable()); + backImage.setImageDrawable(wrapped); + int color = getAttrColor(getContext(), enabled ? R.attr.text_color_primary : R.attr.text_color_hint); + DrawableCompat.setTint(wrapped, color); + backText.setEnabled(enabled); + backText.setTextColor(color); + } + + public RecyclerView getRecyclerView() { + return recyclerView; + } + + public ViewGroup getBackLayout() { + return backLayout; + } + + @Override + public void onFileItemClicked(FileItem fileItem) { + currentHistory.clickedItem = fileItem; + callback.onFileItemClicked(fileItem); + } + + @Override + public void onClick(View view) { + if (view == backLayout) { + currentHistory.clickedItem = null; + callback.onBackClicked(); + } + } + + private class FileItemHistory { + int index, top; + FileItem clickedItem; + } + + public interface Callback { + void onBackClicked(); + + void onFileItemClicked(FileItem fileItem); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/utils/RecyclerUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/RecyclerUtils.java index c3ea9bfc..26d55b92 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/RecyclerUtils.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/RecyclerUtils.java @@ -18,6 +18,7 @@ package org.floens.chan.utils; import android.support.v7.widget.RecyclerView; +import android.view.View; import java.lang.reflect.Field; @@ -34,4 +35,16 @@ public class RecyclerUtils { Logger.e(TAG, "Error clearing RecyclerView cache with reflection", e); } } + + public static int[] getIndexAndTop(RecyclerView recyclerView) { + int index = 0, top = 0; + if (recyclerView.getLayoutManager().getChildCount() > 0) { + View topChild = recyclerView.getLayoutManager().getChildAt(0); + index = ((RecyclerView.LayoutParams) topChild.getLayoutParams()).getViewLayoutPosition(); + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) topChild.getLayoutParams(); + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + top = layoutManager.getDecoratedTop(topChild) - params.topMargin - recyclerView.getPaddingTop(); + } + return new int[]{index, top}; + } } diff --git a/Clover/app/src/main/res/drawable-mdpi/ic_chevron_left_black_24dp.xml b/Clover/app/src/main/res/drawable-mdpi/ic_chevron_left_black_24dp.xml new file mode 100644 index 00000000..e6bb3ca9 --- /dev/null +++ b/Clover/app/src/main/res/drawable-mdpi/ic_chevron_left_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/Clover/app/src/main/res/drawable-xxhdpi/ic_folder_black_24dp.xml b/Clover/app/src/main/res/drawable-xxhdpi/ic_folder_black_24dp.xml new file mode 100644 index 00000000..d7c6145c --- /dev/null +++ b/Clover/app/src/main/res/drawable-xxhdpi/ic_folder_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/Clover/app/src/main/res/layout/cell_file.xml b/Clover/app/src/main/res/layout/cell_file.xml new file mode 100644 index 00000000..91e1e278 --- /dev/null +++ b/Clover/app/src/main/res/layout/cell_file.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/Clover/app/src/main/res/layout/controller_save_location.xml b/Clover/app/src/main/res/layout/controller_save_location.xml new file mode 100644 index 00000000..842cede3 --- /dev/null +++ b/Clover/app/src/main/res/layout/controller_save_location.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 679a7d66..8331d928 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -67,6 +67,7 @@ along with this program. If not, see . Add Close Back + Up OK Exit Delete @@ -326,6 +327,15 @@ along with this program. If not, see . Filters + Save location + Storage permission required + +"Permission to access storage is required for browsing files. + +Re-enable this permission in the app settings if you permanently disabled it." + App settings + Grant + Select images (%1$d / %2$d) Please select images to download %1$s will be downloaded to the folder %2$s diff --git a/Clover/build.gradle b/Clover/build.gradle index 856d3d7f..aa141380 100644 --- a/Clover/build.gradle +++ b/Clover/build.gradle @@ -4,7 +4,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.1.0' + classpath 'com.android.tools.build:gradle:2.1.2' } }