New MP4 muxer + Queue changes + Storage fixes

Main changes:
* correctly check the available space (CircularFile.java)
* misc cleanup (CircularFile.java)
* use the "Error Reporter" for non-http errors
* rewrite network state checking and add better support for API 21 (Lollipop) or higher
* implement "metered networks"
* add buttons in "Downloads" activity to start/pause all pending downloads, ignoring the queue flag or if the network is "metered"
* add workaround for VPN connections and/or network switching. Example: switching WiFi to 3G
* rewrite DataReader ¡Webm muxer is now 57% more faster!
* rewrite CircularFile, use file buffers instead of memory buffers. Less troubles in low-end devices
* fix missing offset for KaxCluster (WebMWriter.java), manifested as no thumbnails on file explorers

Download queue:
* remember queue status, unless the user pause the download (un-queue)
* semi-automatic downloads, between networks. Effective if the user create a new download or the downloads activity is starts
* allow enqueue failed downloads
* new option, queue limit, enabled by default. Used to allow one or multiple downloads at same time

Miscellaneous:
* fix crash while selecting details/error menu (mistake on MissionFragment.java)
* misc serialize changes (DownloadMission.java)
* minor UI tweaks
* allow overwrite paused downloads
* fix wrong icons for grid/list button in downloads
* add share option
* implement #2006
* correct misspelled word in strings.xml (es) (cmn)
* fix MissionAdapter crash during device shutdown

New Mp4Muxer + required changes:
* new mp4 muxer (from dash only) with this, muxing on Android 7 is possible now!!!
* re-work in SharpStream
* drop mp4 dash muxer
* misc changes: add warning in SecondaryStreamHelper.java,
* strip m4a DASH files to normal m4a format (youtube only)

Fix storage issues:
* warn to the user if is choosing a "read only" download directory (for external SD Cards), useless is rooted :)
* "write proof" allow post-processing resuming only if the device ran out of space
* implement "insufficient storage" error for downloads
This commit is contained in:
kapodamy 2019-03-22 22:54:07 -03:00
parent b19caad970
commit 8d8059229f
49 changed files with 2715 additions and 1936 deletions

View file

@ -14,15 +14,17 @@ import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
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.support.v7.widget.RecyclerView.Adapter;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.util.Log;
import android.util.SparseArray;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@ -36,14 +38,17 @@ import android.widget.Toast;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.common.Deleter;
@ -57,10 +62,13 @@ import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST;
import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION;
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT;
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE;
import static us.shandian.giga.get.DownloadMission.ERROR_INSUFFICIENT_STORAGE;
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION;
import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED;
import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST;
@ -69,6 +77,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
private static final SparseArray<String> ALGORITHMS = new SparseArray<>();
private static final String TAG = "MissionAdapter";
private static final String UNDEFINED_PROGRESS = "--.-%";
private static final String DEFAULT_MIME_TYPE = "*/*";
static {
@ -85,9 +94,11 @@ public class MissionAdapter extends Adapter<ViewHolder> {
private ArrayList<ViewHolderItem> mPendingDownloadsItems = new ArrayList<>();
private Handler mHandler;
private MenuItem mClear;
private MenuItem mStartButton;
private MenuItem mPauseButton;
private View mEmptyMessage;
public MissionAdapter(Context context, DownloadManager downloadManager, MenuItem clearButton, View emptyMessage) {
public MissionAdapter(Context context, DownloadManager downloadManager, View emptyMessage) {
mContext = context;
mDownloadManager = downloadManager;
mDeleter = null;
@ -105,10 +116,18 @@ public class MissionAdapter extends Adapter<ViewHolder> {
onServiceMessage(msg);
break;
}
if (mStartButton != null && mPauseButton != null) switch (msg.what) {
case DownloadManagerService.MESSAGE_DELETED:
case DownloadManagerService.MESSAGE_ERROR:
case DownloadManagerService.MESSAGE_FINISHED:
case DownloadManagerService.MESSAGE_PAUSED:
checkMasterButtonsVisibility();
break;
}
}
};
mClear = clearButton;
mEmptyMessage = emptyMessage;
mIterator = downloadManager.getIterator();
@ -225,8 +244,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
long deltaDone = mission.done - h.lastDone;
boolean hasError = mission.errCode != ERROR_NOTHING;
// on error hide marquee or show if condition (mission.done < 1 || mission.unknownLength) is true
h.progress.setMarquee(!hasError && (mission.done < 1 || mission.unknownLength));
// hide on error
// show if current resource length is not fetched
// show if length is unknown
h.progress.setMarquee(!hasError && (!mission.isInitialized() || mission.unknownLength));
float progress;
if (mission.unknownLength) {
@ -305,36 +326,64 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
}
private boolean viewWithFileProvider(@NonNull File file) {
if (!file.exists()) return true;
private void viewWithFileProvider(Mission mission) {
if (checkInvalidFile(mission)) return;
String ext = Utility.getFileExt(file.getName());
if (ext == null) return false;
String mimeType = resolveMimeType(mission);
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
if (BuildConfig.DEBUG)
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
Uri uri = FileProvider.getUriForFile(mContext, BuildConfig.APPLICATION_ID + ".provider", file);
Uri uri = FileProvider.getUriForFile(
mContext,
BuildConfig.APPLICATION_ID + ".provider",
mission.getDownloadedFile()
);
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
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);
}
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
}
//mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
Log.v(TAG, "Starting intent: " + intent);
if (intent.resolveActivity(mContext.getPackageManager()) != null) {
mContext.startActivity(intent);
} else {
Toast noPlayerToast = Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG);
noPlayerToast.show();
Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG).show();
}
}
private void shareFile(Mission mission) {
if (checkInvalidFile(mission)) return;
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(resolveMimeType(mission));
intent.putExtra(Intent.EXTRA_STREAM, mission.getDownloadedFile().toURI());
mContext.startActivity(Intent.createChooser(intent, null));
}
private static String resolveMimeType(@NonNull Mission mission) {
String ext = Utility.getFileExt(mission.getDownloadedFile().getName());
if (ext == null) return DEFAULT_MIME_TYPE;
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
return mimeType == null ? DEFAULT_MIME_TYPE : mimeType;
}
private boolean checkInvalidFile(@NonNull Mission mission) {
if (mission.getDownloadedFile().exists()) return false;
Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show();
return true;
}
@ -343,15 +392,9 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
private void onServiceMessage(@NonNull Message msg) {
switch (msg.what) {
case DownloadManagerService.MESSAGE_PROGRESS:
setAutoRefresh(true);
return;
case DownloadManagerService.MESSAGE_ERROR:
case DownloadManagerService.MESSAGE_FINISHED:
break;
default:
return;
if (msg.what == DownloadManagerService.MESSAGE_PROGRESS) {
setAutoRefresh(true);
return;
}
for (int i = 0; i < mPendingDownloadsItems.size(); i++) {
@ -370,74 +413,98 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
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');
@StringRes int msg = R.string.general_error;
String msgEx = null;
switch (mission.errCode) {
case 416:
str.append(mContext.getString(R.string.error_http_requested_range_not_satisfiable));
msg = R.string.error_http_requested_range_not_satisfiable;
break;
case 404:
str.append(mContext.getString(R.string.error_http_not_found));
msg = R.string.error_http_not_found;
break;
case ERROR_NOTHING:
str.append("¿?");
break;
return;// this never should happen
case ERROR_FILE_CREATION:
str.append(mContext.getString(R.string.error_file_creation));
msg = R.string.error_file_creation;
break;
case ERROR_HTTP_NO_CONTENT:
str.append(mContext.getString(R.string.error_http_no_content));
msg = R.string.error_http_no_content;
break;
case ERROR_HTTP_UNSUPPORTED_RANGE:
str.append(mContext.getString(R.string.error_http_unsupported_range));
msg = R.string.error_http_unsupported_range;
break;
case ERROR_PATH_CREATION:
str.append(mContext.getString(R.string.error_path_creation));
msg = R.string.error_path_creation;
break;
case ERROR_PERMISSION_DENIED:
str.append(mContext.getString(R.string.permission_denied));
msg = R.string.permission_denied;
break;
case ERROR_SSL_EXCEPTION:
str.append(mContext.getString(R.string.error_ssl_exception));
msg = R.string.error_ssl_exception;
break;
case ERROR_UNKNOWN_HOST:
str.append(mContext.getString(R.string.error_unknown_host));
msg = R.string.error_unknown_host;
break;
case ERROR_CONNECT_HOST:
str.append(mContext.getString(R.string.error_connect_host));
msg = R.string.error_connect_host;
break;
case ERROR_POSTPROCESSING_STOPPED:
msg = R.string.error_postprocessing_stopped;
break;
case ERROR_POSTPROCESSING:
str.append(mContext.getString(R.string.error_postprocessing_failed));
case ERROR_UNKNOWN_EXCEPTION:
case ERROR_POSTPROCESSING_HOLD:
showError(mission.errObject, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed);
return;
case ERROR_INSUFFICIENT_STORAGE:
msg = R.string.error_insufficient_storage;
break;
case ERROR_UNKNOWN_EXCEPTION:
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error);
return;
default:
if (mission.errCode >= 100 && mission.errCode < 600) {
str = new StringBuilder(8);
str.append("HTTP ");
str.append(mission.errCode);
msgEx = "HTTP " + mission.errCode;
} else if (mission.errObject == null) {
str.append("(not_decelerated_error_code)");
msgEx = "(not_decelerated_error_code)";
} else {
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, msg);
return;
}
break;
}
if (mission.errObject != null) {
str.append("\n\n");
str.append(mission.errObject.toString());
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
if (msgEx != null)
builder.setMessage(msgEx);
else
builder.setMessage(msg);
// add report button for non-HTTP errors (range 100-599)
if (mission.errObject != null && (mission.errCode < 100 || mission.errCode >= 600)) {
@StringRes final int mMsg = msg;
builder.setPositiveButton(R.string.error_report_title, (dialog, which) ->
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, mMsg)
);
}
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setTitle(mission.name)
.setMessage(str)
.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel())
builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel())
.setTitle(mission.name)
.create()
.show();
}
private void showError(Exception exception, UserAction action, @StringRes int reason) {
ErrorActivity.reportError(
mContext,
Collections.singletonList(exception),
null,
null,
ErrorActivity.ErrorInfo.make(action, "-", "-", reason)
);
}
public void clearFinishedDownloads() {
mDownloadManager.forgetFinishedDownloads();
applyChanges();
@ -466,16 +533,24 @@ public class MissionAdapter extends Adapter<ViewHolder> {
showError(mission);
return true;
case R.id.queue:
h.queue.setChecked(!h.queue.isChecked());
mission.enqueued = h.queue.isChecked();
boolean flag = !h.queue.isChecked();
h.queue.setChecked(flag);
mission.setEnqueued(flag);
updateProgress(h);
return true;
case R.id.retry:
mission.psContinue(true);
return true;
case R.id.cancel:
mission.psContinue(false);
return false;
}
}
switch (id) {
case R.id.open:
return viewWithFileProvider(h.item.mission.getDownloadedFile());
case R.id.menu_item_share:
shareFile(h.item.mission);
return true;
case R.id.delete:
if (mDeleter == null) {
mDownloadManager.deleteMission(h.item.mission);
@ -529,15 +604,42 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
public void setClearButton(MenuItem clearButton) {
if (mClear == null) clearButton.setVisible(mIterator.hasFinishedMissions());
if (mClear == null)
clearButton.setVisible(mIterator.hasFinishedMissions());
mClear = clearButton;
}
public void setMasterButtons(MenuItem startButton, MenuItem pauseButton) {
boolean init = mStartButton == null || mPauseButton == null;
mStartButton = startButton;
mPauseButton = pauseButton;
if (init) checkMasterButtonsVisibility();
}
private void checkEmptyMessageVisibility() {
int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE;
if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag);
}
private void checkMasterButtonsVisibility() {
boolean[] state = mIterator.hasValidPendingMissions();
mStartButton.setVisible(state[0]);
mPauseButton.setVisible(state[1]);
}
public void ensurePausedMissions() {
for (ViewHolderItem h : mPendingDownloadsItems) {
if (((DownloadMission) h.item.mission).running) continue;
updateProgress(h);
h.lastTimeStamp = -1;
h.lastDone = -1;
}
}
public void deleterDispose(Bundle bundle) {
if (mDeleter != null) mDeleter.dispose(bundle);
@ -604,6 +706,8 @@ public class MissionAdapter extends Adapter<ViewHolder> {
ProgressDrawable progress;
PopupMenu popupMenu;
MenuItem retry;
MenuItem cancel;
MenuItem start;
MenuItem pause;
MenuItem open;
@ -636,22 +740,34 @@ public class MissionAdapter extends Adapter<ViewHolder> {
button.setOnClickListener(v -> showPopupMenu());
Menu menu = popupMenu.getMenu();
retry = menu.findItem(R.id.retry);
cancel = menu.findItem(R.id.cancel);
start = menu.findItem(R.id.start);
pause = menu.findItem(R.id.pause);
open = menu.findItem(R.id.open);
open = menu.findItem(R.id.menu_item_share);
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);
itemView.setOnClickListener((v) -> {
itemView.setHapticFeedbackEnabled(true);
itemView.setOnClickListener(v -> {
if (item.mission instanceof FinishedMission)
viewWithFileProvider(item.mission.getDownloadedFile());
viewWithFileProvider(item.mission);
});
itemView.setOnLongClickListener(v -> {
v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
showPopupMenu();
return true;
});
}
private void showPopupMenu() {
retry.setVisible(false);
cancel.setVisible(false);
start.setVisible(false);
pause.setVisible(false);
open.setVisible(false);
@ -664,7 +780,16 @@ public class MissionAdapter extends Adapter<ViewHolder> {
DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null;
if (mission != null) {
if (!mission.isPsRunning()) {
if (mission.isPsRunning()) {
switch (mission.errCode) {
case ERROR_INSUFFICIENT_STORAGE:
case ERROR_POSTPROCESSING_HOLD:
retry.setVisible(true);
cancel.setVisible(true);
showError.setVisible(true);
break;
}
} else {
if (mission.running) {
pause.setVisible(true);
} else {

View file

@ -35,6 +35,8 @@ public class MissionsFragment extends Fragment {
private boolean mLinear;
private MenuItem mSwitch;
private MenuItem mClear = null;
private MenuItem mStart = null;
private MenuItem mPause = null;
private RecyclerView mList;
private View mEmpty;
@ -54,9 +56,11 @@ public class MissionsFragment extends Fragment {
mBinder = (DownloadManagerService.DMBinder) binder;
mBinder.clearDownloadNotifications();
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mClear, mEmpty);
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
mAdapter.deleterLoad(mBundle, getView());
setAdapterButtons();
mBundle = null;
mBinder.addMissionEventListener(mAdapter.getMessenger());
@ -132,7 +136,7 @@ public class MissionsFragment extends Fragment {
public void onAttach(Activity activity) {
super.onAttach(activity);
mContext = activity.getApplicationContext();
mContext = activity;
}
@ -154,7 +158,11 @@ public class MissionsFragment extends Fragment {
public void onPrepareOptionsMenu(Menu menu) {
mSwitch = menu.findItem(R.id.switch_mode);
mClear = menu.findItem(R.id.clear_list);
if (mAdapter != null) mAdapter.setClearButton(mClear);
mStart = menu.findItem(R.id.start_downloads);
mPause = menu.findItem(R.id.pause_downloads);
if (mAdapter != null) setAdapterButtons();
super.onPrepareOptionsMenu(menu);
}
@ -168,6 +176,14 @@ public class MissionsFragment extends Fragment {
case R.id.clear_list:
mAdapter.clearFinishedDownloads();
return true;
case R.id.start_downloads:
item.setVisible(false);
mBinder.getDownloadManager().startAllMissions();
return true;
case R.id.pause_downloads:
item.setVisible(false);
mBinder.getDownloadManager().pauseAllMissions(false);
mAdapter.ensurePausedMissions();// update items view
default:
return super.onOptionsItemSelected(item);
}
@ -193,9 +209,9 @@ public class MissionsFragment extends Fragment {
int icon;
if (mLinear)
icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp;
else
icon = isLight ? R.drawable.ic_grid_black_24dp : R.drawable.ic_grid_white_24dp;
else
icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp;
mSwitch.setIcon(icon);
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list);
@ -203,6 +219,13 @@ public class MissionsFragment extends Fragment {
}
}
private void setAdapterButtons() {
if (mClear == null || mStart == null || mPause == null) return;
mAdapter.setClearButton(mClear);
mAdapter.setMasterButtons(mStart, mPause);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);