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' } }