main commit

Post-processing infrastructure
* remove interfaces with one implementation
* fix download resources with unknow length
* marquee style for ProgressDrawable
* "view details" option in mission context menu
* notification for finished downloads
* postprocessing infrastructure: sub-missions, circular file, layers for layers of abstractions for Java IO streams
* Mp4 muxing (only DASH brand)
* WebM muxing
* Captions downloading
* alert dialog for overwrite existing downloads finished or not.

Misc changes
* delete SQLiteDownloadDataSource.java
* delete DownloadMissionSQLiteHelper.java
* implement Localization from #114

Misc fixes (this branch)
* restore old mission listeners variables. Prevents registered listeners get de-referenced on low-end devices
* DownloadManagerService.checkForRunningMission() now return false if the mission has a error.
* use Intent.FLAG_ACTIVITY_NEW_TASK when launching an activity from gigaget threads (apparently it is required in old versions of android)

More changes
* proper error handling "infrastructure"
* queue instead of multiple downloads
* move serialized pending downloads (.giga files) to app data
* stop downloads when swicthing to mobile network (never works, see 2nd point)
* save the thread count for next downloads
* a lot of incoherences fixed
* delete DownloadManagerTest.java (too many changes to keep this file updated)
This commit is contained in:
kapodamy 2018-09-23 15:12:23 -03:00
parent 2dc2a56671
commit 21e205a6be
48 changed files with 4379 additions and 1119 deletions

View file

@ -1,5 +1,6 @@
package us.shandian.giga.ui.adapter;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
@ -7,12 +8,20 @@ import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.FileProvider;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@ -24,28 +33,28 @@ import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.download.DeleteDownloadManager;
import org.schabi.newpipe.util.NavigationHelper;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import us.shandian.giga.get.DownloadManager;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.common.Deleter;
import us.shandian.giga.ui.common.ProgressDrawable;
import us.shandian.giga.util.Utility;
import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHolder> {
private static final Map<Integer, String> ALGORITHMS = new HashMap<>();
public class MissionAdapter extends RecyclerView.Adapter<ViewHolder> {
private static final SparseArray<String> ALGORITHMS = new SparseArray<>();
private static final String TAG = "MissionAdapter";
static {
@ -53,109 +62,131 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
ALGORITHMS.put(R.id.sha1, "SHA1");
}
private Activity mContext;
private Context mContext;
private LayoutInflater mInflater;
private DownloadManager mDownloadManager;
private DeleteDownloadManager mDeleteDownloadManager;
private List<DownloadMission> mItemList;
private DownloadManagerService.DMBinder mBinder;
private Deleter mDeleter;
private int mLayout;
private DownloadManager.MissionIterator mIterator;
private Handler mHandler;
private ArrayList<ViewHolderItem> mPendingDownloadsItems = new ArrayList<>();
private MenuItem mClear;
private View mEmptyMessage;
public MissionAdapter(Activity context, DownloadManagerService.DMBinder binder, DownloadManager downloadManager, DeleteDownloadManager deleteDownloadManager, boolean isLinear) {
public MissionAdapter(Context context, DownloadManager downloadManager, MenuItem clearButton, View emptyMessage) {
mContext = context;
mDownloadManager = downloadManager;
mDeleteDownloadManager = deleteDownloadManager;
mBinder = binder;
mDeleter = null;
mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mLayout = isLinear ? R.layout.mission_item_linear : R.layout.mission_item;
mLayout = R.layout.mission_item;
mItemList = new ArrayList<>();
updateItemList();
mHandler = new Handler(Looper.myLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DownloadManagerService.MESSAGE_PROGRESS:
case DownloadManagerService.MESSAGE_ERROR:
case DownloadManagerService.MESSAGE_FINISHED:
onServiceMessage(msg);
}
}
};
mClear = clearButton;
mEmptyMessage = emptyMessage;
mIterator = downloadManager.getIterator();
checkEmptyMessageVisibility();
}
public void updateItemList() {
mItemList.clear();
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
switch (viewType) {
case DownloadManager.SPECIAL_PENDING:
case DownloadManager.SPECIAL_FINISHED:
return new ViewHolderHeader(mInflater.inflate(R.layout.missions_header, parent, false));
}
for (int i = 0; i < mDownloadManager.getCount(); i++) {
DownloadMission mission = mDownloadManager.getMission(i);
if (!mDeleteDownloadManager.contains(mission)) {
mItemList.add(mDownloadManager.getMission(i));
return new ViewHolderItem(mInflater.inflate(mLayout, parent, false));
}
@Override
public void onViewRecycled(@NonNull ViewHolder view) {
super.onViewRecycled(view);
if (view instanceof ViewHolderHeader) return;
ViewHolderItem h = (ViewHolderItem) view;
if (h.item.mission instanceof DownloadMission) mPendingDownloadsItems.remove(h);
h.popupMenu.dismiss();
h.item = null;
h.lastTimeStamp = -1;
h.lastDone = -1;
h.lastCurrent = -1;
h.state = 0;
}
@Override
@SuppressLint("SetTextI18n")
public void onBindViewHolder(@NonNull ViewHolder view, @SuppressLint("RecyclerView") int pos) {
DownloadManager.MissionItem item = mIterator.getItem(pos);
if (view instanceof ViewHolderHeader) {
if (item.special == DownloadManager.SPECIAL_NOTHING) return;
int str;
if (item.special == DownloadManager.SPECIAL_PENDING) {
str = R.string.missions_header_pending;
} else {
str = R.string.missions_header_finished;
mClear.setVisible(true);
}
((ViewHolderHeader) view).header.setText(str);
return;
}
ViewHolderItem h = (ViewHolderItem) view;
h.item = item;
Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.name);
h.icon.setImageResource(Utility.getIconForFileType(type));
h.name.setText(item.mission.name);
h.size.setText(Utility.formatBytes(item.mission.length));
h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type));
if (h.item.mission instanceof DownloadMission) {
DownloadMission mission = (DownloadMission) item.mission;
h.progress.setMarquee(mission.done < 1);
updateProgress(h);
h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause);
mPendingDownloadsItems.add(h);
} else {
h.progress.setMarquee(false);
h.status.setText("100%");
h.progress.setProgress(1f);
}
}
@Override
public MissionAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final ViewHolder h = new ViewHolder(mInflater.inflate(mLayout, parent, false));
h.menu.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
buildPopup(h);
}
});
/*h.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showDetail(h);
}
});*/
return h;
}
@Override
public void onViewRecycled(MissionAdapter.ViewHolder h) {
super.onViewRecycled(h);
h.mission.removeListener(h.observer);
h.mission = null;
h.observer = null;
h.progress = null;
h.position = -1;
h.lastTimeStamp = -1;
h.lastDone = -1;
h.colorId = 0;
}
@Override
public void onBindViewHolder(MissionAdapter.ViewHolder h, int pos) {
DownloadMission ms = mItemList.get(pos);
h.mission = ms;
h.position = pos;
Utility.FileType type = Utility.getFileType(ms.name);
h.icon.setImageResource(Utility.getIconForFileType(type));
h.name.setText(ms.name);
h.size.setText(Utility.formatBytes(ms.length));
h.progress = new ProgressDrawable(mContext, Utility.getBackgroundForFileType(type), Utility.getForegroundForFileType(type));
ViewCompat.setBackground(h.bkg, h.progress);
h.observer = new MissionObserver(this, h);
ms.addListener(h.observer);
updateProgress(h);
}
@Override
public int getItemCount() {
return mItemList.size();
return mIterator.getOldListSize();
}
@Override
public long getItemId(int position) {
return position;
public int getItemViewType(int position) {
return mIterator.getSpecialAtItem(position);
}
private void updateProgress(ViewHolder h) {
updateProgress(h, false);
}
private void updateProgress(ViewHolderItem h) {
if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return;
private void updateProgress(ViewHolder h, boolean finished) {
if (h.mission == null) return;
DownloadMission mission = (DownloadMission) h.item.mission;
long now = System.currentTimeMillis();
@ -164,130 +195,110 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
}
if (h.lastDone == -1) {
h.lastDone = h.mission.done;
h.lastDone = mission.done;
}
if (h.lastCurrent != mission.current) {
h.lastCurrent = mission.current;
h.lastDone = 0;
h.lastTimeStamp = now;
}
long deltaTime = now - h.lastTimeStamp;
long deltaDone = h.mission.done - h.lastDone;
long deltaDone = mission.done - h.lastDone;
boolean hasError = mission.errCode != DownloadMission.ERROR_NOTHING;
if (deltaTime == 0 || deltaTime > 1000 || finished) {
if (h.mission.errCode > 0) {
h.status.setText(R.string.msg_error);
if (hasError || deltaTime == 0 || deltaTime > 1000) {
// on error hide marquee or show if condition (mission.done < 1 || mission.unknownLength) is true
h.progress.setMarquee(!hasError && (mission.done < 1 || mission.unknownLength));
float progress;
if (mission.unknownLength) {
progress = Float.NaN;
h.progress.setProgress(0f);
} else {
float progress = (float) h.mission.done / h.mission.length;
h.status.setText(String.format(Locale.US, "%.2f%%", progress * 100));
progress = (float) mission.done / mission.length;
if (mission.urls.length > 1 && mission.current < mission.urls.length) {
progress = (progress / mission.urls.length) + ((float) mission.current / mission.urls.length);
}
}
if (hasError) {
if (Float.isNaN(progress) || Float.isInfinite(progress)) h.progress.setProgress(1f);
h.status.setText(R.string.msg_error);
} else if (Float.isNaN(progress) || Float.isInfinite(progress)) {
h.status.setText("--.-%");
} else {
h.status.setText(String.format("%.2f%%", progress * 100));
h.progress.setProgress(progress);
}
}
long length = mission.offsets[mission.current < mission.offsets.length ? mission.current : (mission.offsets.length - 1)];
length += mission.length;
int state = 0;
if (!mission.isFinished()) {
if (!mission.running) {
state = mission.enqueued ? 1 : 2;
} else if (mission.postprocessingRunning) {
state = 3;
}
}
if (state != 0) {
if (h.state != state) {
String statusStr;
h.state = state;
switch (state) {
case 1:
statusStr = mContext.getString(R.string.queued);
break;
case 2:
statusStr = mContext.getString(R.string.paused);
break;
case 3:
statusStr = mContext.getString(R.string.post_processing);
break;
default:
statusStr = "?";
break;
}
h.size.setText(Utility.formatBytes(length).concat(" (").concat(statusStr).concat(")"));
} else if (deltaTime > 1000 && deltaDone > 0) {
h.lastTimeStamp = now;
h.lastDone = mission.done;
}
return;
}
if (deltaTime > 1000 && deltaDone > 0) {
float speed = (float) deltaDone / deltaTime;
String speedStr = Utility.formatSpeed(speed * 1000);
String sizeStr = Utility.formatBytes(h.mission.length);
String sizeStr = Utility.formatBytes(length);
h.size.setText(sizeStr + " " + speedStr);
h.size.setText(sizeStr.concat(" ").concat(speedStr));
h.lastTimeStamp = now;
h.lastDone = h.mission.done;
h.lastDone = mission.done;
}
}
private boolean viewWithFileProvider(@NonNull File file) {
if (!file.exists()) return true;
private void buildPopup(final ViewHolder h) {
PopupMenu popup = new PopupMenu(mContext, h.menu);
popup.inflate(R.menu.mission);
String ext = Utility.getFileExt(file.getName());
if (ext == null) return false;
Menu menu = popup.getMenu();
MenuItem start = menu.findItem(R.id.start);
MenuItem pause = menu.findItem(R.id.pause);
MenuItem view = menu.findItem(R.id.view);
MenuItem delete = menu.findItem(R.id.delete);
MenuItem checksum = menu.findItem(R.id.checksum);
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
// Set to false first
start.setVisible(false);
pause.setVisible(false);
view.setVisible(false);
delete.setVisible(false);
checksum.setVisible(false);
if (!h.mission.finished) {
if (!h.mission.running) {
if (h.mission.errCode == -1) {
start.setVisible(true);
}
delete.setVisible(true);
} else {
pause.setVisible(true);
}
} else {
view.setVisible(true);
delete.setVisible(true);
checksum.setVisible(true);
}
popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.start:
mDownloadManager.resumeMission(h.position);
mBinder.onMissionAdded(mItemList.get(h.position));
return true;
case R.id.pause:
mDownloadManager.pauseMission(h.position);
mBinder.onMissionRemoved(mItemList.get(h.position));
h.lastTimeStamp = -1;
h.lastDone = -1;
return true;
case R.id.view:
File f = new File(h.mission.location, h.mission.name);
String ext = Utility.getFileExt(h.mission.name);
Log.d(TAG, "Viewing file: " + f.getAbsolutePath() + " ext: " + ext);
if (ext == null) {
Log.w(TAG, "Can't view file because it has no extension: " +
h.mission.name);
return false;
}
String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
Log.v(TAG, "Mime: " + mime + " package: " + mContext.getApplicationContext().getPackageName() + ".provider");
if (f.exists()) {
viewFileWithFileProvider(f, mime);
} else {
Log.w(TAG, "File doesn't exist");
}
return true;
case R.id.delete:
mDeleteDownloadManager.add(h.mission);
updateItemList();
notifyDataSetChanged();
return true;
case R.id.md5:
case R.id.sha1:
DownloadMission mission = mItemList.get(h.position);
new ChecksumTask(mContext).execute(mission.location + "/" + mission.name, ALGORITHMS.get(id));
return true;
default:
return false;
}
}
});
popup.show();
}
private void viewFileWithFileProvider(File file, String mimetype) {
String ourPackage = mContext.getApplicationContext().getPackageName();
Uri uri = FileProvider.getUriForFile(mContext, ourPackage + ".provider", file);
Uri uri = FileProvider.getUriForFile(mContext, BuildConfig.APPLICATION_ID + ".provider", file);
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(uri, mimetype);
intent.setDataAndType(uri, mimeType);
intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION);
@ -300,75 +311,338 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
Toast noPlayerToast = Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG);
noPlayerToast.show();
}
return true;
}
static class ViewHolder extends RecyclerView.ViewHolder {
public DownloadMission mission;
public int position;
public Handler getMessenger() {
return mHandler;
}
public final TextView status;
public final ImageView icon;
public final TextView name;
public final TextView size;
public final View bkg;
public final ImageView menu;
public ProgressDrawable progress;
public MissionObserver observer;
private void onServiceMessage(@NonNull Message msg) {
switch (msg.what) {
case DownloadManagerService.MESSAGE_PROGRESS:
case DownloadManagerService.MESSAGE_ERROR:
case DownloadManagerService.MESSAGE_FINISHED:
break;
default:
return;
}
public long lastTimeStamp = -1;
public long lastDone = -1;
public int colorId;
for (int i = 0; i < mPendingDownloadsItems.size(); i++) {
ViewHolderItem h = mPendingDownloadsItems.get(i);
if (h.item.mission != msg.obj) continue;
public ViewHolder(View v) {
super(v);
if (msg.what == DownloadManagerService.MESSAGE_FINISHED) {
// DownloadManager should mark the download as finished
applyChanges();
status = v.findViewById(R.id.item_status);
icon = v.findViewById(R.id.item_icon);
name = v.findViewById(R.id.item_name);
size = v.findViewById(R.id.item_size);
bkg = v.findViewById(R.id.item_bkg);
menu = v.findViewById(R.id.item_more);
mPendingDownloadsItems.remove(i);
return;
}
updateProgress(h);
return;
}
}
static class MissionObserver implements DownloadMission.MissionListener {
private final MissionAdapter mAdapter;
private final ViewHolder mHolder;
private void showError(@NonNull DownloadMission mission) {
StringBuilder str = new StringBuilder();
str.append(mContext.getString(R.string.label_code));
str.append(": ");
str.append(mission.errCode);
str.append('\n');
public MissionObserver(MissionAdapter adapter, ViewHolder holder) {
mAdapter = adapter;
mHolder = holder;
switch (mission.errCode) {
case 416:
str.append(mContext.getString(R.string.error_http_requested_range_not_satisfiable));
break;
case 404:
str.append(mContext.getString(R.string.error_http_not_found));
break;
case DownloadMission.ERROR_NOTHING:
str.append("¿?");
break;
case DownloadMission.ERROR_FILE_CREATION:
str.append(mContext.getString(R.string.error_file_creation));
break;
case DownloadMission.ERROR_HTTP_NO_CONTENT:
str.append(mContext.getString(R.string.error_http_no_content));
break;
case DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE:
str.append(mContext.getString(R.string.error_http_unsupported_range));
break;
case DownloadMission.ERROR_PATH_CREATION:
str.append(mContext.getString(R.string.error_path_creation));
break;
case DownloadMission.ERROR_PERMISSION_DENIED:
str.append(mContext.getString(R.string.permission_denied));
break;
case DownloadMission.ERROR_SSL_EXCEPTION:
str.append(mContext.getString(R.string.error_ssl_exception));
break;
case DownloadMission.ERROR_UNKNOWN_HOST:
str.append(mContext.getString(R.string.error_unknown_host));
break;
case DownloadMission.ERROR_CONNECT_HOST:
str.append(mContext.getString(R.string.error_connect_host));
break;
case DownloadMission.ERROR_POSTPROCESSING_FAILED:
str.append(R.string.error_postprocessing_failed);
case DownloadMission.ERROR_UNKNOWN_EXCEPTION:
break;
default:
if (mission.errCode >= 100 && mission.errCode < 600) {
str.append("HTTP");
} else if (mission.errObject == null) {
str.append("(not_decelerated_error_code)");
}
break;
}
@Override
public void onProgressUpdate(DownloadMission downloadMission, long done, long total) {
mAdapter.updateProgress(mHolder);
if (mission.errObject != null) {
str.append("\n\n");
str.append(mission.errObject.toString());
}
@Override
public void onFinish(DownloadMission downloadMission) {
//mAdapter.mManager.deleteMission(mHolder.position);
// TODO Notification
//mAdapter.notifyDataSetChanged();
if (mHolder.mission != null) {
mHolder.size.setText(Utility.formatBytes(mHolder.mission.length));
mAdapter.updateProgress(mHolder, true);
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setTitle(mission.name)
.setMessage(str)
.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel())
.create()
.show();
}
public void clearFinishedDownloads() {
mDownloadManager.forgetFinishedDownloads();
applyChanges();
mClear.setVisible(false);
}
private boolean handlePopupItem(@NonNull ViewHolderItem h, @NonNull MenuItem option) {
int id = option.getItemId();
DownloadMission mission = h.item.mission instanceof DownloadMission ? (DownloadMission) h.item.mission : null;
if (mission != null) {
switch (id) {
case R.id.start:
h.state = -1;
h.size.setText(Utility.formatBytes(mission.length));
mDownloadManager.resumeMission(mission);
return true;
case R.id.pause:
h.state = -1;
mDownloadManager.pauseMission(mission);
notifyItemChanged(h.getAdapterPosition());
h.lastTimeStamp = -1;
h.lastDone = -1;
return true;
case R.id.error_message_view:
showError(mission);
return true;
case R.id.queue:
h.queue.setChecked(!h.queue.isChecked());
mission.enqueued = h.queue.isChecked();
updateProgress(h);
return true;
}
}
@Override
public void onError(DownloadMission downloadMission, int errCode) {
mAdapter.updateProgress(mHolder);
switch (id) {
case R.id.open:
return viewWithFileProvider(h.item.mission.getDownloadedFile());
case R.id.delete:
if (mDeleter == null) {
mDownloadManager.deleteMission(h.item.mission);
} else {
mDeleter.append(h.item.mission);
}
applyChanges();
return true;
case R.id.md5:
case R.id.sha1:
new ChecksumTask(mContext).execute(h.item.mission.getDownloadedFile().getAbsolutePath(), ALGORITHMS.get(id));
return true;
case R.id.source:
/*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source));
mContext.startActivity(intent);*/
try {
Intent intent = NavigationHelper.getIntentByLink(mContext, h.item.mission.source);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
} catch (Exception e) {
Log.w(TAG, "Selected item has a invalid source", e);
}
return true;
default:
return false;
}
}
private static class ChecksumTask extends AsyncTask<String, Void, String> {
ProgressDialog prog;
final WeakReference<Activity> weakReference;
public void applyChanges() {
mIterator.start();
DiffUtil.calculateDiff(mIterator, true).dispatchUpdatesTo(this);
mIterator.end();
ChecksumTask(@NonNull Activity activity) {
weakReference = new WeakReference<>(activity);
checkEmptyMessageVisibility();
if (mIterator.getOldListSize() > 0) {
int lastItemType = mIterator.getSpecialAtItem(mIterator.getOldListSize() - 1);
mClear.setVisible(lastItemType == DownloadManager.SPECIAL_FINISHED);
}
}
public void forceUpdate() {
mIterator.start();
mIterator.end();
notifyDataSetChanged();
}
public void setLinear(boolean isLinear) {
mLayout = isLinear ? R.layout.mission_item_linear : R.layout.mission_item;
}
private void checkEmptyMessageVisibility() {
int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE;
if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag);
}
public void deleterDispose(Bundle bundle) {
if (mDeleter != null) mDeleter.dispose(bundle);
}
public void deleterLoad(Bundle bundle, View view) {
if (mDeleter == null)
mDeleter = new Deleter(bundle, view, mContext, this, mDownloadManager, mIterator, mHandler);
}
public void deleterResume() {
if (mDeleter != null) mDeleter.resume();
}
class ViewHolderItem extends RecyclerView.ViewHolder {
DownloadManager.MissionItem item;
TextView status;
ImageView icon;
TextView name;
TextView size;
ProgressDrawable progress;
PopupMenu popupMenu;
MenuItem start;
MenuItem pause;
MenuItem open;
MenuItem queue;
MenuItem showError;
MenuItem delete;
MenuItem source;
MenuItem checksum;
long lastTimeStamp = -1;
long lastDone = -1;
int lastCurrent = -1;
int state = 0;
ViewHolderItem(View view) {
super(view);
progress = new ProgressDrawable();
ViewCompat.setBackground(itemView.findViewById(R.id.item_bkg), progress);
status = itemView.findViewById(R.id.item_status);
name = itemView.findViewById(R.id.item_name);
icon = itemView.findViewById(R.id.item_icon);
size = itemView.findViewById(R.id.item_size);
name.setSelected(true);
ImageView button = itemView.findViewById(R.id.item_more);
popupMenu = buildPopup(button);
button.setOnClickListener(v -> showPopupMenu());
Menu menu = popupMenu.getMenu();
start = menu.findItem(R.id.start);
pause = menu.findItem(R.id.pause);
open = menu.findItem(R.id.open);
queue = menu.findItem(R.id.queue);
showError = menu.findItem(R.id.error_message_view);
delete = menu.findItem(R.id.delete);
source = menu.findItem(R.id.source);
checksum = menu.findItem(R.id.checksum);
//h.itemView.setOnClickListener(v -> showDetail(h));
}
private void showPopupMenu() {
start.setVisible(false);
pause.setVisible(false);
open.setVisible(false);
queue.setVisible(false);
showError.setVisible(false);
delete.setVisible(false);
source.setVisible(false);
checksum.setVisible(false);
DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null;
if (mission != null) {
if (!mission.postprocessingRunning) {
if (mission.running) {
pause.setVisible(true);
} else {
if (mission.errCode != DownloadMission.ERROR_NOTHING) {
showError.setVisible(true);
}
queue.setChecked(mission.enqueued);
start.setVisible(mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED);
delete.setVisible(true);
queue.setVisible(true);
}
}
} else {
open.setVisible(true);
delete.setVisible(true);
checksum.setVisible(true);
}
if (item.mission.source != null && !item.mission.source.isEmpty()) {
source.setVisible(true);
}
popupMenu.show();
}
private PopupMenu buildPopup(final View button) {
PopupMenu popup = new PopupMenu(mContext, button);
popup.inflate(R.menu.mission);
popup.setOnMenuItemClickListener(option -> handlePopupItem(this, option));
return popup;
}
}
class ViewHolderHeader extends RecyclerView.ViewHolder {
TextView header;
ViewHolderHeader(View view) {
super(view);
header = itemView.findViewById(R.id.item_name);
}
}
static class ChecksumTask extends AsyncTask<String, Void, String> {
ProgressDialog progressDialog;
WeakReference<Activity> weakReference;
ChecksumTask(@NonNull Context context) {
weakReference = new WeakReference<>((Activity) context);
}
@Override
@ -378,10 +652,10 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
Activity activity = getActivity();
if (activity != null) {
// Create dialog
prog = new ProgressDialog(activity);
prog.setCancelable(false);
prog.setMessage(activity.getString(R.string.msg_wait));
prog.show();
progressDialog = new ProgressDialog(activity);
progressDialog.setCancelable(false);
progressDialog.setMessage(activity.getString(R.string.msg_wait));
progressDialog.show();
}
}
@ -394,10 +668,10 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
protected void onPostExecute(String result) {
super.onPostExecute(result);
if (prog != null) {
Utility.copyToClipboard(prog.getContext(), result);
if (progressDialog != null) {
Utility.copyToClipboard(progressDialog.getContext(), result);
if (getActivity() != null) {
prog.dismiss();
progressDialog.dismiss();
}
}
}
@ -413,4 +687,5 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
}
}
}
}

View file

@ -0,0 +1,169 @@
package us.shandian.giga.ui.common;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.Snackbar;
import android.view.View;
import org.schabi.newpipe.R;
import java.util.ArrayList;
import us.shandian.giga.get.Mission;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManager.MissionIterator;
import us.shandian.giga.ui.adapter.MissionAdapter;
public class Deleter {
private static final int TIMEOUT = 5000;// ms
private static final int DELAY = 350;// ms
private static final String BUNDLE_NAMES = "us.shandian.giga.ui.common.deleter.names";
private static final String BUNDLE_LOCATIONS = "us.shandian.giga.ui.common.deleter.locations";
private Snackbar snackbar;
private ArrayList<Mission> items;
private boolean running = true;
private Context mContext;
private MissionAdapter mAdapter;
private DownloadManager mDownloadManager;
private MissionIterator mIterator;
private Handler mHandler;
private View mView;
private final Runnable rShow;
private final Runnable rNext;
private final Runnable rCommit;
public Deleter(Bundle b, View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) {
mView = v;
mContext = c;
mAdapter = a;
mDownloadManager = d;
mIterator = i;
mHandler = h;
// use variables to know the reference of the lambdas
rShow = this::show;
rNext = this::next;
rCommit = this::commit;
items = new ArrayList<>(2);
if (b != null) {
String[] names = b.getStringArray(BUNDLE_NAMES);
String[] locations = b.getStringArray(BUNDLE_LOCATIONS);
if (names == null || locations == null) return;
if (names.length < 1 || locations.length < 1) return;
if (names.length != locations.length) return;
items.ensureCapacity(names.length);
for (int j = 0; j < locations.length; j++) {
Mission mission = mDownloadManager.getAnyMission(locations[j], names[j]);
if (mission == null) continue;
items.add(mission);
mIterator.hide(mission);
}
if (items.size() > 0) resume();
}
}
public void append(Mission item) {
mIterator.hide(item);
items.add(0, item);
show();
}
private void forget() {
mIterator.unHide(items.remove(0));
mAdapter.applyChanges();
show();
}
private void show() {
if (items.size() < 1) return;
pause();
running = true;
mHandler.postDelayed(rNext, DELAY);
}
private void next() {
if (items.size() < 1) return;
String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).name);
snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
snackbar.setAction(R.string.undo, s -> forget());
snackbar.setActionTextColor(Color.YELLOW);
snackbar.show();
mHandler.postDelayed(rCommit, TIMEOUT);
}
private void commit() {
if (items.size() < 1) return;
while (items.size() > 0) {
Mission mission = items.remove(0);
if (mission.deleted) continue;
mIterator.unHide(mission);
mDownloadManager.deleteMission(mission);
break;
}
if (items.size() < 1) {
pause();
return;
}
show();
}
private void pause() {
running = false;
mHandler.removeCallbacks(rNext);
mHandler.removeCallbacks(rShow);
mHandler.removeCallbacks(rCommit);
if (snackbar != null) snackbar.dismiss();
}
public void resume() {
if (running) return;
mHandler.postDelayed(rShow, (int) (DELAY * 1.5f));// 150% of the delay
}
public void dispose(Bundle bundle) {
if (items.size() < 1) return;
pause();
if (bundle == null) {
for (Mission mission : items) mDownloadManager.deleteMission(mission);
items = null;
return;
}
String[] names = new String[items.size()];
String[] locations = new String[items.size()];
for (int i = 0; i < items.size(); i++) {
Mission mission = items.get(i);
names[i] = mission.name;
locations[i] = mission.location;
}
bundle.putStringArray(BUNDLE_NAMES, names);
bundle.putStringArray(BUNDLE_LOCATIONS, locations);
}
}

View file

@ -1,25 +1,36 @@
package us.shandian.giga.ui.common;
package us.shandian.giga.ui.common;// TODO: ¡git it!
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.ColorRes;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
public class ProgressDrawable extends Drawable {
private float mProgress;
private final int mBackgroundColor;
private final int mForegroundColor;
private static final int MARQUEE_INTERVAL = 150;
public ProgressDrawable(Context context, @ColorRes int background, @ColorRes int foreground) {
this(ContextCompat.getColor(context, background), ContextCompat.getColor(context, foreground));
private float mProgress;
private int mBackgroundColor, mForegroundColor;
private Handler mMarqueeHandler;
private float mMarqueeProgress;
private Path mMarqueeLine;
private int mMarqueeSize;
private long mMarqueeNext;
public ProgressDrawable() {
mMarqueeLine = null;// marquee disabled
mMarqueeProgress = 0f;
mMarqueeSize = 0;
mMarqueeNext = 0;
}
public ProgressDrawable(int background, int foreground) {
public void setColors(@ColorInt int background, @ColorInt int foreground) {
mBackgroundColor = background;
mForegroundColor = foreground;
}
@ -29,10 +40,20 @@ public class ProgressDrawable extends Drawable {
invalidateSelf();
}
public void setMarquee(boolean marquee) {
if (marquee == (mMarqueeLine != null)) {
return;
}
mMarqueeLine = marquee ? new Path() : null;
mMarqueeHandler = marquee ? new Handler(Looper.getMainLooper()) : null;
mMarqueeSize = 0;
mMarqueeNext = 0;
}
@Override
public void draw(@NonNull Canvas canvas) {
int width = canvas.getWidth();
int height = canvas.getHeight();
int width = getBounds().width();
int height = getBounds().height();
Paint paint = new Paint();
@ -40,6 +61,42 @@ public class ProgressDrawable extends Drawable {
canvas.drawRect(0, 0, width, height, paint);
paint.setColor(mForegroundColor);
if (mMarqueeLine != null) {
if (mMarqueeSize < 1) setupMarquee(width, height);
int size = mMarqueeSize;
Paint paint2 = new Paint();
paint2.setColor(mForegroundColor);
paint2.setStrokeWidth(size);
paint2.setStyle(Paint.Style.STROKE);
size *= 2;
if (mMarqueeProgress >= size) {
mMarqueeProgress = 1;
} else {
mMarqueeProgress++;
}
// render marquee
width += size * 2;
Path marquee = new Path();
for (float i = -size; i < width; i += size) {
marquee.addPath(mMarqueeLine, i + mMarqueeProgress, 0);
}
marquee.close();
canvas.drawPath(marquee, paint2);// draw marquee
if (System.currentTimeMillis() >= mMarqueeNext) {
// program next update
mMarqueeNext = System.currentTimeMillis() + MARQUEE_INTERVAL;
mMarqueeHandler.postDelayed(this::invalidateSelf, MARQUEE_INTERVAL);
}
return;
}
canvas.drawRect(0, 0, (int) (mProgress * width), height, paint);
}
@ -58,4 +115,17 @@ public class ProgressDrawable extends Drawable {
return PixelFormat.OPAQUE;
}
@Override
public void onBoundsChange(Rect rect) {
if (mMarqueeLine != null) setupMarquee(rect.width(), rect.height());
}
private void setupMarquee(int width, int height) {
mMarqueeSize = (int) ((width * 10f) / 100f);// the size is 10% of the width
mMarqueeLine.rewind();
mMarqueeLine.moveTo(-mMarqueeSize, -mMarqueeSize);
mMarqueeLine.lineTo(-mMarqueeSize * 4, height + mMarqueeSize);
mMarqueeLine.close();
}
}

View file

@ -10,8 +10,6 @@ import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@ -23,39 +21,47 @@ import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.download.DeleteDownloadManager;
import io.reactivex.disposables.Disposable;
import us.shandian.giga.get.DownloadManager;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.service.DownloadManagerService.DMBinder;
import us.shandian.giga.ui.adapter.MissionAdapter;
public abstract class MissionsFragment extends Fragment {
private DownloadManager mDownloadManager;
private DownloadManagerService.DMBinder mBinder;
public class MissionsFragment extends Fragment {
private static final int SPAN_SIZE = 2;
private SharedPreferences mPrefs;
private boolean mLinear;
private MenuItem mSwitch;
private MenuItem mClear;
private RecyclerView mList;
private View mEmpty;
private MissionAdapter mAdapter;
private GridLayoutManager mGridManager;
private LinearLayoutManager mLinearManager;
private Context mActivity;
private DeleteDownloadManager mDeleteDownloadManager;
private Disposable mDeleteDisposable;
private DMBinder mBinder;
private Bundle mBundle;
private boolean mForceUpdate;
private final ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
mBinder = (DownloadManagerService.DMBinder) binder;
mDownloadManager = setupDownloadManager(mBinder);
if (mDeleteDownloadManager != null) {
mDeleteDownloadManager.setDownloadManager(mDownloadManager);
updateList();
}
mBinder.resetFinishedDownloadCount();
mAdapter = new MissionAdapter(mActivity, mBinder.getDownloadManager(), mClear, mEmpty);
mAdapter.deleterLoad(mBundle, getView());
mBundle = null;
mBinder.addMissionEventListener(mAdapter.getMessenger());
updateList();
}
@Override
@ -66,14 +72,6 @@ public abstract class MissionsFragment extends Fragment {
};
public void setDeleteManager(@NonNull DeleteDownloadManager deleteDownloadManager) {
mDeleteDownloadManager = deleteDownloadManager;
if (mDownloadManager != null) {
mDeleteDownloadManager.setDownloadManager(mDownloadManager);
updateList();
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.missions, container, false);
@ -81,24 +79,47 @@ public abstract class MissionsFragment extends Fragment {
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
mLinear = mPrefs.getBoolean("linear", false);
mActivity = getActivity();
mBundle = savedInstanceState;
// Bind the service
Intent i = new Intent();
i.setClass(getActivity(), DownloadManagerService.class);
getActivity().bindService(i, mConnection, Context.BIND_AUTO_CREATE);
mActivity.bindService(new Intent(mActivity, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
// Views
mEmpty = v.findViewById(R.id.list_empty_view);
mList = v.findViewById(R.id.mission_recycler);
// Init
mGridManager = new GridLayoutManager(getActivity(), 2);
mGridManager = new GridLayoutManager(getActivity(), SPAN_SIZE);
mGridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
switch (mAdapter.getItemViewType(position)) {
case DownloadManager.SPECIAL_PENDING:
case DownloadManager.SPECIAL_FINISHED:
return SPAN_SIZE;
default:
return 1;
}
}
});
mLinearManager = new LinearLayoutManager(getActivity());
mList.setLayoutManager(mGridManager);
setHasOptionsMenu(true);
return v;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
if (menu != null) {
mSwitch = menu.findItem(R.id.switch_mode);
mClear = menu.findItem(R.id.clear_list);
}
}
/**
* Added in API level 23.
*/
@ -108,7 +129,7 @@ public abstract class MissionsFragment extends Fragment {
// Bug: in api< 23 this is never called
// so mActivity=null
// so app crashes with nullpointer exception
// so app crashes with null-pointer exception
mActivity = activity;
}
@ -119,71 +140,78 @@ public abstract class MissionsFragment extends Fragment {
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mActivity = activity;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (mDeleteDownloadManager != null) {
mDeleteDisposable = mDeleteDownloadManager.getUndoObservable().subscribe(mission -> {
if (mAdapter != null) {
mAdapter.updateItemList();
mAdapter.notifyDataSetChanged();
}
});
public void onDestroy() {
super.onDestroy();
if (mBinder == null || mAdapter == null) return;
mBinder.removeMissionEventListener(mAdapter.getMessenger());
mActivity.unbindService(mConnection);
mAdapter.deleterDispose(null);
mBinder = null;
mAdapter = null;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mAdapter != null) {
mAdapter.deleterDispose(outState);
mForceUpdate = true;
mBinder.removeMissionEventListener(mAdapter.getMessenger());
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
getActivity().unbindService(mConnection);
if (mDeleteDisposable != null) {
mDeleteDisposable.dispose();
}
}
public void onResume() {
super.onResume();
if (mAdapter != null) {
mAdapter.deleterResume();
@Override
public void onPrepareOptionsMenu(Menu menu) {
mSwitch = menu.findItem(R.id.switch_mode);
super.onPrepareOptionsMenu(menu);
if (mForceUpdate) {
mForceUpdate = false;
mAdapter.forceUpdate();
}
mBinder.addMissionEventListener(mAdapter.getMessenger());
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.switch_mode:
mLinear = !mLinear;
updateList();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
public void notifyChange() {
mAdapter.notifyDataSetChanged();
mLinear = !mLinear;
updateList();
return true;
case R.id.clear_list:
mAdapter.clearFinishedDownloads();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void updateList() {
mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mDownloadManager, mDeleteDownloadManager, mLinear);
if (mLinear) {
mList.setLayoutManager(mLinearManager);
} else {
mList.setLayoutManager(mGridManager);
}
mList.setAdapter(null);
mAdapter.notifyDataSetChanged();
mAdapter.setLinear(mLinear);
mList.setAdapter(mAdapter);
if (mSwitch != null) {
mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list);
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list);
mPrefs.edit().putBoolean("linear", mLinear).apply();
}
mPrefs.edit().putBoolean("linear", mLinear).apply();
}
protected abstract DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder);
}