Update the ImageSaver to use a task based approach. Do not use DownloadManager for album downloads, also fixes saving to an external sd card problems. A persistent notification is now shown while downloading, to avoid clover being killed when downloading.multisite
@ -0,0 +1,219 @@ |
|||||||
|
/* |
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
package org.floens.chan.core.saver; |
||||||
|
|
||||||
|
import android.content.Intent; |
||||||
|
import android.graphics.Bitmap; |
||||||
|
import android.media.MediaScannerConnection; |
||||||
|
import android.net.Uri; |
||||||
|
|
||||||
|
import org.floens.chan.Chan; |
||||||
|
import org.floens.chan.core.cache.FileCache; |
||||||
|
import org.floens.chan.core.model.PostImage; |
||||||
|
import org.floens.chan.utils.AndroidUtils; |
||||||
|
import org.floens.chan.utils.IOUtils; |
||||||
|
import org.floens.chan.utils.ImageDecoder; |
||||||
|
import org.floens.chan.utils.Logger; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.io.FileInputStream; |
||||||
|
import java.io.FileOutputStream; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStream; |
||||||
|
import java.io.OutputStream; |
||||||
|
|
||||||
|
import static org.floens.chan.utils.AndroidUtils.dp; |
||||||
|
import static org.floens.chan.utils.AndroidUtils.getAppContext; |
||||||
|
|
||||||
|
public class ImageSaveTask implements Runnable, FileCache.DownloadedCallback { |
||||||
|
private static final String TAG = "ImageSaveTask"; |
||||||
|
|
||||||
|
private PostImage postImage; |
||||||
|
private ImageSaveTaskCallback callback; |
||||||
|
private File destination; |
||||||
|
private boolean share; |
||||||
|
private boolean makeBitmap; |
||||||
|
private Bitmap bitmap; |
||||||
|
private boolean showToast; |
||||||
|
|
||||||
|
private boolean success = false; |
||||||
|
|
||||||
|
public ImageSaveTask(PostImage postImage) { |
||||||
|
this.postImage = postImage; |
||||||
|
} |
||||||
|
|
||||||
|
public void setCallback(ImageSaveTaskCallback callback) { |
||||||
|
this.callback = callback; |
||||||
|
} |
||||||
|
|
||||||
|
public PostImage getPostImage() { |
||||||
|
return postImage; |
||||||
|
} |
||||||
|
|
||||||
|
public void setDestination(File destination) { |
||||||
|
this.destination = destination; |
||||||
|
} |
||||||
|
|
||||||
|
public File getDestination() { |
||||||
|
return destination; |
||||||
|
} |
||||||
|
|
||||||
|
public void setShare(boolean share) { |
||||||
|
this.share = share; |
||||||
|
} |
||||||
|
|
||||||
|
public void setMakeBitmap(boolean makeBitmap) { |
||||||
|
this.makeBitmap = makeBitmap; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isMakeBitmap() { |
||||||
|
return makeBitmap; |
||||||
|
} |
||||||
|
|
||||||
|
public Bitmap getBitmap() { |
||||||
|
return bitmap; |
||||||
|
} |
||||||
|
|
||||||
|
public void setShowToast(boolean showToast) { |
||||||
|
this.showToast = showToast; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isShowToast() { |
||||||
|
return showToast; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
try { |
||||||
|
Logger.test("Start! " + toString()); |
||||||
|
|
||||||
|
if (destination.exists()) { |
||||||
|
onDestination(); |
||||||
|
} else { |
||||||
|
FileCache.FileCacheDownloader fileCacheDownloader = Chan.getFileCache().downloadFile(postImage.imageUrl, this); |
||||||
|
// If the fileCacheDownloader is null then the callbacks were already executed here,
|
||||||
|
// else wait for the download to finish to avoid that the next task is immediately executed.
|
||||||
|
if (fileCacheDownloader != null) { |
||||||
|
// If the file is now downloading
|
||||||
|
fileCacheDownloader.getFuture().get(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Logger.test("End! " + toString()); |
||||||
|
} catch (InterruptedException e) { |
||||||
|
onInterrupted(); |
||||||
|
} catch (Exception e) { |
||||||
|
Logger.e(TAG, "Uncaught exception", e); |
||||||
|
} finally { |
||||||
|
postFinished(success); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onProgress(long downloaded, long total, boolean done) { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onFail(boolean notFound) { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onSuccess(File file) { |
||||||
|
copyToDestination(file); |
||||||
|
onDestination(); |
||||||
|
} |
||||||
|
|
||||||
|
private void onInterrupted() { |
||||||
|
if (destination.exists()) { |
||||||
|
if (!destination.delete()) { |
||||||
|
Logger.e(TAG, "Could not delete destination after an interrupt"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void onDestination() { |
||||||
|
scanDestination(); |
||||||
|
if (makeBitmap) { |
||||||
|
bitmap = ImageDecoder.decodeFile(destination, dp(512), dp(256)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void copyToDestination(File source) { |
||||||
|
InputStream is = null; |
||||||
|
OutputStream os = null; |
||||||
|
try { |
||||||
|
File parent = destination.getParentFile(); |
||||||
|
if (!parent.mkdirs() && !parent.isDirectory()) { |
||||||
|
throw new IOException("Could not create parent directory"); |
||||||
|
} |
||||||
|
|
||||||
|
if (destination.isDirectory()) { |
||||||
|
throw new IOException("Destination file is already a directory"); |
||||||
|
} |
||||||
|
|
||||||
|
is = new FileInputStream(source); |
||||||
|
os = new FileOutputStream(destination); |
||||||
|
IOUtils.copy(is, os); |
||||||
|
} catch (IOException e) { |
||||||
|
Logger.e(TAG, "Error writing to file", e); |
||||||
|
} finally { |
||||||
|
IOUtils.closeQuietly(is); |
||||||
|
IOUtils.closeQuietly(os); |
||||||
|
} |
||||||
|
success = true; |
||||||
|
} |
||||||
|
|
||||||
|
private void scanDestination() { |
||||||
|
MediaScannerConnection.scanFile(getAppContext(), new String[]{destination.getAbsolutePath()}, null, new MediaScannerConnection.OnScanCompletedListener() { |
||||||
|
@Override |
||||||
|
public void onScanCompleted(String path, final Uri uri) { |
||||||
|
// Runs on a binder thread
|
||||||
|
AndroidUtils.runOnUiThread(new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
afterScan(uri); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private void afterScan(final Uri uri) { |
||||||
|
Logger.d(TAG, "Media scan succeeded: " + uri); |
||||||
|
|
||||||
|
if (share) { |
||||||
|
Intent intent = new Intent(Intent.ACTION_SEND); |
||||||
|
intent.setType("image/*"); |
||||||
|
intent.putExtra(Intent.EXTRA_STREAM, uri); |
||||||
|
AndroidUtils.openIntent(intent); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void postFinished(final boolean success) { |
||||||
|
AndroidUtils.runOnUiThread(new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
callback.imageSaveTaskFinished(ImageSaveTask.this, success); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public interface ImageSaveTaskCallback { |
||||||
|
void imageSaveTaskFinished(ImageSaveTask task, boolean success); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,213 @@ |
|||||||
|
/* |
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
package org.floens.chan.core.saver; |
||||||
|
|
||||||
|
import android.app.NotificationManager; |
||||||
|
import android.content.Context; |
||||||
|
import android.content.Intent; |
||||||
|
import android.support.v7.app.NotificationCompat; |
||||||
|
import android.widget.Toast; |
||||||
|
|
||||||
|
import org.floens.chan.R; |
||||||
|
import org.floens.chan.core.model.PostImage; |
||||||
|
import org.floens.chan.core.settings.ChanSettings; |
||||||
|
import org.floens.chan.ui.service.SavingNotification; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.util.List; |
||||||
|
import java.util.concurrent.ExecutorService; |
||||||
|
import java.util.concurrent.Executors; |
||||||
|
import java.util.regex.Pattern; |
||||||
|
|
||||||
|
import de.greenrobot.event.EventBus; |
||||||
|
|
||||||
|
import static org.floens.chan.utils.AndroidUtils.getAppContext; |
||||||
|
import static org.floens.chan.utils.AndroidUtils.getString; |
||||||
|
|
||||||
|
public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback { |
||||||
|
private static final String TAG = "ImageSaver"; |
||||||
|
private static final int NOTIFICATION_ID = 3; |
||||||
|
private static final int MAX_NAME_LENGTH = 50; |
||||||
|
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 ImageSaver instance = new ImageSaver(); |
||||||
|
|
||||||
|
private NotificationManager notificationManager; |
||||||
|
private ExecutorService executor = Executors.newSingleThreadExecutor(); |
||||||
|
private int doneTasks = 0; |
||||||
|
private int totalTasks = 0; |
||||||
|
|
||||||
|
public static ImageSaver getInstance() { |
||||||
|
return instance; |
||||||
|
} |
||||||
|
|
||||||
|
private ImageSaver() { |
||||||
|
EventBus.getDefault().register(this); |
||||||
|
notificationManager = (NotificationManager) getAppContext().getSystemService(Context.NOTIFICATION_SERVICE); |
||||||
|
} |
||||||
|
|
||||||
|
public void startBundledTask(String subFolder, List<ImageSaveTask> tasks) { |
||||||
|
for (ImageSaveTask task : tasks) { |
||||||
|
PostImage postImage = task.getPostImage(); |
||||||
|
String fileName = filterName(postImage.originalName + "." + postImage.extension); |
||||||
|
task.setDestination(new File(getSaveLocation() + File.separator + subFolder + File.separator + fileName)); |
||||||
|
|
||||||
|
startTask(task); |
||||||
|
} |
||||||
|
updateNotification(); |
||||||
|
} |
||||||
|
|
||||||
|
public String getSubFolder(String name) { |
||||||
|
String filtered = filterName(name); |
||||||
|
filtered = filtered.substring(0, Math.min(filtered.length(), MAX_NAME_LENGTH)); |
||||||
|
return filtered; |
||||||
|
} |
||||||
|
|
||||||
|
public void startDownloadTask(ImageSaveTask task) { |
||||||
|
PostImage postImage = task.getPostImage(); |
||||||
|
String name = ChanSettings.saveOriginalFilename.get() ? postImage.originalName : postImage.filename; |
||||||
|
String fileName = filterName(name + "." + postImage.extension); |
||||||
|
task.setDestination(findUnusedFileName(new File(getSaveLocation(), fileName), false)); |
||||||
|
|
||||||
|
// task.setMakeBitmap(true);
|
||||||
|
task.setShowToast(true); |
||||||
|
|
||||||
|
startTask(task); |
||||||
|
updateNotification(); |
||||||
|
} |
||||||
|
|
||||||
|
public File getSaveLocation() { |
||||||
|
return new File(ChanSettings.saveLocation.get()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void imageSaveTaskFinished(ImageSaveTask task, boolean success) { |
||||||
|
doneTasks++; |
||||||
|
if (doneTasks == totalTasks) { |
||||||
|
totalTasks = 0; |
||||||
|
doneTasks = 0; |
||||||
|
} |
||||||
|
updateNotification(); |
||||||
|
|
||||||
|
if (task.isMakeBitmap()) { |
||||||
|
showImageSaved(task); |
||||||
|
} |
||||||
|
if (task.isShowToast()) { |
||||||
|
showToast(task); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void onEvent(SavingNotification.SavingCancelRequestMessage message) { |
||||||
|
cancelAll(); |
||||||
|
} |
||||||
|
|
||||||
|
private void startTask(ImageSaveTask task) { |
||||||
|
task.setCallback(this); |
||||||
|
|
||||||
|
totalTasks++; |
||||||
|
executor.execute(task); |
||||||
|
} |
||||||
|
|
||||||
|
private void cancelAll() { |
||||||
|
executor.shutdownNow(); |
||||||
|
executor = Executors.newSingleThreadExecutor(); |
||||||
|
|
||||||
|
totalTasks = 0; |
||||||
|
doneTasks = 0; |
||||||
|
updateNotification(); |
||||||
|
} |
||||||
|
|
||||||
|
private void updateNotification() { |
||||||
|
Intent service = new Intent(getAppContext(), SavingNotification.class); |
||||||
|
if (totalTasks == 0) { |
||||||
|
getAppContext().stopService(service); |
||||||
|
} else { |
||||||
|
service.putExtra(SavingNotification.DONE_TASKS_KEY, doneTasks); |
||||||
|
service.putExtra(SavingNotification.TOTAL_TASKS_KEY, totalTasks); |
||||||
|
getAppContext().startService(service); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void showImageSaved(ImageSaveTask task) { |
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(getAppContext()); |
||||||
|
builder.setSmallIcon(R.drawable.ic_stat_notify); |
||||||
|
builder.setContentTitle(getString(R.string.image_save_saved)); |
||||||
|
String savedAs = getAppContext().getString(R.string.image_save_as, task.getDestination().getName()); |
||||||
|
builder.setContentText(savedAs); |
||||||
|
builder.setPriority(NotificationCompat.PRIORITY_HIGH); |
||||||
|
builder.setStyle(new NotificationCompat.BigPictureStyle() |
||||||
|
.bigPicture(task.getBitmap()) |
||||||
|
.setSummaryText(savedAs)); |
||||||
|
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build()); |
||||||
|
} |
||||||
|
|
||||||
|
private void showToast(ImageSaveTask task) { |
||||||
|
String savedAs = getAppContext().getString(R.string.image_save_as, task.getDestination().getName()); |
||||||
|
Toast.makeText(getAppContext(), savedAs, Toast.LENGTH_LONG).show(); |
||||||
|
} |
||||||
|
|
||||||
|
private String filterName(String name) { |
||||||
|
name = name.replace(' ', '_'); |
||||||
|
name = SAFE_CHARACTERS_PATTERN.matcher(name).replaceAll(""); |
||||||
|
name = REPEATED_UNDERSCORES_PATTERN.matcher(name).replaceAll("_"); |
||||||
|
if (name.length() == 0) { |
||||||
|
name = "_"; |
||||||
|
} |
||||||
|
return name; |
||||||
|
} |
||||||
|
|
||||||
|
private File findUnusedFileName(File start, boolean directory) { |
||||||
|
String base; |
||||||
|
String extension; |
||||||
|
|
||||||
|
if (directory) { |
||||||
|
base = start.getAbsolutePath(); |
||||||
|
extension = null; |
||||||
|
} else { |
||||||
|
String[] splitted = start.getAbsolutePath().split("\\.(?=[^\\.]+$)"); |
||||||
|
if (splitted.length == 2) { |
||||||
|
base = splitted[0]; |
||||||
|
extension = "." + splitted[1]; |
||||||
|
} else { |
||||||
|
base = splitted[0]; |
||||||
|
extension = "."; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
File test; |
||||||
|
if (directory) { |
||||||
|
test = new File(base); |
||||||
|
} else { |
||||||
|
test = new File(base + extension); |
||||||
|
} |
||||||
|
|
||||||
|
int index = 0; |
||||||
|
int tries = 0; |
||||||
|
while (test.exists() && tries++ < 100) { |
||||||
|
if (directory) { |
||||||
|
test = new File(base + "_" + index); |
||||||
|
} else { |
||||||
|
test = new File(base + "_" + index + extension); |
||||||
|
} |
||||||
|
index++; |
||||||
|
} |
||||||
|
|
||||||
|
return test; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,271 @@ |
|||||||
|
/* |
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
package org.floens.chan.ui.controller; |
||||||
|
|
||||||
|
import android.content.Context; |
||||||
|
import android.content.DialogInterface; |
||||||
|
import android.graphics.drawable.Drawable; |
||||||
|
import android.support.design.widget.FloatingActionButton; |
||||||
|
import android.support.v4.graphics.drawable.DrawableCompat; |
||||||
|
import android.support.v7.app.AlertDialog; |
||||||
|
import android.support.v7.widget.GridLayoutManager; |
||||||
|
import android.support.v7.widget.RecyclerView; |
||||||
|
import android.view.LayoutInflater; |
||||||
|
import android.view.View; |
||||||
|
import android.view.ViewGroup; |
||||||
|
import android.view.animation.DecelerateInterpolator; |
||||||
|
import android.widget.ImageView; |
||||||
|
|
||||||
|
import org.floens.chan.R; |
||||||
|
import org.floens.chan.controller.Controller; |
||||||
|
import org.floens.chan.core.model.Loadable; |
||||||
|
import org.floens.chan.core.model.PostImage; |
||||||
|
import org.floens.chan.core.saver.ImageSaveTask; |
||||||
|
import org.floens.chan.core.saver.ImageSaver; |
||||||
|
import org.floens.chan.ui.theme.ThemeHelper; |
||||||
|
import org.floens.chan.ui.toolbar.ToolbarMenu; |
||||||
|
import org.floens.chan.ui.toolbar.ToolbarMenuItem; |
||||||
|
import org.floens.chan.ui.view.FloatingMenuItem; |
||||||
|
import org.floens.chan.ui.view.GridRecyclerView; |
||||||
|
import org.floens.chan.ui.view.ThumbnailView; |
||||||
|
import org.floens.chan.utils.RecyclerUtils; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import static org.floens.chan.utils.AndroidUtils.dp; |
||||||
|
|
||||||
|
public class AlbumDownloadController extends Controller implements ToolbarMenuItem.ToolbarMenuItemCallback, View.OnClickListener { |
||||||
|
private static final int CHECK_ALL = 1; |
||||||
|
|
||||||
|
private GridRecyclerView recyclerView; |
||||||
|
private GridLayoutManager gridLayoutManager; |
||||||
|
private FloatingActionButton download; |
||||||
|
|
||||||
|
private List<AlbumDownloadItem> items = new ArrayList<>(); |
||||||
|
private Loadable loadable; |
||||||
|
|
||||||
|
private boolean allChecked = true; |
||||||
|
private AlbumAdapter adapter; |
||||||
|
private ImageSaver imageSaver; |
||||||
|
|
||||||
|
public AlbumDownloadController(Context context) { |
||||||
|
super(context); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onCreate() { |
||||||
|
super.onCreate(); |
||||||
|
|
||||||
|
imageSaver = ImageSaver.getInstance(); |
||||||
|
|
||||||
|
view = inflateRes(R.layout.controller_album_download); |
||||||
|
|
||||||
|
updateTitle(); |
||||||
|
|
||||||
|
navigationItem.menu = new ToolbarMenu(context); |
||||||
|
navigationItem.menu.addItem(new ToolbarMenuItem(context, this, CHECK_ALL, R.drawable.ic_select_all_white_24dp)); |
||||||
|
|
||||||
|
download = (FloatingActionButton) view.findViewById(R.id.download); |
||||||
|
download.setOnClickListener(this); |
||||||
|
recyclerView = (GridRecyclerView) view.findViewById(R.id.recycler_view); |
||||||
|
recyclerView.setHasFixedSize(true); |
||||||
|
gridLayoutManager = new GridLayoutManager(context, 3); |
||||||
|
recyclerView.setLayoutManager(gridLayoutManager); |
||||||
|
recyclerView.setSpanWidth(dp(90)); |
||||||
|
|
||||||
|
adapter = new AlbumAdapter(); |
||||||
|
recyclerView.setAdapter(adapter); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onClick(View v) { |
||||||
|
if (v == download) { |
||||||
|
int checkCount = getCheckCount(); |
||||||
|
if (checkCount == 0) { |
||||||
|
new AlertDialog.Builder(context) |
||||||
|
.setMessage(R.string.album_download_none_checked) |
||||||
|
.setPositiveButton(R.string.ok, null) |
||||||
|
.show(); |
||||||
|
} else { |
||||||
|
final String folderForAlbum = imageSaver.getSubFolder(loadable.title); |
||||||
|
|
||||||
|
String message = context.getString(R.string.album_download_confirm, |
||||||
|
context.getResources().getQuantityString(R.plurals.image, checkCount, checkCount), |
||||||
|
folderForAlbum); |
||||||
|
|
||||||
|
new AlertDialog.Builder(context) |
||||||
|
.setMessage(message) |
||||||
|
.setNegativeButton(R.string.cancel, null) |
||||||
|
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { |
||||||
|
@Override |
||||||
|
public void onClick(DialogInterface dialog, int which) { |
||||||
|
List<ImageSaveTask> tasks = new ArrayList<>(items.size()); |
||||||
|
for (AlbumDownloadItem item : items) { |
||||||
|
if (item.checked) { |
||||||
|
tasks.add(new ImageSaveTask(item.postImage)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
imageSaver.startBundledTask(folderForAlbum, tasks); |
||||||
|
navigationController.popController(); |
||||||
|
} |
||||||
|
}) |
||||||
|
.show(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onMenuItemClicked(ToolbarMenuItem menuItem) { |
||||||
|
if ((Integer) menuItem.getId() == CHECK_ALL) { |
||||||
|
RecyclerUtils.clearRecyclerCache(recyclerView); |
||||||
|
|
||||||
|
for (int i = 0, itemsSize = items.size(); i < itemsSize; i++) { |
||||||
|
AlbumDownloadItem item = items.get(i); |
||||||
|
if (item.checked == allChecked) { |
||||||
|
item.checked = !allChecked; |
||||||
|
AlbumDownloadCell cell = (AlbumDownloadCell) recyclerView.findViewHolderForAdapterPosition(i); |
||||||
|
if (cell != null) { |
||||||
|
setItemChecked(cell, item.checked, true); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
updateAllChecked(); |
||||||
|
updateTitle(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) { |
||||||
|
} |
||||||
|
|
||||||
|
public void setPostImages(Loadable loadable, List<PostImage> postImages) { |
||||||
|
this.loadable = loadable; |
||||||
|
for (int i = 0, postImagesSize = postImages.size(); i < postImagesSize; i++) { |
||||||
|
PostImage postImage = postImages.get(i); |
||||||
|
items.add(new AlbumDownloadItem(postImage, true, i)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void updateTitle() { |
||||||
|
navigationItem.title = context.getString(R.string.album_download_screen, getCheckCount(), items.size()); |
||||||
|
navigationItem.updateTitle(); |
||||||
|
} |
||||||
|
|
||||||
|
private void updateAllChecked() { |
||||||
|
allChecked = getCheckCount() == items.size(); |
||||||
|
} |
||||||
|
|
||||||
|
private int getCheckCount() { |
||||||
|
int checkCount = 0; |
||||||
|
for (AlbumDownloadItem item : items) { |
||||||
|
if (item.checked) { |
||||||
|
checkCount++; |
||||||
|
} |
||||||
|
} |
||||||
|
return checkCount; |
||||||
|
} |
||||||
|
|
||||||
|
private static class AlbumDownloadItem { |
||||||
|
public PostImage postImage; |
||||||
|
public boolean checked; |
||||||
|
public int id; |
||||||
|
|
||||||
|
public AlbumDownloadItem(PostImage postImage, boolean checked, int id) { |
||||||
|
this.postImage = postImage; |
||||||
|
this.checked = checked; |
||||||
|
this.id = id; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private class AlbumAdapter extends RecyclerView.Adapter<AlbumDownloadCell> { |
||||||
|
public AlbumAdapter() { |
||||||
|
setHasStableIds(true); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AlbumDownloadCell onCreateViewHolder(ViewGroup parent, int viewType) { |
||||||
|
return new AlbumDownloadCell(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_album_download, parent, false)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onBindViewHolder(AlbumDownloadCell holder, int position) { |
||||||
|
AlbumDownloadItem item = items.get(position); |
||||||
|
|
||||||
|
holder.thumbnailView.setUrl(item.postImage.thumbnailUrl, dp(100), dp(100)); |
||||||
|
setItemChecked(holder, item.checked, false); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getItemCount() { |
||||||
|
return items.size(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public long getItemId(int position) { |
||||||
|
return items.get(position).id; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private class AlbumDownloadCell extends RecyclerView.ViewHolder implements View.OnClickListener { |
||||||
|
private ImageView checkbox; |
||||||
|
private ThumbnailView thumbnailView; |
||||||
|
|
||||||
|
public AlbumDownloadCell(View itemView) { |
||||||
|
super(itemView); |
||||||
|
itemView.getLayoutParams().height = recyclerView.getRealSpanWidth(); |
||||||
|
checkbox = (ImageView) itemView.findViewById(R.id.checkbox); |
||||||
|
thumbnailView = (ThumbnailView) itemView.findViewById(R.id.thumbnail_view); |
||||||
|
itemView.setOnClickListener(this); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onClick(View v) { |
||||||
|
int adapterPosition = getAdapterPosition(); |
||||||
|
AlbumDownloadItem item = items.get(adapterPosition); |
||||||
|
item.checked = !item.checked; |
||||||
|
updateAllChecked(); |
||||||
|
updateTitle(); |
||||||
|
setItemChecked(this, item.checked, true); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("deprecation") |
||||||
|
private void setItemChecked(AlbumDownloadCell cell, boolean checked, boolean animated) { |
||||||
|
float scale = checked ? 0.75f : 1f; |
||||||
|
if (animated) { |
||||||
|
cell.thumbnailView.animate().scaleX(scale).scaleY(scale) |
||||||
|
.setInterpolator(new DecelerateInterpolator(3f)).setDuration(500).start(); |
||||||
|
} else { |
||||||
|
cell.thumbnailView.setScaleX(scale); |
||||||
|
cell.thumbnailView.setScaleY(scale); |
||||||
|
} |
||||||
|
|
||||||
|
Drawable drawable = context.getResources().getDrawable(checked ? R.drawable.ic_check_circle_white_24dp : |
||||||
|
R.drawable.ic_radio_button_unchecked_white_24dp); |
||||||
|
|
||||||
|
if (checked) { |
||||||
|
Drawable wrapped = DrawableCompat.wrap(drawable); |
||||||
|
DrawableCompat.setTint(wrapped, ThemeHelper.PrimaryColor.BLUE.color); |
||||||
|
cell.checkbox.setImageDrawable(wrapped); |
||||||
|
} else { |
||||||
|
cell.checkbox.setImageDrawable(drawable); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,108 @@ |
|||||||
|
/* |
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
package org.floens.chan.ui.service; |
||||||
|
|
||||||
|
import android.app.Notification; |
||||||
|
import android.app.NotificationManager; |
||||||
|
import android.app.PendingIntent; |
||||||
|
import android.app.Service; |
||||||
|
import android.content.Intent; |
||||||
|
import android.os.Bundle; |
||||||
|
import android.os.IBinder; |
||||||
|
import android.support.annotation.Nullable; |
||||||
|
import android.support.v7.app.NotificationCompat; |
||||||
|
|
||||||
|
import org.floens.chan.R; |
||||||
|
|
||||||
|
import de.greenrobot.event.EventBus; |
||||||
|
|
||||||
|
import static org.floens.chan.utils.AndroidUtils.getAppContext; |
||||||
|
|
||||||
|
public class SavingNotification extends Service { |
||||||
|
public static final String DONE_TASKS_KEY = "done_tasks"; |
||||||
|
public static final String TOTAL_TASKS_KEY = "total_tasks"; |
||||||
|
private static final String CANCEL_KEY = "cancel"; |
||||||
|
|
||||||
|
private static final int NOTIFICATION_ID = 2; |
||||||
|
|
||||||
|
private NotificationManager notificationManager; |
||||||
|
|
||||||
|
private boolean inForeground = false; |
||||||
|
private int doneTasks; |
||||||
|
private int totalTasks; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public IBinder onBind(Intent intent) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onCreate() { |
||||||
|
super.onCreate(); |
||||||
|
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onDestroy() { |
||||||
|
super.onDestroy(); |
||||||
|
notificationManager.cancel(NOTIFICATION_ID); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) { |
||||||
|
if (intent != null && intent.getExtras() != null) { |
||||||
|
Bundle extras = intent.getExtras(); |
||||||
|
|
||||||
|
if (extras.getBoolean(CANCEL_KEY)) { |
||||||
|
EventBus.getDefault().post(new SavingCancelRequestMessage()); |
||||||
|
} else { |
||||||
|
doneTasks = extras.getInt(DONE_TASKS_KEY); |
||||||
|
totalTasks = extras.getInt(TOTAL_TASKS_KEY); |
||||||
|
|
||||||
|
if (!inForeground) { |
||||||
|
startForeground(NOTIFICATION_ID, getNotification()); |
||||||
|
inForeground = true; |
||||||
|
} else { |
||||||
|
notificationManager.notify(NOTIFICATION_ID, getNotification()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return START_STICKY; |
||||||
|
} |
||||||
|
|
||||||
|
private Notification getNotification() { |
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(getAppContext()); |
||||||
|
builder.setSmallIcon(R.drawable.ic_stat_notify); |
||||||
|
builder.setContentTitle(getString(R.string.image_save_notification_downloading)); |
||||||
|
builder.setContentText(getString(R.string.image_save_notification_cancel)); |
||||||
|
builder.setProgress(totalTasks, doneTasks, false); |
||||||
|
builder.setContentInfo(doneTasks + "/" + totalTasks); |
||||||
|
|
||||||
|
Intent intent = new Intent(this, SavingNotification.class); |
||||||
|
intent.putExtra(CANCEL_KEY, true); |
||||||
|
PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); |
||||||
|
builder.setContentIntent(pendingIntent); |
||||||
|
|
||||||
|
return builder.build(); |
||||||
|
} |
||||||
|
|
||||||
|
public static class SavingCancelRequestMessage { |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,75 @@ |
|||||||
|
/* |
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
package org.floens.chan.ui.view; |
||||||
|
|
||||||
|
import android.content.Context; |
||||||
|
import android.support.v7.widget.GridLayoutManager; |
||||||
|
import android.support.v7.widget.RecyclerView; |
||||||
|
import android.util.AttributeSet; |
||||||
|
|
||||||
|
/** |
||||||
|
* A RecyclerView with a GridLayoutManager that manages the span count by dividing the width of the |
||||||
|
* view with the value set by {@link #setSpanWidth(int)}. |
||||||
|
*/ |
||||||
|
public class GridRecyclerView extends RecyclerView { |
||||||
|
private GridLayoutManager gridLayoutManager; |
||||||
|
private int spanWidth; |
||||||
|
private int realSpanWidth; |
||||||
|
|
||||||
|
public GridRecyclerView(Context context) { |
||||||
|
super(context); |
||||||
|
} |
||||||
|
|
||||||
|
public GridRecyclerView(Context context, AttributeSet attrs) { |
||||||
|
super(context, attrs); |
||||||
|
} |
||||||
|
|
||||||
|
public GridRecyclerView(Context context, AttributeSet attrs, int defStyle) { |
||||||
|
super(context, attrs, defStyle); |
||||||
|
} |
||||||
|
|
||||||
|
public void setLayoutManager(GridLayoutManager gridLayoutManager) { |
||||||
|
this.gridLayoutManager = gridLayoutManager; |
||||||
|
super.setLayoutManager(gridLayoutManager); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Set the width of each span in pixels. |
||||||
|
* |
||||||
|
* @param spanWidth width of each span in pixels. |
||||||
|
*/ |
||||||
|
public void setSpanWidth(int spanWidth) { |
||||||
|
this.spanWidth = spanWidth; |
||||||
|
} |
||||||
|
|
||||||
|
public int getRealSpanWidth() { |
||||||
|
return realSpanWidth; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void onMeasure(int widthSpec, int heightSpec) { |
||||||
|
super.onMeasure(widthSpec, heightSpec); |
||||||
|
int spanCount = Math.max(1, getMeasuredWidth() / spanWidth); |
||||||
|
gridLayoutManager.setSpanCount(spanCount); |
||||||
|
int oldRealSpanWidth = realSpanWidth; |
||||||
|
realSpanWidth = getMeasuredWidth() / spanCount; |
||||||
|
if (realSpanWidth != oldRealSpanWidth) { |
||||||
|
getAdapter().notifyDataSetChanged(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,307 +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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/ |
|
||||||
package org.floens.chan.utils; |
|
||||||
|
|
||||||
import android.app.AlertDialog; |
|
||||||
import android.app.DownloadManager; |
|
||||||
import android.content.BroadcastReceiver; |
|
||||||
import android.content.Context; |
|
||||||
import android.content.DialogInterface; |
|
||||||
import android.content.Intent; |
|
||||||
import android.content.IntentFilter; |
|
||||||
import android.media.MediaScannerConnection; |
|
||||||
import android.net.Uri; |
|
||||||
import android.widget.Toast; |
|
||||||
|
|
||||||
import org.floens.chan.Chan; |
|
||||||
import org.floens.chan.R; |
|
||||||
import org.floens.chan.core.cache.FileCache; |
|
||||||
import org.floens.chan.core.settings.ChanSettings; |
|
||||||
|
|
||||||
import java.io.File; |
|
||||||
import java.io.FileInputStream; |
|
||||||
import java.io.FileOutputStream; |
|
||||||
import java.io.IOException; |
|
||||||
import java.io.InputStream; |
|
||||||
import java.io.OutputStream; |
|
||||||
import java.util.ArrayList; |
|
||||||
import java.util.List; |
|
||||||
import java.util.concurrent.atomic.AtomicBoolean; |
|
||||||
|
|
||||||
public class ImageSaver { |
|
||||||
private static final String TAG = "ImageSaver"; |
|
||||||
private static final ImageSaver instance = new ImageSaver(); |
|
||||||
|
|
||||||
public static ImageSaver getInstance() { |
|
||||||
return instance; |
|
||||||
} |
|
||||||
|
|
||||||
private BroadcastReceiver receiver; |
|
||||||
|
|
||||||
public void saveAll(final Context context, String folderName, final List<DownloadPair> list) { |
|
||||||
final File subFolder = new File(ChanSettings.saveLocation.get() + File.separator + filterName(folderName)); |
|
||||||
|
|
||||||
String text = context.getString(R.string.image_download_confirm, Integer.toString(list.size()), subFolder.getAbsolutePath()); |
|
||||||
|
|
||||||
new AlertDialog.Builder(context).setMessage(text).setNegativeButton(R.string.cancel, null) |
|
||||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { |
|
||||||
@Override |
|
||||||
public void onClick(DialogInterface dialog, int which) { |
|
||||||
listDownload(context, subFolder, list); |
|
||||||
} |
|
||||||
}).show(); |
|
||||||
} |
|
||||||
|
|
||||||
public void saveImage(final Context context, String imageUrl, final String name, final String extension, final boolean share) { |
|
||||||
Chan.getFileCache().downloadFile(imageUrl, new FileCache.DownloadedCallback() { |
|
||||||
@Override |
|
||||||
@SuppressWarnings("deprecation") |
|
||||||
public void onProgress(long downloaded, long total, boolean done) { |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onSuccess(final File source) { |
|
||||||
onFileDownloaded(context, name, extension, source, share); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onFail(boolean notFound) { |
|
||||||
Toast.makeText(context, R.string.image_open_failed, Toast.LENGTH_LONG).show(); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
private void onFileDownloaded(final Context context, final String name, final String extension, final File downloaded, boolean share) { |
|
||||||
if (share) { |
|
||||||
scanFile(context, downloaded, true); |
|
||||||
} else { |
|
||||||
/*if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { |
|
||||||
Toast.makeText(context, R.string.image_save_not_mounted, Toast.LENGTH_LONG).show(); |
|
||||||
return; |
|
||||||
}*/ |
|
||||||
|
|
||||||
new Thread(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
File saveDir = new File(ChanSettings.saveLocation.get()); |
|
||||||
|
|
||||||
if (!saveDir.isDirectory() && !saveDir.mkdirs()) { |
|
||||||
showToast(context, context.getString(R.string.image_save_directory_error)); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
String fileName = filterName(name + "." + extension); |
|
||||||
File destination = findUnused(new File(saveDir, fileName), false); |
|
||||||
|
|
||||||
final boolean success = storeImage(downloaded, destination); |
|
||||||
if (success) { |
|
||||||
scanFile(context, destination, false); |
|
||||||
showToast(context, context.getString(R.string.image_save_succeeded) + " " + destination.getAbsolutePath()); |
|
||||||
} else { |
|
||||||
showToast(context, context.getString(R.string.image_save_failed)); |
|
||||||
} |
|
||||||
} |
|
||||||
}).start(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private void showToast(final Context context, final String message) { |
|
||||||
AndroidUtils.runOnUiThread(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show(); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
private void listDownload(Context context, File subFolder, final List<DownloadPair> list) { |
|
||||||
final DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); |
|
||||||
|
|
||||||
if (!subFolder.isDirectory() && !subFolder.mkdirs()) { |
|
||||||
Toast.makeText(context, R.string.image_save_directory_error, Toast.LENGTH_LONG).show(); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
final List<Pair> files = new ArrayList<>(list.size()); |
|
||||||
for (DownloadPair uri : list) { |
|
||||||
File destination = new File(subFolder, filterName(uri.imageName)); |
|
||||||
if (destination.exists()) continue; |
|
||||||
|
|
||||||
Pair p = new Pair(); |
|
||||||
p.uri = Uri.parse(uri.imageUrl); |
|
||||||
p.file = destination; |
|
||||||
files.add(p); |
|
||||||
} |
|
||||||
|
|
||||||
final AtomicBoolean stopped = new AtomicBoolean(false); |
|
||||||
final List<Long> ids = new ArrayList<>(); |
|
||||||
|
|
||||||
new Thread(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
for (Pair pair : files) { |
|
||||||
if (stopped.get()) { |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
DownloadManager.Request request; |
|
||||||
try { |
|
||||||
request = new DownloadManager.Request(pair.uri); |
|
||||||
} catch (IllegalArgumentException e) { |
|
||||||
continue; |
|
||||||
} |
|
||||||
|
|
||||||
request.setDestinationUri(Uri.fromFile(pair.file)); |
|
||||||
request.setVisibleInDownloadsUi(false); |
|
||||||
request.allowScanningByMediaScanner(); |
|
||||||
|
|
||||||
synchronized (stopped) { |
|
||||||
if (stopped.get()) { |
|
||||||
break; |
|
||||||
} |
|
||||||
ids.add(dm.enqueue(request)); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}).start(); |
|
||||||
|
|
||||||
receiver = new BroadcastReceiver() { |
|
||||||
@Override |
|
||||||
public void onReceive(Context context, Intent intent) { |
|
||||||
if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(intent.getAction())) { |
|
||||||
synchronized (stopped) { |
|
||||||
stopped.set(true); |
|
||||||
|
|
||||||
for (Long id : ids) { |
|
||||||
dm.remove(id); |
|
||||||
} |
|
||||||
} |
|
||||||
} else if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { |
|
||||||
if (receiver != null) { |
|
||||||
context.unregisterReceiver(receiver); |
|
||||||
receiver = null; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
IntentFilter filter = new IntentFilter(); |
|
||||||
filter.addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED); |
|
||||||
filter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE); |
|
||||||
|
|
||||||
context.registerReceiver(receiver, filter); |
|
||||||
} |
|
||||||
|
|
||||||
private String filterName(String name) { |
|
||||||
return name.replaceAll("[^a-zA-Z0-9.]", "_"); |
|
||||||
} |
|
||||||
|
|
||||||
private File findUnused(File start, boolean isDir) { |
|
||||||
String base; |
|
||||||
String extension; |
|
||||||
|
|
||||||
if (isDir) { |
|
||||||
base = start.getAbsolutePath(); |
|
||||||
extension = null; |
|
||||||
} else { |
|
||||||
String[] splitted = start.getAbsolutePath().split("\\.(?=[^\\.]+$)"); |
|
||||||
if (splitted.length == 2) { |
|
||||||
base = splitted[0]; |
|
||||||
extension = "." + splitted[1]; |
|
||||||
} else { |
|
||||||
base = splitted[0]; |
|
||||||
extension = "."; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
File test; |
|
||||||
if (isDir) { |
|
||||||
test = new File(base); |
|
||||||
} else { |
|
||||||
test = new File(base + extension); |
|
||||||
} |
|
||||||
int index = 0; |
|
||||||
int tries = 0; |
|
||||||
while (test.exists() && tries++ < 100) { |
|
||||||
if (isDir) { |
|
||||||
test = new File(base + "_" + index); |
|
||||||
} else { |
|
||||||
test = new File(base + "_" + index + extension); |
|
||||||
} |
|
||||||
index++; |
|
||||||
} |
|
||||||
|
|
||||||
return test; |
|
||||||
} |
|
||||||
|
|
||||||
private boolean storeImage(final File source, final File destination) { |
|
||||||
boolean res = true; |
|
||||||
InputStream is = null; |
|
||||||
OutputStream os = null; |
|
||||||
try { |
|
||||||
is = new FileInputStream(source); |
|
||||||
os = new FileOutputStream(destination); |
|
||||||
IOUtils.copy(is, os); |
|
||||||
} catch (IOException e) { |
|
||||||
res = false; |
|
||||||
} finally { |
|
||||||
IOUtils.closeQuietly(is); |
|
||||||
IOUtils.closeQuietly(os); |
|
||||||
} |
|
||||||
|
|
||||||
return res; |
|
||||||
} |
|
||||||
|
|
||||||
private void scanFile(final Context context, final File file, final boolean shareAfterwards) { |
|
||||||
MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null, |
|
||||||
new MediaScannerConnection.OnScanCompletedListener() { |
|
||||||
@Override |
|
||||||
public void onScanCompleted(String unused, final Uri uri) { |
|
||||||
AndroidUtils.runOnUiThread(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
Logger.d(TAG, "Media scan succeeded: " + uri); |
|
||||||
|
|
||||||
if (shareAfterwards) { |
|
||||||
Intent intent = new Intent(Intent.ACTION_SEND); |
|
||||||
intent.setType("image/*"); |
|
||||||
intent.putExtra(Intent.EXTRA_STREAM, uri); |
|
||||||
context.startActivity(Intent.createChooser(intent, context.getString(R.string.action_share))); |
|
||||||
} |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
public static class DownloadPair { |
|
||||||
public String imageUrl; |
|
||||||
public String imageName; |
|
||||||
|
|
||||||
public DownloadPair(String uri, String name) { |
|
||||||
this.imageUrl = uri; |
|
||||||
this.imageName = name; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private static class Pair { |
|
||||||
public Uri uri; |
|
||||||
public File file; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,37 @@ |
|||||||
|
/* |
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
package org.floens.chan.utils; |
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView; |
||||||
|
|
||||||
|
import java.lang.reflect.Field; |
||||||
|
|
||||||
|
public class RecyclerUtils { |
||||||
|
private static final String TAG = "RecyclerUtils"; |
||||||
|
|
||||||
|
public static void clearRecyclerCache(RecyclerView recyclerView) { |
||||||
|
try { |
||||||
|
Field field = RecyclerView.class.getDeclaredField("mRecycler"); |
||||||
|
field.setAccessible(true); |
||||||
|
RecyclerView.Recycler recycler = (RecyclerView.Recycler) field.get(recyclerView); |
||||||
|
recycler.clear(); |
||||||
|
} catch (Exception e) { |
||||||
|
Logger.e(TAG, "Error clearing RecyclerView cache with reflection", e); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 399 B |
After Width: | Height: | Size: 461 B |
After Width: | Height: | Size: 222 B |
After Width: | Height: | Size: 267 B |
After Width: | Height: | Size: 310 B |
After Width: | Height: | Size: 141 B |
After Width: | Height: | Size: 510 B |
After Width: | Height: | Size: 637 B |
After Width: | Height: | Size: 196 B |
After Width: | Height: | Size: 738 B |
After Width: | Height: | Size: 927 B |
After Width: | Height: | Size: 262 B |
After Width: | Height: | Size: 963 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 315 B |
@ -0,0 +1,39 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- |
||||||
|
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 <http://www.gnu.org/licenses/>. |
||||||
|
--> |
||||||
|
<FrameLayout |
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
xmlns:tools="http://schemas.android.com/tools" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="0dp" |
||||||
|
tools:ignore="ContentDescription,RtlHardcoded,RtlSymmetry"> |
||||||
|
|
||||||
|
<org.floens.chan.ui.view.ThumbnailView |
||||||
|
android:id="@+id/thumbnail_view" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent" |
||||||
|
android:padding="5dp"/> |
||||||
|
|
||||||
|
<ImageView |
||||||
|
android:id="@+id/checkbox" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:layout_gravity="left|top" |
||||||
|
android:paddingLeft="7dp" |
||||||
|
android:paddingTop="7dp"/> |
||||||
|
|
||||||
|
</FrameLayout> |
@ -0,0 +1,44 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- |
||||||
|
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 <http://www.gnu.org/licenses/>. |
||||||
|
--> |
||||||
|
<android.support.design.widget.CoordinatorLayout |
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent" |
||||||
|
android:background="?backcolor"> |
||||||
|
|
||||||
|
<org.floens.chan.ui.view.GridRecyclerView |
||||||
|
android:id="@+id/recycler_view" |
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent" |
||||||
|
android:background="#ff000000" |
||||||
|
android:clipToPadding="false" |
||||||
|
android:padding="5dp" |
||||||
|
android:paddingBottom="72dp" |
||||||
|
android:scrollbarStyle="outsideOverlay" |
||||||
|
android:scrollbars="vertical"/> |
||||||
|
|
||||||
|
<android.support.design.widget.FloatingActionButton |
||||||
|
android:id="@+id/download" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:layout_gravity="right|bottom" |
||||||
|
android:layout_margin="16dp" |
||||||
|
android:src="@drawable/ic_file_download_white_24dp"/> |
||||||
|
|
||||||
|
</android.support.design.widget.CoordinatorLayout> |
@ -1,6 +1,6 @@ |
|||||||
#Thu May 14 11:43:47 CEST 2015 |
#Tue Aug 11 17:05:27 CEST 2015 |
||||||
distributionBase=GRADLE_USER_HOME |
distributionBase=GRADLE_USER_HOME |
||||||
distributionPath=wrapper/dists |
distributionPath=wrapper/dists |
||||||
zipStoreBase=GRADLE_USER_HOME |
zipStoreBase=GRADLE_USER_HOME |
||||||
zipStorePath=wrapper/dists |
zipStorePath=wrapper/dists |
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip |
distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-bin.zip |
||||||
|