Improve DownloadManager and -Service

* Fix permission at some places
 * Fix access problem for downloaded files with external player
 * Store finished Downloads
 * Remove binding to DownloadService just to download a file
 * Javadoc
 * Code improvements
This commit is contained in:
Coffeemakr 2017-01-10 11:41:24 +01:00
parent 53ff58daa3
commit ea76f1d6e2
23 changed files with 1066 additions and 323 deletions

View file

@ -46,5 +46,6 @@ dependencies {
compile 'com.google.code.gson:gson:2.4' compile 'com.google.code.gson:gson:2.4'
compile 'com.nononsenseapps:filepicker:3.0.0' compile 'com.nononsenseapps:filepicker:3.0.0'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19'
compile 'ch.acra:acra:4.9.0' compile 'ch.acra:acra:4.9.0'
} }

View file

@ -161,6 +161,16 @@
<activity <activity
android:name=".ReCaptchaActivity" android:name=".ReCaptchaActivity"
android:label="@string/reCaptchaActivity" /> android:label="@string/reCaptchaActivity" />
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
</application> </application>
</manifest> </manifest>

View file

@ -11,6 +11,7 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.PermissionHelper;
/** /**
* Created by Christian Schabesberger on 02.08.16. * Created by Christian Schabesberger on 02.08.16.
@ -74,6 +75,9 @@ public class MainActivity extends AppCompatActivity {
return true; return true;
} }
case R.id.action_show_downloads: { case R.id.action_show_downloads: {
if(!PermissionHelper.checkStoragePermissions(this)) {
return false;
}
Intent intent = new Intent(this, org.schabi.newpipe.download.DownloadActivity.class); Intent intent = new Intent(this, org.schabi.newpipe.download.DownloadActivity.class);
startActivity(intent); startActivity(intent);
return true; return true;

View file

@ -59,6 +59,7 @@ import org.schabi.newpipe.extractor.stream_info.VideoStream;
import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayer;
import org.schabi.newpipe.player.PlayVideoActivity; import org.schabi.newpipe.player.PlayVideoActivity;
import org.schabi.newpipe.player.ExoPlayerActivity; import org.schabi.newpipe.player.ExoPlayerActivity;
import org.schabi.newpipe.util.PermissionHelper;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST; import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST;
@ -393,6 +394,10 @@ public class VideoItemDetailFragment extends Fragment {
actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() { actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() {
@Override @Override
public void onActionSelected(int selectedStreamId) { public void onActionSelected(int selectedStreamId) {
if(!PermissionHelper.checkStoragePermissions(getActivity())) {
return;
}
try { try {
Bundle args = new Bundle(); Bundle args = new Bundle();

View file

@ -15,6 +15,7 @@ import android.support.v4.app.NavUtils;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -30,6 +31,7 @@ import android.widget.Toast;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.settings.SettingsActivity;
import java.io.File; import java.io.File;
@ -53,26 +55,11 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O
private MissionsFragment mFragment; private MissionsFragment mFragment;
private DownloadManager mManager;
private DownloadManagerService.DMBinder mBinder;
private String mPendingUrl; private String mPendingUrl;
private SharedPreferences mPrefs; private SharedPreferences mPrefs;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName p1, IBinder binder) {
mBinder = (DownloadManagerService.DMBinder) binder;
mManager = mBinder.getDownloadManager();
}
@Override
public void onServiceDisconnected(ComponentName p1) {
}
};
@Override @Override
@TargetApi(21) @TargetApi(21)
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -83,7 +70,6 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O
Intent i = new Intent(); Intent i = new Intent();
i.setClass(this, DownloadManagerService.class); i.setClass(this, DownloadManagerService.class);
startService(i); startService(i);
bindService(i, mConnection, Context.BIND_AUTO_CREATE);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_downloader); setContentView(R.layout.activity_downloader);
@ -91,7 +77,7 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O
//noinspection ConstantConditions //noinspection ConstantConditions
// its ok if this failes, we will catch that error later, and send it as report // its ok if this fails, we will catch that error later, and send it as report
ActionBar actionBar = getSupportActionBar(); ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(R.string.downloads_title); actionBar.setTitle(R.string.downloads_title);
@ -202,22 +188,24 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O
@Override @Override
public boolean onMenuItemClick(MenuItem item) { public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.okay) { if (item.getItemId() == R.id.okay) {
String location;
if(audioButton.isChecked()) {
location = NewPipeSettings.getAudioDownloadPath(DownloadActivity.this);
} else {
location = NewPipeSettings.getVideoDownloadPath(DownloadActivity.this);
}
String fName = name.getText().toString().trim(); String fName = name.getText().toString().trim();
File f = new File(mManager.getLocation() + "/" + fName); File f = new File(location, fName);
if (f.exists()) { if (f.exists()) {
Toast.makeText(DownloadActivity.this, R.string.msg_exists, Toast.LENGTH_SHORT).show(); Toast.makeText(DownloadActivity.this, R.string.msg_exists, Toast.LENGTH_SHORT).show();
} else { } else {
DownloadManagerService.startMission(
while (mBinder == null); DownloadActivity.this,
getIntent().getData().toString(), location, fName,
int res = mManager.startMission( audioButton.isChecked(), threads.getProgress() + 1);
getIntent().getData().toString(),
fName,
audioButton.isChecked(),
threads.getProgress() + 1);
mBinder.onMissionAdded(mManager.getMission(res));
mFragment.notifyChange(); mFragment.notifyChange();
mPrefs.edit().putInt(THREADS, threads.getProgress() + 1).commit(); mPrefs.edit().putInt(THREADS, threads.getProgress() + 1).commit();
@ -277,4 +265,5 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O
super.onOptionsItemSelected(item); super.onOptionsItemSelected(item);
} }
} }
} }

View file

@ -26,6 +26,8 @@ import android.widget.TextView;
import org.schabi.newpipe.App; import org.schabi.newpipe.App;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.settings.NewPipeSettings;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
@ -65,24 +67,6 @@ public class DownloadDialog extends DialogFragment {
public static final String AUDIO_URL = "audio_url"; public static final String AUDIO_URL = "audio_url";
public static final String VIDEO_URL = "video_url"; public static final String VIDEO_URL = "video_url";
private DownloadManager mManager;
private DownloadManagerService.DMBinder mBinder;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName p1, IBinder binder) {
mBinder = (DownloadManagerService.DMBinder) binder;
mManager = mBinder.getDownloadManager();
}
@Override
public void onServiceDisconnected(ComponentName p1) {
}
};
public DownloadDialog() { public DownloadDialog() {
} }
@ -102,12 +86,6 @@ public class DownloadDialog extends DialogFragment {
if(ContextCompat.checkSelfPermission(this.getContext(),Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) if(ContextCompat.checkSelfPermission(this.getContext(),Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED)
ActivityCompat.requestPermissions(getActivity(),new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},0); ActivityCompat.requestPermissions(getActivity(),new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},0);
Intent i = new Intent();
i.setClass(getContext(), DownloadManagerService.class);
getContext().startService(i);
getContext().bindService(i, mConnection, Context.BIND_AUTO_CREATE);
return inflater.inflate(R.layout.dialog_url, container); return inflater.inflate(R.layout.dialog_url, container);
} }
@ -219,36 +197,22 @@ public class DownloadDialog extends DialogFragment {
String fName = name.getText().toString().trim(); String fName = name.getText().toString().trim();
// todo: add timeout? would be bad if the thread gets locked dueto this. boolean isAudio = audioButton.isChecked();
while (mBinder == null); String url, location, filename;
if(isAudio) {
if(audioButton.isChecked()){ url = arguments.getString(AUDIO_URL);
int res = mManager.startMission( location = NewPipeSettings.getAudioDownloadPath(getContext());
arguments.getString(AUDIO_URL), filename = fName + arguments.getString(FILE_SUFFIX_AUDIO);
fName + arguments.getString(FILE_SUFFIX_AUDIO), } else {
audioButton.isChecked(), url = arguments.getString(VIDEO_URL);
threads.getProgress() + 1); location = NewPipeSettings.getVideoDownloadPath(getContext());
DownloadMission mission = mManager.getMission(res); filename = fName + arguments.getString(FILE_SUFFIX_VIDEO);
mBinder.onMissionAdded(mission);
// add download listener to allow media scan notification
DownloadListener listener = new DownloadListener(getContext(), mission);
mission.addListener(listener);
} }
if(videoButton.isChecked()){ DownloadManagerService.startMission(getContext(), url, location, filename, isAudio,
int res = mManager.startMission(
arguments.getString(VIDEO_URL),
fName + arguments.getString(FILE_SUFFIX_VIDEO),
audioButton.isChecked(),
threads.getProgress() + 1); threads.getProgress() + 1);
DownloadMission mission = mManager.getMission(res);
mBinder.onMissionAdded(mission);
// add download listener to allow media scan notification
DownloadListener listener = new DownloadListener(getContext(), mission);
mission.addListener(listener);
}
getDialog().dismiss(); getDialog().dismiss();
} }
private void download(String url, String title, private void download(String url, String title,

View file

@ -1,62 +0,0 @@
package org.schabi.newpipe.download;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.DownloadMission.MissionListener;
/**
* Created by erwin on 06.11.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* DownloadListener.java is part of NewPipe.
*
* NewPipe 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.
*
* NewPipe 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
class DownloadListener implements MissionListener
{
DownloadMission mMission;
Context mContext;
public DownloadListener(Context context, DownloadMission mission)
{
super();
mMission = mission;
mContext = context;
}
@Override
public void onProgressUpdate(long done, long total)
{
// do nothing special ...
}
@Override
public void onFinish()
{
// notify media scanner on downloaded media file ...
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
Uri.parse( "file://" + mMission.location
+ "/" + mMission.name)));
}
@Override
public void onError(int errCode)
{
// do nothing special ...
}
}

View file

@ -42,7 +42,7 @@ import info.guardianproject.netcipher.NetCipher;
*/ */
// TODO: FOR HEVEN SAKE !!! DO NOT SIMPLY USE ASYNCTASK. MAKE THIS A PROPER SERVICE !!! // TODO: FOR HEAVEN SAKE !!! DO NOT SIMPLY USE ASYNCTASK. MAKE THIS A PROPER SERVICE !!!
public class FileDownloader extends AsyncTask<Void, Integer, Void> { public class FileDownloader extends AsyncTask<Void, Integer, Void> {
public static final String TAG = "FileDownloader"; public static final String TAG = "FileDownloader";

View file

@ -88,7 +88,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
// $$el_type$$ will be replaced by the actual el_type (se the declarations below) // $$el_type$$ will be replaced by the actual el_type (se the declarations below)
private static final String GET_VIDEO_INFO_URL = private static final String GET_VIDEO_INFO_URL =
"https://www.youtube.com/get_video_info?video_id=%%video_id%%$$el_type$$&ps=default&eurl=&gl=US&hl=en"; "https://www.youtube.com/get_video_info?video_id=%%video_id%%$$el_type$$&ps=default&eurl=&gl=US&hl=en";
// eltype is nececeary for the url aboth // eltype is necessary for the url above
private static final String EL_INFO = "el=info"; private static final String EL_INFO = "el=info";
public enum ItagType { public enum ItagType {

View file

@ -0,0 +1,68 @@
package org.schabi.newpipe.util;
import android.Manifest;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
public class PermissionHelper {
public static final int PERMISSION_WRITE_STORAGE = 778;
public static final int PERMISSION_READ_STORAGE = 777;
public static boolean checkStoragePermissions(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
if(!checkReadStoragePermissions(activity)) return false;
}
return checkWriteStoragePermissions(activity);
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public static boolean checkReadStoragePermissions(Activity activity) {
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity,
new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE},
PERMISSION_READ_STORAGE);
return false;
}
return true;
}
public static boolean checkWriteStoragePermissions(Activity activity) {
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// Should we show an explanation?
/*if (ActivityCompat.shouldShowRequestPermissionRationale(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
} else {*/
// No explanation needed, we can request the permission.
ActivityCompat.requestPermissions(activity,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
PERMISSION_WRITE_STORAGE);
// PERMISSION_WRITE_STORAGE is an
// app-defined int constant. The callback method gets the
// result of the request.
/*}*/
return false;
}
return true;
}
}

View file

@ -0,0 +1,36 @@
package us.shandian.giga.get;
import java.util.List;
/**
* Provides access to the storage of {@link DownloadMission}s
*/
public interface DownloadDataSource {
/**
* Load all missions
* @return a list of download missions
*/
List<DownloadMission> loadMissions();
/**
* Add a downlaod mission to the storage
* @param downloadMission the download mission to add
* @return the identifier of the mission
*/
void addMission(DownloadMission downloadMission);
/**
* Update a download mission which exists in the storage
* @param downloadMission the download mission to update
* @throws IllegalArgumentException if the mission was not added to storage
*/
void updateMission(DownloadMission downloadMission);
/**
* Delete a download mission
* @param downloadMission the mission to delete
*/
void deleteMission(DownloadMission downloadMission);
}

View file

@ -4,11 +4,45 @@ public interface DownloadManager
{ {
int BLOCK_SIZE = 512 * 1024; int BLOCK_SIZE = 512 * 1024;
int startMission(String url, String name, boolean isAudio, int threads); /**
* Start a new download mission
* @param url the url to download
* @param location the location
* @param name the name of the file to create
* @param isAudio true if the download is an audio file
* @param threads the number of threads maximal used to download chunks of the file. @return the identifier of the mission.
*/
int startMission(String url, String location, String name, boolean isAudio, int threads);
/**
* Resume the execution of a download mission.
* @param id the identifier of the mission to resume.
*/
void resumeMission(int id); void resumeMission(int id);
/**
* Pause the execution of a download mission.
* @param id the identifier of the mission to pause.
*/
void pauseMission(int id); void pauseMission(int id);
/**
* Deletes the mission from the downloaded list but keeps the downloaded file.
* @param id The mission identifier
*/
void deleteMission(int id); void deleteMission(int id);
/**
* Get the download mission by its identifier
* @param id the identifier of the download mission
* @return the download mission or null if the mission doesn't exist
*/
DownloadMission getMission(int id); DownloadMission getMission(int id);
/**
* Get the number of download missions.
* @return the number of download missions.
*/
int getCount(); int getCount();
String getLocation();
} }

View file

@ -1,17 +1,21 @@
package us.shandian.giga.get; package us.shandian.giga.get;
import android.content.Context; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import com.google.gson.Gson; import com.google.gson.Gson;
import org.schabi.newpipe.settings.NewPipeSettings;
import java.io.File; import java.io.File;
import java.io.FilenameFilter;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import us.shandian.giga.util.Utility; import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG; import static org.schabi.newpipe.BuildConfig.DEBUG;
@ -19,30 +23,45 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadManagerImpl implements DownloadManager public class DownloadManagerImpl implements DownloadManager
{ {
private static final String TAG = DownloadManagerImpl.class.getSimpleName(); private static final String TAG = DownloadManagerImpl.class.getSimpleName();
private final DownloadDataSource mDownloadDataSource;
private Context mContext; private final ArrayList<DownloadMission> mMissions = new ArrayList<DownloadMission>();
private String mLocation;
protected ArrayList<DownloadMission> mMissions = new ArrayList<DownloadMission>();
public DownloadManagerImpl(Context context, String location) { /**
mContext = context; * Create a new instance
mLocation = location; * @param searchLocations the directories to search for unfinished downloads
loadMissions(); * @param downloadDataSource the data source for finished downloads
*/
public DownloadManagerImpl(Collection<String> searchLocations, DownloadDataSource downloadDataSource) {
mDownloadDataSource = downloadDataSource;
loadMissions(searchLocations);
} }
@Override @Override
public int startMission(String url, String name, boolean isAudio, int threads) { public int startMission(String url, String location, String name, boolean isAudio, int threads) {
DownloadMission mission = new DownloadMission(); DownloadMission existingMission = getMissionByLocation(location, name);
mission.url = url; if(existingMission != null) {
mission.name = name; // Already downloaded or downloading
if(isAudio) { if(existingMission.finished) {
mission.location = NewPipeSettings.getAudioDownloadPath(mContext); // Overwrite mission
deleteMission(mMissions.indexOf(existingMission));
} else { } else {
mission.location = NewPipeSettings.getVideoDownloadPath(mContext); // Rename file (?)
try {
name = generateUniqueName(location, name);
}catch (Exception e) {
Log.e(TAG, "Unable to generate unique name", e);
name = System.currentTimeMillis() + name ;
Log.i(TAG, "Using " + name);
} }
}
}
DownloadMission mission = new DownloadMission(name, url, location);
mission.timestamp = System.currentTimeMillis(); mission.timestamp = System.currentTimeMillis();
mission.threadCount = threads; mission.threadCount = threads;
new Initializer(mContext, mission).start(); mission.addListener(new MissionListener(mission));
new Initializer(mission).start();
return insertMission(mission); return insertMission(mission);
} }
@ -64,22 +83,70 @@ public class DownloadManagerImpl implements DownloadManager
@Override @Override
public void deleteMission(int i) { public void deleteMission(int i) {
getMission(i).delete(); DownloadMission mission = getMission(i);
if(mission.finished) {
mDownloadDataSource.deleteMission(mission);
}
mission.delete();
mMissions.remove(i); mMissions.remove(i);
} }
private void loadMissions() { private void loadMissions(Iterable<String> searchLocations) {
File f = new File(mLocation); mMissions.clear();
loadFinishedMissions();
for(String location: searchLocations) {
loadMissions(location);
}
}
/**
* Loads finished missions from the data source
*/
private void loadFinishedMissions() {
List<DownloadMission> finishedMissions = mDownloadDataSource.loadMissions();
if(finishedMissions == null) {
finishedMissions = new ArrayList<>();
}
// Ensure its sorted
Collections.sort(finishedMissions, new Comparator<DownloadMission>() {
@Override
public int compare(DownloadMission o1, DownloadMission o2) {
return (int) (o1.timestamp - o2.timestamp);
}
});
mMissions.ensureCapacity(mMissions.size() + finishedMissions.size());
for(DownloadMission mission: finishedMissions) {
File downloadedFile = mission.getDownloadedFile();
if(!downloadedFile.isFile()) {
if(DEBUG) {
Log.d(TAG, "downloaded file removed: " + downloadedFile.getAbsolutePath());
}
mDownloadDataSource.deleteMission(mission);
} else {
mission.length = downloadedFile.length();
mission.finished = true;
mission.running = false;
mMissions.add(mission);
}
}
}
private void loadMissions(String location) {
File f = new File(location);
if (f.exists() && f.isDirectory()) { if (f.exists() && f.isDirectory()) {
File[] subs = f.listFiles(); File[] subs = f.listFiles();
for (File sub : subs) { if(subs == null) {
if (sub.isDirectory()) { Log.e(TAG, "listFiles() returned null");
continue; return;
} }
if (sub.getName().endsWith(".giga")) { for (File sub : subs) {
if (sub.isFile() && sub.getName().endsWith(".giga")) {
String str = Utility.readFromFile(sub.getAbsolutePath()); String str = Utility.readFromFile(sub.getAbsolutePath());
if (str != null && !str.trim().equals("")) { if (str != null && !str.trim().equals("")) {
@ -91,7 +158,9 @@ public class DownloadManagerImpl implements DownloadManager
DownloadMission mis = new Gson().fromJson(str, DownloadMission.class); DownloadMission mis = new Gson().fromJson(str, DownloadMission.class);
if (mis.finished) { if (mis.finished) {
sub.delete(); if(!sub.delete()) {
Log.w(TAG, "Unable to delete .giga file: " + sub.getPath());
}
continue; continue;
} }
@ -99,17 +168,6 @@ public class DownloadManagerImpl implements DownloadManager
mis.recovered = true; mis.recovered = true;
insertMission(mis); insertMission(mis);
} }
} else if (!sub.getName().startsWith(".") && !new File(sub.getPath() + ".giga").exists()) {
// Add a dummy mission for downloaded files
DownloadMission mis = new DownloadMission();
mis.length = sub.length();
mis.done = mis.length;
mis.finished = true;
mis.running = false;
mis.name = sub.getName();
mis.location = mLocation;
mis.timestamp = sub.lastModified();
insertMission(mis);
} }
} }
} }
@ -145,17 +203,80 @@ public class DownloadManagerImpl implements DownloadManager
return i; return i;
} }
/**
* Get a mission by its location and name
* @param location the location
* @param name the name
* @return the mission or null if no such mission exists
*/
private @Nullable DownloadMission getMissionByLocation(String location, String name) {
for(DownloadMission mission: mMissions) {
if(location.equals(mission.location) && name.equals(mission.name)) {
return mission;
}
}
return null;
}
/**
* Splits the filename into name and extension
*
* Dots are ignored if they appear: not at all, at the beginning of the file,
* at the end of the file
*
* @param name the name to split
* @return a string array with a length of 2 containing the name and the extension
*/
private static String[] splitName(String name) {
int dotIndex = name.lastIndexOf('.');
if(dotIndex <= 0 || (dotIndex == name.length() - 1)) {
return new String[]{name, ""};
} else {
return new String[]{name.substring(0, dotIndex), name.substring(dotIndex + 1)};
}
}
/**
* Generates a unique file name.
*
* e.g. "myname (1).txt" if the name "myname.txt" exists.
* @param location the location (to check for existing files)
* @param name the name of the file
* @return the unique file name
* @throws IllegalArgumentException if the location is not a directory
* @throws SecurityException if the location is not readable
*/
private static String generateUniqueName(String location, String name) {
if(location == null) throw new NullPointerException("location is null");
if(name == null) throw new NullPointerException("name is null");
File destination = new File(location);
if(!destination.isDirectory()) {
throw new IllegalArgumentException("location is not a directory: " + location);
}
final String[] nameParts = splitName(name);
String[] existingName = destination.list(new FilenameFilter() {
@Override @Override
public String getLocation() { public boolean accept(File dir, String name) {
return mLocation; return name.startsWith(nameParts[0]);
}
});
Arrays.sort(existingName);
String newName;
int downloadIndex = 0;
do {
newName = nameParts[0] + " (" + downloadIndex + ")." + nameParts[1];
++downloadIndex;
if(downloadIndex == 1000) { // Probably an error on our side
throw new RuntimeException("Too many existing files");
}
} while (Arrays.binarySearch(existingName, newName) >= 0);
return newName;
} }
private class Initializer extends Thread { private class Initializer extends Thread {
private Context context;
private DownloadMission mission; private DownloadMission mission;
public Initializer(Context context, DownloadMission mission) { public Initializer(DownloadMission mission) {
this.context = context;
this.mission = mission; this.mission = mission;
} }
@ -217,4 +338,30 @@ public class DownloadManagerImpl implements DownloadManager
} }
} }
} }
/**
* Waits for mission to finish to add it to the {@link #mDownloadDataSource}
*/
private class MissionListener implements DownloadMission.MissionListener {
private final DownloadMission mMission;
private MissionListener(DownloadMission mission) {
if(mission == null) throw new NullPointerException("mission is null");
// Could the mission be passed in onFinish()?
mMission = mission;
}
@Override
public void onProgressUpdate(DownloadMission downloadMission, long done, long total) {
}
@Override
public void onFinish(DownloadMission downloadMission) {
mDownloadDataSource.addMission(mMission);
}
@Override
public void onError(DownloadMission downloadMission, int errCode) {
}
}
} }

View file

@ -1,6 +1,5 @@
package us.shandian.giga.get; package us.shandian.giga.get;
import android.content.Context;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Log; import android.util.Log;
@ -10,12 +9,13 @@ import com.google.gson.Gson;
import java.io.File; import java.io.File;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import us.shandian.giga.util.Utility; import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG; import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadMission public class DownloadMission
@ -25,24 +25,47 @@ public class DownloadMission
public interface MissionListener { public interface MissionListener {
HashMap<MissionListener, Handler> handlerStore = new HashMap<>(); HashMap<MissionListener, Handler> handlerStore = new HashMap<>();
void onProgressUpdate(long done, long total); void onProgressUpdate(DownloadMission downloadMission, long done, long total);
void onFinish(); void onFinish(DownloadMission downloadMission);
void onError(int errCode); void onError(DownloadMission downloadMission, int errCode);
} }
public static final int ERROR_SERVER_UNSUPPORTED = 206; public static final int ERROR_SERVER_UNSUPPORTED = 206;
public static final int ERROR_UNKNOWN = 233; public static final int ERROR_UNKNOWN = 233;
public String name = ""; /**
public String url = ""; * The filename
public String location = ""; */
public String name;
/**
* The url of the file to download
*/
public String url;
/**
* The directory to store the download
*/
public String location;
/**
* Number of blocks the size of {@link DownloadManager#BLOCK_SIZE}
*/
public long blocks; public long blocks;
/**
* Number of bytes
*/
public long length; public long length;
/**
* Number of bytes downloaded
*/
public long done; public long done;
public int threadCount = 3; public int threadCount = 3;
public int finishCount; public int finishCount;
public List<Long> threadPositions = new ArrayList<Long>(); private List<Long> threadPositions = new ArrayList<Long>();
public Map<Long, Boolean> blockState = new HashMap<Long, Boolean>(); public final Map<Long, Boolean> blockState = new HashMap<Long, Boolean>();
public boolean running; public boolean running;
public boolean finished; public boolean finished;
public boolean fallback; public boolean fallback;
@ -54,22 +77,64 @@ public class DownloadMission
private transient ArrayList<WeakReference<MissionListener>> mListeners = new ArrayList<WeakReference<MissionListener>>(); private transient ArrayList<WeakReference<MissionListener>> mListeners = new ArrayList<WeakReference<MissionListener>>();
private transient boolean mWritingToFile; private transient boolean mWritingToFile;
private static final int NO_IDENTIFIER = -1;
private long db_identifier = NO_IDENTIFIER;
public DownloadMission() {
}
public DownloadMission(String name, String url, String location) {
if(name == null) throw new NullPointerException("name is null");
if(name.isEmpty()) throw new IllegalArgumentException("name is empty");
if(url == null) throw new NullPointerException("url is null");
if(url.isEmpty()) throw new IllegalArgumentException("url is empty");
if(location == null) throw new NullPointerException("location is null");
if(location.isEmpty()) throw new IllegalArgumentException("location is empty");
this.url = url;
this.name = name;
this.location = location;
}
private void checkBlock(long block) {
if(block < 0 || block >= blocks) {
throw new IllegalArgumentException("illegal block identifier");
}
}
/**
* Check if a block is reserved
* @param block the block identifier
* @return true if the block is reserved and false if otherwise
*/
public boolean isBlockPreserved(long block) { public boolean isBlockPreserved(long block) {
checkBlock(block);
return blockState.containsKey(block) ? blockState.get(block) : false; return blockState.containsKey(block) ? blockState.get(block) : false;
} }
public void preserveBlock(long block) { public void preserveBlock(long block) {
checkBlock(block);
synchronized (blockState) { synchronized (blockState) {
blockState.put(block, true); blockState.put(block, true);
} }
} }
public void setPosition(int id, long position) { /**
threadPositions.set(id, position); * Set the download position of the file
* @param threadId the identifier of the thread
* @param position the download position of the thread
*/
public void setPosition(int threadId, long position) {
threadPositions.set(threadId, position);
} }
public long getPosition(int id) { /**
return threadPositions.get(id); * Get the position of a thread
* @param threadId the identifier of the thread
* @return the position for the thread
*/
public long getPosition(int threadId) {
return threadPositions.get(threadId);
} }
public synchronized void notifyProgress(long deltaLen) { public synchronized void notifyProgress(long deltaLen) {
@ -95,13 +160,16 @@ public class DownloadMission
MissionListener.handlerStore.get(listener).post(new Runnable() { MissionListener.handlerStore.get(listener).post(new Runnable() {
@Override @Override
public void run() { public void run() {
listener.onProgressUpdate(done, length); listener.onProgressUpdate(DownloadMission.this, done, length);
} }
}); });
} }
} }
} }
/**
* Called by a download thread when it finished.
*/
public synchronized void notifyFinished() { public synchronized void notifyFinished() {
if (errCode > 0) return; if (errCode > 0) return;
@ -112,6 +180,9 @@ public class DownloadMission
} }
} }
/**
* Called when all parts are downloaded
*/
private void onFinish() { private void onFinish() {
if (errCode > 0) return; if (errCode > 0) return;
@ -130,7 +201,7 @@ public class DownloadMission
MissionListener.handlerStore.get(listener).post(new Runnable() { MissionListener.handlerStore.get(listener).post(new Runnable() {
@Override @Override
public void run() { public void run() {
listener.onFinish(); listener.onFinish(DownloadMission.this);
} }
}); });
} }
@ -147,7 +218,7 @@ public class DownloadMission
MissionListener.handlerStore.get(listener).post(new Runnable() { MissionListener.handlerStore.get(listener).post(new Runnable() {
@Override @Override
public void run() { public void run() {
listener.onError(errCode); listener.onError(DownloadMission.this, errCode);
} }
}); });
} }
@ -170,6 +241,9 @@ public class DownloadMission
} }
} }
/**
* Start downloading with multiple threads.
*/
public void start() { public void start() {
if (!running && !finished) { if (!running && !finished) {
running = true; running = true;
@ -201,11 +275,18 @@ public class DownloadMission
} }
} }
/**
* Removes the file and the meta file
*/
public void delete() { public void delete() {
deleteThisFromFile(); deleteThisFromFile();
new File(location + "/" + name).delete(); new File(location, name).delete();
} }
/**
* Write this {@link DownloadMission} to the meta file asynchronously
* if no thread is already running.
*/
public void writeThisToFile() { public void writeThisToFile() {
if (!mWritingToFile) { if (!mWritingToFile) {
mWritingToFile = true; mWritingToFile = true;
@ -219,13 +300,29 @@ public class DownloadMission
} }
} }
/**
* Write this {@link DownloadMission} to the meta file.
*/
private void doWriteThisToFile() { private void doWriteThisToFile() {
synchronized (blockState) { synchronized (blockState) {
Utility.writeToFile(location + "/" + name + ".giga", new Gson().toJson(this)); Utility.writeToFile(getMetaFilename(), new Gson().toJson(this));
} }
} }
private void deleteThisFromFile() { private void deleteThisFromFile() {
new File(location + "/" + name + ".giga").delete(); new File(getMetaFilename()).delete();
} }
/**
* Get the path of the meta file
* @return the path to the meta file
*/
private String getMetaFilename() {
return location + "/" + name + ".giga";
}
public File getDownloadedFile() {
return new File(location, name);
}
} }

View file

@ -9,14 +9,19 @@ import java.net.URL;
import static org.schabi.newpipe.BuildConfig.DEBUG; import static org.schabi.newpipe.BuildConfig.DEBUG;
/**
* Runnable to download blocks of a file until the file is completely downloaded,
* an error occurs or the process is stopped.
*/
public class DownloadRunnable implements Runnable public class DownloadRunnable implements Runnable
{ {
private static final String TAG = DownloadRunnable.class.getSimpleName(); private static final String TAG = DownloadRunnable.class.getSimpleName();
private DownloadMission mMission; private final DownloadMission mMission;
private int mId; private final int mId;
public DownloadRunnable(DownloadMission mission, int id) { public DownloadRunnable(DownloadMission mission, int id) {
if(mission == null) throw new NullPointerException("mission is null");
mMission = mission; mMission = mission;
mId = id; mId = id;
} }
@ -86,7 +91,7 @@ public class DownloadRunnable implements Runnable
Log.d(TAG, mId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode()); Log.d(TAG, mId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode());
} }
// A server may be ignoring the range requet // A server may be ignoring the range request
if (conn.getResponseCode() != 206) { if (conn.getResponseCode() != 206) {
mMission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED; mMission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED;
notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED); notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
@ -131,7 +136,7 @@ public class DownloadRunnable implements Runnable
notifyProgress(-total); notifyProgress(-total);
if (DEBUG) { if (DEBUG) {
Log.d(TAG, mId + ":position " + position + " retrying"); Log.d(TAG, mId + ":position " + position + " retrying", e);
} }
} }
} }

View file

@ -8,10 +8,11 @@ import java.net.URL;
// Single-threaded fallback mode // Single-threaded fallback mode
public class DownloadRunnableFallback implements Runnable public class DownloadRunnableFallback implements Runnable
{ {
private DownloadMission mMission; private final DownloadMission mMission;
//private int mId; //private int mId;
public DownloadRunnableFallback(DownloadMission mission) { public DownloadRunnableFallback(DownloadMission mission) {
if(mission == null) throw new NullPointerException("mission is null");
//mId = id; //mId = id;
mMission = mission; mMission = mission;
} }
@ -35,7 +36,7 @@ public class DownloadRunnableFallback implements Runnable
f.write(buf, 0, len); f.write(buf, 0, len);
notifyProgress(len); notifyProgress(len);
if (Thread.currentThread().interrupted()) { if (Thread.interrupted()) {
break; break;
} }

View file

@ -0,0 +1,102 @@
package us.shandian.giga.get.sqlite;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import us.shandian.giga.get.DownloadMission;
/**
* SqliteHelper to store {@link us.shandian.giga.get.DownloadMission}
*/
public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper {
private final String TAG = "DownloadMissionHelper";
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
private static final String DATABASE_NAME = "newpipe.db";
private static final int DATABASE_VERSION = 2;
/**
* The table name of download missions
*/
static final String MISSIONS_TABLE_NAME = "download_missions";
/**
* The key to the directory location of the mission
*/
static final String KEY_LOCATION = "location";
/**
* The key to the url of a mission
*/
static final String KEY_URL = "url";
/**
* The key to the name of a mission
*/
static final String KEY_NAME = "name";
/**
* The key to the done.
*/
static final String KEY_DONE = "bytes_downloaded";
static final String KEY_TIMESTAMP = "timestamp";
/**
* The statement to create the table
*/
private static final String MISSIONS_CREATE_TABLE =
"CREATE TABLE " + MISSIONS_TABLE_NAME + " (" +
KEY_LOCATION + " TEXT NOT NULL, " +
KEY_NAME + " TEXT NOT NULL, " +
KEY_URL + " TEXT NOT NULL, " +
KEY_DONE + " INTEGER NOT NULL, " +
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
" UNIQUE(" + KEY_LOCATION + ", " + KEY_NAME + "));";
DownloadMissionSQLiteHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
/**
* Returns all values of the download mission as ContentValues.
* @param downloadMission the download mission
* @return the content values
*/
public static ContentValues getValuesOfMission(DownloadMission downloadMission) {
ContentValues values = new ContentValues();
values.put(KEY_URL, downloadMission.url);
values.put(KEY_LOCATION, downloadMission.location);
values.put(KEY_NAME, downloadMission.name);
values.put(KEY_DONE, downloadMission.done);
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
return values;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(MISSIONS_CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Currently nothing to do
}
public static DownloadMission getMissionFromCursor(Cursor cursor) {
if(cursor == null) throw new NullPointerException("cursor is null");
int pos;
String name = cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME));
String location = cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION));
String url = cursor.getString(cursor.getColumnIndexOrThrow(KEY_URL));
DownloadMission mission = new DownloadMission(name, url, location);
mission.done = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
mission.finished = true;
return mission;
}
}

View file

@ -0,0 +1,79 @@
package us.shandian.giga.get.sqlite;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import us.shandian.giga.get.DownloadDataSource;
import us.shandian.giga.get.DownloadMission;
import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.KEY_LOCATION;
import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.KEY_NAME;
import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.MISSIONS_TABLE_NAME;
/**
* Non-thread-safe implementation of {@link DownloadDataSource}
*/
public class SQLiteDownloadDataSource implements DownloadDataSource {
private static final String TAG = "DownloadDataSourceImpl";
private final DownloadMissionSQLiteHelper downloadMissionSQLiteHelper;
public SQLiteDownloadDataSource(Context context) {
downloadMissionSQLiteHelper = new DownloadMissionSQLiteHelper(context);
}
@Override
public List<DownloadMission> loadMissions() {
ArrayList<DownloadMission> result;
SQLiteDatabase database = downloadMissionSQLiteHelper.getReadableDatabase();
Cursor cursor = database.query(MISSIONS_TABLE_NAME, null, null,
null, null, null, DownloadMissionSQLiteHelper.KEY_TIMESTAMP);
int count = cursor.getCount();
if(count == 0) return new ArrayList<>();
result = new ArrayList<>(count);
while (cursor.moveToNext()) {
result.add(DownloadMissionSQLiteHelper.getMissionFromCursor(cursor));
}
return result;
}
@Override
public void addMission(DownloadMission downloadMission) {
if(downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase();
ContentValues values = DownloadMissionSQLiteHelper.getValuesOfMission(downloadMission);
database.insert(MISSIONS_TABLE_NAME, null, values);
}
@Override
public void updateMission(DownloadMission downloadMission) {
if(downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase();
ContentValues values = DownloadMissionSQLiteHelper.getValuesOfMission(downloadMission);
String whereClause = KEY_LOCATION+ " = ? AND " +
KEY_NAME + " = ?";
int rowsAffected = database.update(MISSIONS_TABLE_NAME, values,
whereClause, new String[]{downloadMission.location, downloadMission.name});
if(rowsAffected != 1) {
Log.e(TAG, "Expected 1 row to be affected by update but got " + rowsAffected);
}
}
@Override
public void deleteMission(DownloadMission downloadMission) {
if(downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase();
database.delete(MISSIONS_TABLE_NAME,
KEY_LOCATION + " = ? AND " +
KEY_NAME + " = ?",
new String[]{downloadMission.location, downloadMission.name});
}
}

View file

@ -4,38 +4,69 @@ import android.Manifest;
import android.app.Notification; import android.app.Notification;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Binder; import android.os.Binder;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.IBinder; import android.os.IBinder;
import android.os.Message; import android.os.Message;
import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.app.NotificationCompat.Builder;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.PermissionChecker; import android.support.v4.content.PermissionChecker;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import org.schabi.newpipe.R;
import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.R;
import java.util.ArrayList;
import us.shandian.giga.get.DownloadDataSource;
import us.shandian.giga.get.DownloadManager; import us.shandian.giga.get.DownloadManager;
import us.shandian.giga.get.DownloadManagerImpl; import us.shandian.giga.get.DownloadManagerImpl;
import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.sqlite.SQLiteDownloadDataSource;
import static org.schabi.newpipe.BuildConfig.DEBUG; import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadManagerService extends Service implements DownloadMission.MissionListener public class DownloadManagerService extends Service
{ {
private static final String TAG = DownloadManagerService.class.getSimpleName(); private static final String TAG = DownloadManagerService.class.getSimpleName();
/**
* Message code of update messages stored as {@link Message#what}.
*/
private static final int UPDATE_MESSAGE = 0;
private static final int NOTIFICATION_ID = 1000;
private static final String EXTRA_NAME = "DownloadManagerService.extra.name";
private static final String EXTRA_LOCATION = "DownloadManagerService.extra.location";
private static final String EXTRA_IS_AUDIO = "DownloadManagerService.extra.is_audio";
private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads";
private DMBinder mBinder; private DMBinder mBinder;
private DownloadManager mManager; private DownloadManager mManager;
private Notification mNotification; private Notification mNotification;
private Handler mHandler; private Handler mHandler;
private long mLastTimeStamp = System.currentTimeMillis(); private long mLastTimeStamp = System.currentTimeMillis();
private DownloadDataSource mDataSource;
private MissionListener missionListener = new MissionListener();
private void notifyMediaScanner(DownloadMission mission) {
Uri uri = Uri.parse("file://" + mission.location + "/" + mission.name);
// notify media scanner on downloaded media file ...
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
}
@Override @Override
public void onCreate() { public void onCreate() {
@ -46,12 +77,17 @@ public class DownloadManagerService extends Service implements DownloadMission.M
} }
mBinder = new DMBinder(); mBinder = new DMBinder();
if(mDataSource == null) {
mDataSource = new SQLiteDownloadDataSource(this);
}
if (mManager == null) { if (mManager == null) {
String path = NewPipeSettings.getVideoDownloadPath(this); ArrayList<String> paths = new ArrayList<>(2);
mManager = new DownloadManagerImpl(this, path); paths.add(NewPipeSettings.getVideoDownloadPath(this));
paths.add(NewPipeSettings.getAudioDownloadPath(this));
mManager = new DownloadManagerImpl(paths, mDataSource);
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "mManager == null"); Log.d(TAG, "mManager == null");
Log.d(TAG, "Download directory: " + path); Log.d(TAG, "Download directory: " + paths);
} }
} }
@ -87,7 +123,8 @@ public class DownloadManagerService extends Service implements DownloadMission.M
mHandler = new Handler(thread.getLooper()) { mHandler = new Handler(thread.getLooper()) {
@Override @Override
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
if (msg.what == 0) { switch (msg.what) {
case UPDATE_MESSAGE: {
int runningCount = 0; int runningCount = 0;
for (int i = 0; i < mManager.getCount(); i++) { for (int i = 0; i < mManager.getCount(); i++) {
@ -95,20 +132,41 @@ public class DownloadManagerService extends Service implements DownloadMission.M
runningCount++; runningCount++;
} }
} }
updateState(runningCount); updateState(runningCount);
break;
}
} }
} }
}; };
} }
private void startMissionAsync(final String url, final String location, final String name,
final boolean isAudio, final int threads) {
mHandler.post(new Runnable() {
@Override
public void run() {
int missionId = mManager.startMission(url, location, name, isAudio, threads);
mBinder.onMissionAdded(mManager.getMission(missionId));
}
});
}
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Starting"); Log.d(TAG, "Starting");
} }
Log.i(TAG, "Got intent: " + intent);
String action = intent.getAction();
if(action != null && action.equals(Intent.ACTION_RUN)) {
String name = intent.getStringExtra(EXTRA_NAME);
String location = intent.getStringExtra(EXTRA_LOCATION);
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
boolean isAudio = intent.getBooleanExtra(EXTRA_IS_AUDIO, false);
String url = intent.getDataString();
startMissionAsync(url, location, name, isAudio, threads);
}
return START_NOT_STICKY; return START_NOT_STICKY;
} }
@ -129,30 +187,51 @@ public class DownloadManagerService extends Service implements DownloadMission.M
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
int permissionCheck;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
int permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
if(permissionCheck == PermissionChecker.PERMISSION_DENIED) { if(permissionCheck == PermissionChecker.PERMISSION_DENIED) {
Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show();
return null;
} }
}
permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
if(permissionCheck == PermissionChecker.PERMISSION_DENIED) { if(permissionCheck == PermissionChecker.PERMISSION_DENIED) {
Toast.makeText(this, "Permission denied (write)", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Permission denied (write)", Toast.LENGTH_SHORT).show();
return null;
}
} }
return mBinder; return mBinder;
} }
private void postUpdateMessage() {
mHandler.sendEmptyMessage(UPDATE_MESSAGE);
}
private void updateState(int runningCount) {
if (runningCount == 0) {
stopForeground(true);
} else {
startForeground(NOTIFICATION_ID, mNotification);
}
}
public static void startMission(Context context, String url, String location, String name, boolean isAudio, int threads) {
Intent intent = new Intent(context, DownloadManagerService.class);
intent.setAction(Intent.ACTION_RUN);
intent.setData(Uri.parse(url));
intent.putExtra(EXTRA_NAME, name);
intent.putExtra(EXTRA_LOCATION, location);
intent.putExtra(EXTRA_IS_AUDIO, isAudio);
intent.putExtra(EXTRA_THREADS, threads);
context.startService(intent);
}
class MissionListener implements DownloadMission.MissionListener {
@Override @Override
public void onProgressUpdate(long done, long total) { public void onProgressUpdate(DownloadMission downloadMission, long done, long total) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long delta = now - mLastTimeStamp; long delta = now - mLastTimeStamp;
if (delta > 2000) { if (delta > 2000) {
postUpdateMessage(); postUpdateMessage();
mLastTimeStamp = now; mLastTimeStamp = now;
@ -160,25 +239,15 @@ public class DownloadManagerService extends Service implements DownloadMission.M
} }
@Override @Override
public void onFinish() { public void onFinish(DownloadMission downloadMission) {
postUpdateMessage(); postUpdateMessage();
notifyMediaScanner(downloadMission);
} }
@Override @Override
public void onError(int errCode) { public void onError(DownloadMission downloadMission, int errCode) {
postUpdateMessage(); postUpdateMessage();
} }
private void postUpdateMessage() {
mHandler.sendEmptyMessage(0);
}
private void updateState(int runningCount) {
if (runningCount == 0) {
stopForeground(true);
} else {
startForeground(1000, mNotification);
}
} }
@ -189,15 +258,13 @@ public class DownloadManagerService extends Service implements DownloadMission.M
} }
public void onMissionAdded(DownloadMission mission) { public void onMissionAdded(DownloadMission mission) {
mission.addListener(DownloadManagerService.this); mission.addListener(missionListener);
postUpdateMessage(); postUpdateMessage();
} }
public void onMissionRemoved(DownloadMission mission) { public void onMissionRemoved(DownloadMission mission) {
mission.removeListener(DownloadManagerService.this); mission.removeListener(missionListener);
postUpdateMessage(); postUpdateMessage();
} }
} }
} }

View file

@ -5,6 +5,9 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.support.v4.content.FileProvider;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -28,9 +31,13 @@ import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.common.ProgressDrawable; import us.shandian.giga.ui.common.ProgressDrawable;
import us.shandian.giga.util.Utility; 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> public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHolder>
{ {
private static final Map<Integer, String> ALGORITHMS = new HashMap<>(); private static final Map<Integer, String> ALGORITHMS = new HashMap<>();
private static final String TAG = "MissionAdapter";
static { static {
ALGORITHMS.put(R.id.md5, "MD5"); ALGORITHMS.put(R.id.md5, "MD5");
@ -212,23 +219,23 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
h.lastDone = -1; h.lastDone = -1;
return true; return true;
case R.id.view: case R.id.view:
Intent i = new Intent(); File f = new File(h.mission.location, h.mission.name);
i.setAction(Intent.ACTION_VIEW);
File f = new File(h.mission.location + "/" + h.mission.name);
String ext = Utility.getFileExt(h.mission.name); String ext = Utility.getFileExt(h.mission.name);
if (ext == null) return false; 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)); String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
Log.v(TAG, "Mime: " + mime + " package: " + mContext.getApplicationContext().getPackageName() + ".provider");
if (f.exists()) { if (f.exists()) {
i.setDataAndType(Uri.fromFile(f), mime); viewFileWithFileProvider(f, mime);
} else {
try { Log.w(TAG, "File doesn't exist");
mContext.startActivity(i);
} catch (Exception e) {
}
} }
return true; return true;
@ -250,6 +257,34 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
popup.show(); popup.show();
} }
private void viewFile(File file, String mimetype) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file), mimetype);
intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION);
}
//mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
Log.v(TAG, "Starting intent: " + intent);
mContext.startActivity(intent);
}
private void viewFileWithFileProvider(File file, String mimetype) {
String ourPackage = mContext.getApplicationContext().getPackageName();
Uri uri = FileProvider.getUriForFile(mContext, ourPackage + ".provider", file);
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);
}
//mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
Log.v(TAG, "Starting intent: " + intent);
mContext.startActivity(intent);
}
private class ChecksumTask extends AsyncTask<String, Void, String> { private class ChecksumTask extends AsyncTask<String, Void, String> {
ProgressDialog prog; ProgressDialog prog;
@ -316,12 +351,12 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
} }
@Override @Override
public void onProgressUpdate(long done, long total) { public void onProgressUpdate(DownloadMission downloadMission, long done, long total) {
mAdapter.updateProgress(mHolder); mAdapter.updateProgress(mHolder);
} }
@Override @Override
public void onFinish() { public void onFinish(DownloadMission downloadMission) {
//mAdapter.mManager.deleteMission(mHolder.position); //mAdapter.mManager.deleteMission(mHolder.position);
// TODO Notification // TODO Notification
//mAdapter.notifyDataSetChanged(); //mAdapter.notifyDataSetChanged();
@ -332,7 +367,7 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
} }
@Override @Override
public void onError(int errCode) { public void onError(DownloadMission downloadMission, int errCode) {
mAdapter.updateProgress(mHolder); mAdapter.updateProgress(mHolder);
} }

View file

@ -40,7 +40,7 @@ public abstract class MissionsFragment extends Fragment
private MissionAdapter mAdapter; private MissionAdapter mAdapter;
private GridLayoutManager mGridManager; private GridLayoutManager mGridManager;
private LinearLayoutManager mLinearManager; private LinearLayoutManager mLinearManager;
private Activity mActivity; private Context mActivity;
private ServiceConnection mConnection = new ServiceConnection() { private ServiceConnection mConnection = new ServiceConnection() {
@ -85,14 +85,15 @@ public abstract class MissionsFragment extends Fragment
} }
@Override @Override
public void onAttach(Activity activity) { public void onAttach(Context activity) {
super.onAttach(activity); super.onAttach(activity);
mActivity = activity; mActivity = activity;
} }
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onDestroyView() {
super.onDestroyView();
getActivity().unbindService(mConnection);
} }
@Override @Override

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_files" path="."/>
</paths>

View file

@ -0,0 +1,156 @@
package us.shandian.giga.get.get;
import org.junit.Ignore;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import us.shandian.giga.get.DownloadDataSource;
import us.shandian.giga.get.DownloadManagerImpl;
import us.shandian.giga.get.DownloadMission;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Test for {@link DownloadManagerImpl}
*
* TODO: test loading from .giga files, startMission and improve tests
*/
public class DownloadManagerImplTest {
private DownloadManagerImpl downloadManager;
private DownloadDataSource dowloadDataSource;
private ArrayList<DownloadMission> missions;
@org.junit.Before
public void setUp() throws Exception {
dowloadDataSource = mock(DownloadDataSource.class);
missions = new ArrayList<>();
for(int i = 0; i < 50; ++i){
missions.add(generateFinishedDownloadMission());
}
when(dowloadDataSource.loadMissions()).thenReturn(new ArrayList<>(missions));
downloadManager = new DownloadManagerImpl(new ArrayList<String>(), dowloadDataSource);
}
@Test(expected = NullPointerException.class)
public void testConstructorWithNullAsDownloadDataSource() {
new DownloadManagerImpl(new ArrayList<String>(), null);
}
private static DownloadMission generateFinishedDownloadMission() throws IOException {
File file = File.createTempFile("newpipetest", ".mp4");
file.deleteOnExit();
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
randomAccessFile.setLength(1000);
randomAccessFile.close();
DownloadMission downloadMission = new DownloadMission(file.getName(),
"http://google.com/?q=how+to+google", file.getParent());
downloadMission.blocks = 1000;
downloadMission.done = 1000;
downloadMission.finished = true;
return spy(downloadMission);
}
private static void assertMissionEquals(String message, DownloadMission expected, DownloadMission actual) {
if(expected == actual) return;
assertEquals(message + ": Name", expected.name, actual.name);
assertEquals(message + ": Location", expected.location, actual.location);
assertEquals(message + ": Url", expected.url, actual.url);
}
@Test
public void testThatMissionsAreLoaded() throws IOException {
ArrayList<DownloadMission> missions = new ArrayList<>();
long millis = System.currentTimeMillis();
for(int i = 0; i < 50; ++i){
DownloadMission mission = generateFinishedDownloadMission();
mission.timestamp = millis - i; // reverse order by timestamp
missions.add(mission);
}
dowloadDataSource = mock(DownloadDataSource.class);
when(dowloadDataSource.loadMissions()).thenReturn(new ArrayList<>(missions));
downloadManager = new DownloadManagerImpl(new ArrayList<String>(), dowloadDataSource);
verify(dowloadDataSource, times(1)).loadMissions();
assertEquals(50, downloadManager.getCount());
for(int i = 0; i < 50; ++i) {
assertMissionEquals("mission " + i, missions.get(50 - 1 - i), downloadManager.getMission(i));
}
}
@Ignore
@Test
public void startMission() throws Exception {
DownloadMission mission = missions.get(0);
mission = spy(mission);
missions.set(0, mission);
String url = "https://github.com/favicon.ico";
// create a temp file and delete it so we have a temp directory
File tempFile = File.createTempFile("favicon",".ico");
String name = tempFile.getName();
String location = tempFile.getParent();
assertTrue(tempFile.delete());
int id = downloadManager.startMission(url, location, name, true, 10);
}
@Test
public void resumeMission() throws Exception {
DownloadMission mission = missions.get(0);
mission.running = true;
verify(mission, never()).start();
downloadManager.resumeMission(0);
verify(mission, never()).start();
mission.running = false;
downloadManager.resumeMission(0);
verify(mission, times(1)).start();
}
@Test
public void pauseMission() throws Exception {
DownloadMission mission = missions.get(0);
mission.running = false;
downloadManager.pauseMission(0);
verify(mission, never()).pause();
mission.running = true;
downloadManager.pauseMission(0);
verify(mission, times(1)).pause();
}
@Test
public void deleteMission() throws Exception {
DownloadMission mission = missions.get(0);
assertEquals(mission, downloadManager.getMission(0));
downloadManager.deleteMission(0);
verify(mission, times(1)).delete();
assertNotEquals(mission, downloadManager.getMission(0));
assertEquals(49, downloadManager.getCount());
}
@Test(expected = RuntimeException.class)
public void getMissionWithNegativeIndex() throws Exception {
downloadManager.getMission(-1);
}
@Test
public void getMission() throws Exception {
assertSame(missions.get(0), downloadManager.getMission(0));
assertSame(missions.get(1), downloadManager.getMission(1));
}
}