Implement Storage Access Framework

* re-work finished mission database
* re-work DownloadMission and bump it Serializable version
* keep the classic Java IO API
* SAF Tree API support on Android Lollipop or higher
* add wrapper for SAF stream opening
* implement Closeable in SharpStream to replace the dispose() method

* do required changes for this API:
** remove any file creation logic from DownloadInitializer
** make PostProcessing Serializable and reduce the number of iterations
** update all strings.xml files
** storage helpers: StoredDirectoryHelper & StoredFileHelper
** best effort to handle any kind of SAF errors/exceptions
This commit is contained in:
kapodamy 2019-04-05 14:45:39 -03:00
parent 8d8059229f
commit 7ca7952790
62 changed files with 2439 additions and 1180 deletions

View file

@ -8,7 +8,6 @@ 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;
@ -49,6 +48,7 @@ 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.io.StoredFileHelper;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.common.Deleter;
@ -69,6 +69,7 @@ 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_PROGRESS_LOST;
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;
@ -97,8 +98,9 @@ public class MissionAdapter extends Adapter<ViewHolder> {
private MenuItem mStartButton;
private MenuItem mPauseButton;
private View mEmptyMessage;
private RecoverHelper mRecover;
public MissionAdapter(Context context, DownloadManager downloadManager, View emptyMessage) {
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage) {
mContext = context;
mDownloadManager = downloadManager;
mDeleter = null;
@ -156,7 +158,11 @@ public class MissionAdapter extends Adapter<ViewHolder> {
if (h.item.mission instanceof DownloadMission) {
mPendingDownloadsItems.remove(h);
if (mPendingDownloadsItems.size() < 1) setAutoRefresh(false);
if (mPendingDownloadsItems.size() < 1) {
setAutoRefresh(false);
if (mStartButton != null) mStartButton.setVisible(false);
if (mPauseButton != null) mPauseButton.setVisible(false);
}
}
h.popupMenu.dismiss();
@ -189,10 +195,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
ViewHolderItem h = (ViewHolderItem) view;
h.item = item;
Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.name);
Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.storage.getName());
h.icon.setImageResource(Utility.getIconForFileType(type));
h.name.setText(item.mission.name);
h.name.setText(item.mission.storage.getName());
h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type));
@ -273,7 +279,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
long length = mission.getLength();
int state;
if (mission.isPsFailed()) {
if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) {
state = 0;
} else if (!mission.running) {
state = mission.enqueued ? 1 : 2;
@ -334,11 +340,17 @@ public class MissionAdapter extends Adapter<ViewHolder> {
if (BuildConfig.DEBUG)
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
Uri uri = FileProvider.getUriForFile(
mContext,
BuildConfig.APPLICATION_ID + ".provider",
mission.getDownloadedFile()
);
Uri uri;
if (mission.storage.isDirect()) {
uri = FileProvider.getUriForFile(
mContext,
BuildConfig.APPLICATION_ID + ".provider",
mission.storage.getIOFile()
);
} else {
uri = mission.storage.getUri();
}
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
@ -366,13 +378,13 @@ public class MissionAdapter extends Adapter<ViewHolder> {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(resolveMimeType(mission));
intent.putExtra(Intent.EXTRA_STREAM, mission.getDownloadedFile().toURI());
intent.putExtra(Intent.EXTRA_STREAM, mission.storage.getUri());
mContext.startActivity(Intent.createChooser(intent, null));
}
private static String resolveMimeType(@NonNull Mission mission) {
String ext = Utility.getFileExt(mission.getDownloadedFile().getName());
String ext = Utility.getFileExt(mission.storage.getName());
if (ext == null) return DEFAULT_MIME_TYPE;
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
@ -381,7 +393,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
private boolean checkInvalidFile(@NonNull Mission mission) {
if (mission.getDownloadedFile().exists()) return false;
if (mission.storage.existsAsFile()) return false;
Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show();
return true;
@ -462,6 +474,8 @@ public class MissionAdapter extends Adapter<ViewHolder> {
case ERROR_UNKNOWN_EXCEPTION:
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error);
return;
case ERROR_PROGRESS_LOST:
msg = R.string.error_progress_lost;
default:
if (mission.errCode >= 100 && mission.errCode < 600) {
msgEx = "HTTP " + mission.errCode;
@ -490,7 +504,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel())
.setTitle(mission.name)
.setTitle(mission.storage.getName())
.create()
.show();
}
@ -539,6 +553,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
updateProgress(h);
return true;
case R.id.retry:
if (mission.hasInvalidStorage()) {
mRecover.tryRecover(mission);
return true;
}
mission.psContinue(true);
return true;
case R.id.cancel:
@ -561,7 +579,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
return true;
case R.id.md5:
case R.id.sha1:
new ChecksumTask(mContext).execute(h.item.mission.getDownloadedFile().getAbsolutePath(), ALGORITHMS.get(id));
new ChecksumTask(mContext).execute(h.item.mission.storage, ALGORITHMS.get(id));
return true;
case R.id.source:
/*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source));
@ -641,19 +659,38 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
public void deleterDispose(Bundle bundle) {
if (mDeleter != null) mDeleter.dispose(bundle);
public void deleterDispose(boolean commitChanges) {
if (mDeleter != null) mDeleter.dispose(commitChanges);
}
public void deleterLoad(Bundle bundle, View view) {
public void deleterLoad(View view) {
if (mDeleter == null)
mDeleter = new Deleter(bundle, view, mContext, this, mDownloadManager, mIterator, mHandler);
mDeleter = new Deleter(view, mContext, this, mDownloadManager, mIterator, mHandler);
}
public void deleterResume() {
if (mDeleter != null) mDeleter.resume();
}
public void recoverMission(DownloadMission mission, StoredFileHelper newStorage) {
for (ViewHolderItem h : mPendingDownloadsItems) {
if (mission != h.item.mission) continue;
mission.changeStorage(newStorage);
mission.errCode = DownloadMission.ERROR_NOTHING;
mission.errObject = null;
h.status.setText(UNDEFINED_PROGRESS);
h.state = -1;
h.size.setText(Utility.formatBytes(mission.getLength()));
h.progress.setMarquee(true);
mDownloadManager.resumeMission(mission);
return;
}
}
private boolean mUpdaterRunning = false;
private final Runnable rUpdater = this::updater;
@ -695,6 +732,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
return Float.isNaN(value) || Float.isInfinite(value);
}
public void setRecover(@NonNull RecoverHelper callback) {
mRecover = callback;
}
class ViewHolderItem extends RecyclerView.ViewHolder {
DownloadManager.MissionItem item;
@ -780,7 +821,11 @@ public class MissionAdapter extends Adapter<ViewHolder> {
DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null;
if (mission != null) {
if (mission.isPsRunning()) {
if (mission.hasInvalidStorage()) {
retry.setEnabled(true);
delete.setEnabled(true);
showError.setEnabled(true);
} else if (mission.isPsRunning()) {
switch (mission.errCode) {
case ERROR_INSUFFICIENT_STORAGE:
case ERROR_POSTPROCESSING_HOLD:
@ -838,7 +883,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
static class ChecksumTask extends AsyncTask<String, Void, String> {
static class ChecksumTask extends AsyncTask<Object, Void, String> {
ProgressDialog progressDialog;
WeakReference<Activity> weakReference;
@ -861,8 +906,8 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
@Override
protected String doInBackground(String... params) {
return Utility.checksum(params[0], params[1]);
protected String doInBackground(Object... params) {
return Utility.checksum((StoredFileHelper) params[0], (String) params[1]);
}
@Override
@ -889,4 +934,8 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
}
public interface RecoverHelper {
void tryRecover(DownloadMission mission);
}
}

View file

@ -3,8 +3,6 @@ package us.shandian.giga.ui.common;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.Snackbar;
import android.view.View;
@ -23,8 +21,6 @@ public class Deleter {
private static final int TIMEOUT = 5000;// ms
private static final int DELAY = 350;// ms
private static final int DELAY_RESUME = 400;// 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;
@ -41,7 +37,7 @@ public class Deleter {
private final Runnable rNext;
private final Runnable rCommit;
public Deleter(Bundle b, View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) {
public Deleter(View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) {
mView = v;
mContext = c;
mAdapter = a;
@ -55,27 +51,6 @@ public class Deleter {
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) {
@ -104,7 +79,7 @@ public class Deleter {
private void next() {
if (items.size() < 1) return;
String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).name);
String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).storage.getName());
snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
snackbar.setAction(R.string.undo, s -> forget());
@ -125,7 +100,7 @@ public class Deleter {
mDownloadManager.deleteMission(mission);
if (mission instanceof FinishedMission) {
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mission.getDownloadedFile())));
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri()));
}
break;
}
@ -151,27 +126,14 @@ public class Deleter {
mHandler.postDelayed(rShow, DELAY_RESUME);
}
public void dispose(Bundle bundle) {
public void dispose(boolean commitChanges) {
if (items.size() < 1) return;
pause();
if (bundle == null) {
for (Mission mission : items) mDownloadManager.deleteMission(mission);
items = null;
return;
}
if (!commitChanges) 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);
for (Mission mission : items) mDownloadManager.deleteMission(mission);
items = null;
}
}

View file

@ -1,7 +1,6 @@
package us.shandian.giga.ui.fragment;
import android.app.Activity;
import android.app.Fragment;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@ -10,6 +9,7 @@ import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@ -18,18 +18,24 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.IOException;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.service.DownloadManagerService.DMBinder;
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
import us.shandian.giga.ui.adapter.MissionAdapter;
public class MissionsFragment extends Fragment {
private static final int SPAN_SIZE = 2;
private static final int REQUEST_DOWNLOAD_PATH_SAF = 0x1230;
private SharedPreferences mPrefs;
private boolean mLinear;
@ -45,24 +51,32 @@ public class MissionsFragment extends Fragment {
private LinearLayoutManager mLinearManager;
private Context mContext;
private DMBinder mBinder;
private Bundle mBundle;
private DownloadManagerBinder mBinder;
private boolean mForceUpdate;
private DownloadMission unsafeMissionTarget = null;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
mBinder = (DownloadManagerService.DMBinder) binder;
mBinder = (DownloadManagerBinder) binder;
mBinder.clearDownloadNotifications();
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
mAdapter.deleterLoad(mBundle, getView());
mAdapter.deleterLoad(getView());
mAdapter.setRecover(mission ->
StoredFileHelper.requestSafWithFileCreation(
MissionsFragment.this,
REQUEST_DOWNLOAD_PATH_SAF,
mission.storage.getName(),
mission.storage.getType()
)
);
setAdapterButtons();
mBundle = null;
mBinder.addMissionEventListener(mAdapter.getMessenger());
mBinder.enableNotifications(false);
@ -84,9 +98,6 @@ public class MissionsFragment extends Fragment {
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
mLinear = mPrefs.getBoolean("linear", false);
//mContext = getActivity().getApplicationContext();
mBundle = savedInstanceState;
// Bind the service
mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
@ -148,7 +159,7 @@ public class MissionsFragment extends Fragment {
mBinder.removeMissionEventListener(mAdapter.getMessenger());
mBinder.enableNotifications(true);
mContext.unbindService(mConnection);
mAdapter.deleterDispose(null);
mAdapter.deleterDispose(true);
mBinder = null;
mAdapter = null;
@ -178,10 +189,12 @@ public class MissionsFragment extends Fragment {
return true;
case R.id.start_downloads:
item.setVisible(false);
mPause.setVisible(true);
mBinder.getDownloadManager().startAllMissions();
return true;
case R.id.pause_downloads:
item.setVisible(false);
mStart.setVisible(true);
mBinder.getDownloadManager().pauseAllMissions(false);
mAdapter.ensurePausedMissions();// update items view
default:
@ -231,7 +244,7 @@ public class MissionsFragment extends Fragment {
super.onSaveInstanceState(outState);
if (mAdapter != null) {
mAdapter.deleterDispose(outState);
mAdapter.deleterDispose(false);
mForceUpdate = true;
mBinder.removeMissionEventListener(mAdapter.getMessenger());
}
@ -260,4 +273,22 @@ public class MissionsFragment extends Fragment {
if (mAdapter != null) mAdapter.onPaused();
if (mBinder != null) mBinder.enableNotifications(true);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode != REQUEST_DOWNLOAD_PATH_SAF || resultCode != Activity.RESULT_OK) return;
if (unsafeMissionTarget == null || data.getData() == null) {
return;// unsafeMissionTarget cannot be null
}
try {
StoredFileHelper storage = new StoredFileHelper(mContext, data.getData(), unsafeMissionTarget.storage.getTag());
mAdapter.recoverMission(unsafeMissionTarget, storage);
} catch (IOException e) {
Toast.makeText(mContext, R.string.general_error, Toast.LENGTH_LONG).show();
}
}
}