diff --git a/app/build.gradle b/app/build.gradle
index 536d304c8..6021f5433 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -46,5 +46,6 @@ dependencies {
compile 'com.google.code.gson:gson:2.4'
compile 'com.nononsenseapps:filepicker:3.0.0'
testCompile 'junit:junit:4.12'
+ testCompile 'org.mockito:mockito-core:1.10.19'
compile 'ch.acra:acra:4.9.0'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9c5ea2381..2b44f0b53 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -161,6 +161,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java
index 0d6893ebe..cded0f074 100644
--- a/app/src/main/java/org/schabi/newpipe/MainActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java
@@ -10,6 +10,7 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import org.schabi.newpipe.settings.SettingsActivity;
+import org.schabi.newpipe.util.PermissionHelper;
/**
* Created by Christian Schabesberger on 02.08.16.
@@ -72,6 +73,9 @@ public class MainActivity extends ThemableActivity {
return true;
}
case R.id.action_show_downloads: {
+ if(!PermissionHelper.checkStoragePermissions(this)) {
+ return false;
+ }
Intent intent = new Intent(this, org.schabi.newpipe.download.DownloadActivity.class);
startActivity(intent);
return true;
diff --git a/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java b/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java
index 98620b15d..704e74b5b 100644
--- a/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java
@@ -57,8 +57,8 @@ import org.schabi.newpipe.player.BackgroundPlayer;
import org.schabi.newpipe.player.ExoPlayerActivity;
import org.schabi.newpipe.player.PlayVideoActivity;
import org.schabi.newpipe.report.ErrorActivity;
-
import java.util.Vector;
+import org.schabi.newpipe.util.PermissionHelper;
import static android.app.Activity.RESULT_OK;
import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST;
@@ -393,6 +393,10 @@ public class VideoItemDetailFragment extends Fragment {
actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() {
@Override
public void onActionSelected(int selectedStreamId) {
+ if(!PermissionHelper.checkStoragePermissions(getActivity())) {
+ return;
+ }
+
try {
Bundle args = new Bundle();
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java
index e7ff154fe..bb85c4887 100644
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java
@@ -14,6 +14,7 @@ import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -30,6 +31,7 @@ import android.widget.Toast;
import org.schabi.newpipe.ThemableActivity;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.settings.SettingsActivity;
import java.io.File;
@@ -53,26 +55,11 @@ public class DownloadActivity extends ThemableActivity implements AdapterView.On
private MissionsFragment mFragment;
- private DownloadManager mManager;
- private DownloadManagerService.DMBinder mBinder;
+
private String mPendingUrl;
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
@TargetApi(21)
protected void onCreate(Bundle savedInstanceState) {
@@ -83,7 +70,6 @@ public class DownloadActivity extends ThemableActivity implements AdapterView.On
Intent i = new Intent();
i.setClass(this, DownloadManagerService.class);
startService(i);
- bindService(i, mConnection, Context.BIND_AUTO_CREATE);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_downloader);
@@ -91,7 +77,7 @@ public class DownloadActivity extends ThemableActivity implements AdapterView.On
//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.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(R.string.downloads_title);
@@ -202,22 +188,24 @@ public class DownloadActivity extends ThemableActivity implements AdapterView.On
@Override
public boolean onMenuItemClick(MenuItem item) {
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();
- File f = new File(mManager.getLocation() + "/" + fName);
-
+ File f = new File(location, fName);
if (f.exists()) {
Toast.makeText(DownloadActivity.this, R.string.msg_exists, Toast.LENGTH_SHORT).show();
} else {
-
- while (mBinder == null);
-
- int res = mManager.startMission(
- getIntent().getData().toString(),
- fName,
- audioButton.isChecked(),
- threads.getProgress() + 1);
- mBinder.onMissionAdded(mManager.getMission(res));
+ DownloadManagerService.startMission(
+ DownloadActivity.this,
+ getIntent().getData().toString(), location, fName,
+ audioButton.isChecked(), threads.getProgress() + 1);
mFragment.notifyChange();
mPrefs.edit().putInt(THREADS, threads.getProgress() + 1).commit();
@@ -277,4 +265,5 @@ public class DownloadActivity extends ThemableActivity implements AdapterView.On
super.onOptionsItemSelected(item);
}
}
+
}
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
index 2d2159768..22911fc19 100644
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
@@ -26,6 +26,8 @@ import android.widget.TextView;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.extractor.NewPipe;
+import org.schabi.newpipe.settings.NewPipeSettings;
import java.io.File;
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 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() {
}
@@ -102,12 +86,6 @@ public class DownloadDialog extends DialogFragment {
if(ContextCompat.checkSelfPermission(this.getContext(),Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED)
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);
}
@@ -219,36 +197,22 @@ public class DownloadDialog extends DialogFragment {
String fName = name.getText().toString().trim();
- // todo: add timeout? would be bad if the thread gets locked dueto this.
- while (mBinder == null);
-
- if(audioButton.isChecked()){
- int res = mManager.startMission(
- arguments.getString(AUDIO_URL),
- fName + arguments.getString(FILE_SUFFIX_AUDIO),
- audioButton.isChecked(),
- 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);
+ boolean isAudio = audioButton.isChecked();
+ String url, location, filename;
+ if(isAudio) {
+ url = arguments.getString(AUDIO_URL);
+ location = NewPipeSettings.getAudioDownloadPath(getContext());
+ filename = fName + arguments.getString(FILE_SUFFIX_AUDIO);
+ } else {
+ url = arguments.getString(VIDEO_URL);
+ location = NewPipeSettings.getVideoDownloadPath(getContext());
+ filename = fName + arguments.getString(FILE_SUFFIX_VIDEO);
}
- if(videoButton.isChecked()){
- int res = mManager.startMission(
- arguments.getString(VIDEO_URL),
- fName + arguments.getString(FILE_SUFFIX_VIDEO),
- audioButton.isChecked(),
- 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);
- }
+ DownloadManagerService.startMission(getContext(), url, location, filename, isAudio,
+ threads.getProgress() + 1);
+
getDialog().dismiss();
-
}
private void download(String url, String title,
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadListener.java b/app/src/main/java/org/schabi/newpipe/download/DownloadListener.java
deleted file mode 100644
index eab30fddd..000000000
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadListener.java
+++ /dev/null
@@ -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
- * 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 .
- */
-
-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 ...
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/download/FileDownloader.java b/app/src/main/java/org/schabi/newpipe/download/FileDownloader.java
index 1da8d0182..b097a035a 100644
--- a/app/src/main/java/org/schabi/newpipe/download/FileDownloader.java
+++ b/app/src/main/java/org/schabi/newpipe/download/FileDownloader.java
@@ -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 {
public static final String TAG = "FileDownloader";
diff --git a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamExtractor.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamExtractor.java
index 07a0e2ad9..63642e973 100644
--- a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamExtractor.java
+++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamExtractor.java
@@ -88,7 +88,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
// $$el_type$$ will be replaced by the actual el_type (se the declarations below)
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";
- // eltype is nececeary for the url aboth
+ // eltype is necessary for the url above
private static final String EL_INFO = "el=info";
public enum ItagType {
diff --git a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java
new file mode 100644
index 000000000..4c43426c5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java
@@ -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;
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadDataSource.java b/app/src/main/java/us/shandian/giga/get/DownloadDataSource.java
new file mode 100644
index 000000000..87f550cc4
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/get/DownloadDataSource.java
@@ -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 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);
+}
\ No newline at end of file
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadManager.java b/app/src/main/java/us/shandian/giga/get/DownloadManager.java
index 44eb0bb8e..b6579c86d 100644
--- a/app/src/main/java/us/shandian/giga/get/DownloadManager.java
+++ b/app/src/main/java/us/shandian/giga/get/DownloadManager.java
@@ -3,12 +3,46 @@ package us.shandian.giga.get;
public interface DownloadManager
{
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);
+
+ /**
+ * Pause the execution of a download mission.
+ * @param id the identifier of the mission to pause.
+ */
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);
+
+ /**
+ * 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);
+
+ /**
+ * Get the number of download missions.
+ * @return the number of download missions.
+ */
int getCount();
- String getLocation();
+
}
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java b/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java
index 498b9a079..3c37ac7d4 100755
--- a/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java
+++ b/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java
@@ -1,17 +1,21 @@
package us.shandian.giga.get;
-import android.content.Context;
+import android.support.annotation.Nullable;
import android.util.Log;
import com.google.gson.Gson;
-import org.schabi.newpipe.settings.NewPipeSettings;
-
import java.io.File;
+import java.io.FilenameFilter;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
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 static org.schabi.newpipe.BuildConfig.DEBUG;
@@ -19,33 +23,48 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadManagerImpl implements DownloadManager
{
private static final String TAG = DownloadManagerImpl.class.getSimpleName();
-
- private Context mContext;
- private String mLocation;
- protected ArrayList mMissions = new ArrayList();
-
- public DownloadManagerImpl(Context context, String location) {
- mContext = context;
- mLocation = location;
- loadMissions();
+ private final DownloadDataSource mDownloadDataSource;
+
+ private final ArrayList mMissions = new ArrayList();
+
+ /**
+ * Create a new instance
+ * @param searchLocations the directories to search for unfinished downloads
+ * @param downloadDataSource the data source for finished downloads
+ */
+ public DownloadManagerImpl(Collection searchLocations, DownloadDataSource downloadDataSource) {
+ mDownloadDataSource = downloadDataSource;
+ loadMissions(searchLocations);
}
-
+
@Override
- public int startMission(String url, String name, boolean isAudio, int threads) {
- DownloadMission mission = new DownloadMission();
- mission.url = url;
- mission.name = name;
- if(isAudio) {
- mission.location = NewPipeSettings.getAudioDownloadPath(mContext);
- } else {
- mission.location = NewPipeSettings.getVideoDownloadPath(mContext);
+ public int startMission(String url, String location, String name, boolean isAudio, int threads) {
+ DownloadMission existingMission = getMissionByLocation(location, name);
+ if(existingMission != null) {
+ // Already downloaded or downloading
+ if(existingMission.finished) {
+ // Overwrite mission
+ deleteMission(mMissions.indexOf(existingMission));
+ } else {
+ // 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.threadCount = threads;
- new Initializer(mContext, mission).start();
+ mission.addListener(new MissionListener(mission));
+ new Initializer(mission).start();
return insertMission(mission);
}
-
+
@Override
public void resumeMission(int i) {
DownloadMission d = getMission(i);
@@ -53,7 +72,7 @@ public class DownloadManagerImpl implements DownloadManager
d.start();
}
}
-
+
@Override
public void pauseMission(int i) {
DownloadMission d = getMission(i);
@@ -61,55 +80,94 @@ public class DownloadManagerImpl implements DownloadManager
d.pause();
}
}
-
+
@Override
public void deleteMission(int i) {
- getMission(i).delete();
+ DownloadMission mission = getMission(i);
+ if(mission.finished) {
+ mDownloadDataSource.deleteMission(mission);
+ }
+ mission.delete();
mMissions.remove(i);
}
-
- private void loadMissions() {
- File f = new File(mLocation);
+
+ private void loadMissions(Iterable searchLocations) {
+ mMissions.clear();
+ loadFinishedMissions();
+ for(String location: searchLocations) {
+ loadMissions(location);
+ }
+
+ }
+
+
+ /**
+ * Loads finished missions from the data source
+ */
+ private void loadFinishedMissions() {
+ List finishedMissions = mDownloadDataSource.loadMissions();
+ if(finishedMissions == null) {
+ finishedMissions = new ArrayList<>();
+ }
+ // Ensure its sorted
+ Collections.sort(finishedMissions, new Comparator() {
+ @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()) {
File[] subs = f.listFiles();
-
+
+ if(subs == null) {
+ Log.e(TAG, "listFiles() returned null");
+ return;
+ }
+
for (File sub : subs) {
- if (sub.isDirectory()) {
- continue;
- }
-
- if (sub.getName().endsWith(".giga")) {
+ if (sub.isFile() && sub.getName().endsWith(".giga")) {
String str = Utility.readFromFile(sub.getAbsolutePath());
if (str != null && !str.trim().equals("")) {
-
+
if (DEBUG) {
Log.d(TAG, "loading mission " + sub.getName());
Log.d(TAG, str);
}
-
+
DownloadMission mis = new Gson().fromJson(str, DownloadMission.class);
-
+
if (mis.finished) {
- sub.delete();
+ if(!sub.delete()) {
+ Log.w(TAG, "Unable to delete .giga file: " + sub.getPath());
+ }
continue;
}
-
+
mis.running = false;
mis.recovered = true;
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);
}
}
}
@@ -144,18 +202,81 @@ public class DownloadManagerImpl implements DownloadManager
return i;
}
-
- @Override
- public String getLocation() {
- return mLocation;
+
+ /**
+ * 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
+ public boolean accept(File dir, String name) {
+ 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 Context context;
private DownloadMission mission;
- public Initializer(Context context, DownloadMission mission) {
- this.context = context;
+ public Initializer(DownloadMission 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) {
+ }
+ }
}
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java
index 1cdef125f..54e84c5ab 100644
--- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java
+++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java
@@ -1,6 +1,5 @@
package us.shandian.giga.get;
-import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
@@ -10,39 +9,63 @@ import com.google.gson.Gson;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
-import java.util.Iterator;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import us.shandian.giga.util.Utility;
+
import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadMission
{
private static final String TAG = DownloadMission.class.getSimpleName();
-
+
public interface MissionListener {
HashMap handlerStore = new HashMap<>();
- void onProgressUpdate(long done, long total);
- void onFinish();
- void onError(int errCode);
+ void onProgressUpdate(DownloadMission downloadMission, long done, long total);
+ void onFinish(DownloadMission downloadMission);
+ void onError(DownloadMission downloadMission, int errCode);
}
public static final int ERROR_SERVER_UNSUPPORTED = 206;
public static final int ERROR_UNKNOWN = 233;
-
- public String name = "";
- public String url = "";
- public String location = "";
+
+ /**
+ * The filename
+ */
+ 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;
+
+ /**
+ * Number of bytes
+ */
public long length;
+
+ /**
+ * Number of bytes downloaded
+ */
public long done;
public int threadCount = 3;
public int finishCount;
- public List threadPositions = new ArrayList();
- public Map blockState = new HashMap();
+ private List threadPositions = new ArrayList();
+ public final Map blockState = new HashMap();
public boolean running;
public boolean finished;
public boolean fallback;
@@ -53,23 +76,65 @@ public class DownloadMission
private transient ArrayList> mListeners = new ArrayList>();
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) {
+ checkBlock(block);
return blockState.containsKey(block) ? blockState.get(block) : false;
}
public void preserveBlock(long block) {
+ checkBlock(block);
synchronized (blockState) {
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) {
@@ -95,13 +160,16 @@ public class DownloadMission
MissionListener.handlerStore.get(listener).post(new Runnable() {
@Override
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() {
if (errCode > 0) return;
@@ -111,7 +179,10 @@ public class DownloadMission
onFinish();
}
}
-
+
+ /**
+ * Called when all parts are downloaded
+ */
private void onFinish() {
if (errCode > 0) return;
@@ -130,7 +201,7 @@ public class DownloadMission
MissionListener.handlerStore.get(listener).post(new Runnable() {
@Override
public void run() {
- listener.onFinish();
+ listener.onFinish(DownloadMission.this);
}
});
}
@@ -147,7 +218,7 @@ public class DownloadMission
MissionListener.handlerStore.get(listener).post(new Runnable() {
@Override
public void run() {
- listener.onError(errCode);
+ listener.onError(DownloadMission.this, errCode);
}
});
}
@@ -169,7 +240,10 @@ public class DownloadMission
}
}
}
-
+
+ /**
+ * Start downloading with multiple threads.
+ */
public void start() {
if (!running && !finished) {
running = true;
@@ -200,12 +274,19 @@ public class DownloadMission
// if (err)
}
}
-
+
+ /**
+ * Removes the file and the meta file
+ */
public void delete() {
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() {
if (!mWritingToFile) {
mWritingToFile = true;
@@ -218,14 +299,30 @@ public class DownloadMission
}.start();
}
}
-
+
+ /**
+ * Write this {@link DownloadMission} to the meta file.
+ */
private void doWriteThisToFile() {
synchronized (blockState) {
- Utility.writeToFile(location + "/" + name + ".giga", new Gson().toJson(this));
+ Utility.writeToFile(getMetaFilename(), new Gson().toJson(this));
}
}
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);
+ }
+
}
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java
index 1db03c4be..1df5e716f 100644
--- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java
+++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java
@@ -9,14 +9,19 @@ import java.net.URL;
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
{
private static final String TAG = DownloadRunnable.class.getSimpleName();
- private DownloadMission mMission;
- private int mId;
+ private final DownloadMission mMission;
+ private final int mId;
public DownloadRunnable(DownloadMission mission, int id) {
+ if(mission == null) throw new NullPointerException("mission is null");
mMission = mission;
mId = id;
}
@@ -86,7 +91,7 @@ public class DownloadRunnable implements Runnable
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) {
mMission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED;
notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
@@ -131,7 +136,7 @@ public class DownloadRunnable implements Runnable
notifyProgress(-total);
if (DEBUG) {
- Log.d(TAG, mId + ":position " + position + " retrying");
+ Log.d(TAG, mId + ":position " + position + " retrying", e);
}
}
}
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java
index 50bdce858..e0a737024 100644
--- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java
+++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java
@@ -8,10 +8,11 @@ import java.net.URL;
// Single-threaded fallback mode
public class DownloadRunnableFallback implements Runnable
{
- private DownloadMission mMission;
+ private final DownloadMission mMission;
//private int mId;
public DownloadRunnableFallback(DownloadMission mission) {
+ if(mission == null) throw new NullPointerException("mission is null");
//mId = id;
mMission = mission;
}
@@ -35,7 +36,7 @@ public class DownloadRunnableFallback implements Runnable
f.write(buf, 0, len);
notifyProgress(len);
- if (Thread.currentThread().interrupted()) {
+ if (Thread.interrupted()) {
break;
}
diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionSQLiteHelper.java b/app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionSQLiteHelper.java
new file mode 100644
index 000000000..6c29be474
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionSQLiteHelper.java
@@ -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 = "downloads.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;
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/SQLiteDownloadDataSource.java b/app/src/main/java/us/shandian/giga/get/sqlite/SQLiteDownloadDataSource.java
new file mode 100644
index 000000000..556e26a39
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/get/sqlite/SQLiteDownloadDataSource.java
@@ -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 loadMissions() {
+ ArrayList 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});
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
index 930075e0b..d83ed6dc8 100755
--- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
+++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
@@ -1,56 +1,96 @@
package us.shandian.giga.service;
+import android.Manifest;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
+import android.content.Context;
import android.content.Intent;
+import android.content.ServiceConnection;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.support.v4.app.NotificationCompat.Builder;
+import android.support.v4.content.PermissionChecker;
import android.util.Log;
+import android.widget.Toast;
+import org.schabi.newpipe.R;
import org.schabi.newpipe.download.DownloadActivity;
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.DownloadManagerImpl;
import us.shandian.giga.get.DownloadMission;
+import us.shandian.giga.get.sqlite.SQLiteDownloadDataSource;
+
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();
-
+
+ /**
+ * 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 DownloadManager mManager;
private Notification mNotification;
private Handler mHandler;
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
public void onCreate() {
super.onCreate();
-
+
if (DEBUG) {
Log.d(TAG, "onCreate");
}
-
+
mBinder = new DMBinder();
+ if(mDataSource == null) {
+ mDataSource = new SQLiteDownloadDataSource(this);
+ }
if (mManager == null) {
- String path = NewPipeSettings.getVideoDownloadPath(this);
- mManager = new DownloadManagerImpl(this, path);
+ ArrayList paths = new ArrayList<>(2);
+ paths.add(NewPipeSettings.getVideoDownloadPath(this));
+ paths.add(NewPipeSettings.getAudioDownloadPath(this));
+ mManager = new DownloadManagerImpl(paths, mDataSource);
if (DEBUG) {
Log.d(TAG, "mManager == null");
- Log.d(TAG, "Download directory: " + path);
+ Log.d(TAG, "Download directory: " + paths);
}
}
-
+
Intent i = new Intent();
i.setAction(Intent.ACTION_MAIN);
i.setClass(this, DownloadActivity.class);
@@ -83,28 +123,50 @@ public class DownloadManagerService extends Service implements DownloadMission.M
mHandler = new Handler(thread.getLooper()) {
@Override
public void handleMessage(Message msg) {
- if (msg.what == 0) {
- int runningCount = 0;
-
- for (int i = 0; i < mManager.getCount(); i++) {
- if (mManager.getMission(i).running) {
- runningCount++;
+ switch (msg.what) {
+ case UPDATE_MESSAGE: {
+ int runningCount = 0;
+
+ for (int i = 0; i < mManager.getCount(); i++) {
+ if (mManager.getMission(i).running) {
+ runningCount++;
+ }
}
+ updateState(runningCount);
+ break;
}
-
- updateState(runningCount);
}
}
};
}
+ 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
public int onStartCommand(Intent intent, int flags, int startId) {
if (DEBUG) {
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;
}
@@ -119,52 +181,76 @@ public class DownloadManagerService extends Service implements DownloadMission.M
for (int i = 0; i < mManager.getCount(); i++) {
mManager.pauseMission(i);
}
-
+
stopForeground(true);
}
@Override
public IBinder onBind(Intent intent) {
+ int permissionCheck;
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
+ if(permissionCheck == PermissionChecker.PERMISSION_DENIED) {
+ Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ if(permissionCheck == PermissionChecker.PERMISSION_DENIED) {
+ Toast.makeText(this, "Permission denied (write)", Toast.LENGTH_SHORT).show();
+ }
+
return mBinder;
}
-
- @Override
- public void onProgressUpdate(long done, long total) {
-
- long now = System.currentTimeMillis();
-
- long delta = now - mLastTimeStamp;
-
- if (delta > 2000) {
- postUpdateMessage();
- mLastTimeStamp = now;
- }
- }
-
- @Override
- public void onFinish() {
- postUpdateMessage();
- }
-
- @Override
- public void onError(int errCode) {
- postUpdateMessage();
- }
-
private void postUpdateMessage() {
- mHandler.sendEmptyMessage(0);
+ mHandler.sendEmptyMessage(UPDATE_MESSAGE);
}
private void updateState(int runningCount) {
if (runningCount == 0) {
stopForeground(true);
} else {
- startForeground(1000, mNotification);
+ 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
+ public void onProgressUpdate(DownloadMission downloadMission, long done, long total) {
+ long now = System.currentTimeMillis();
+ long delta = now - mLastTimeStamp;
+ if (delta > 2000) {
+ postUpdateMessage();
+ mLastTimeStamp = now;
+ }
+ }
+
+ @Override
+ public void onFinish(DownloadMission downloadMission) {
+ postUpdateMessage();
+ notifyMediaScanner(downloadMission);
+ }
+
+ @Override
+ public void onError(DownloadMission downloadMission, int errCode) {
+ postUpdateMessage();
+ }
+ }
+
+
// Wrapper of DownloadManager
public class DMBinder extends Binder {
public DownloadManager getDownloadManager() {
@@ -172,15 +258,13 @@ public class DownloadManagerService extends Service implements DownloadMission.M
}
public void onMissionAdded(DownloadMission mission) {
- mission.addListener(DownloadManagerService.this);
+ mission.addListener(missionListener);
postUpdateMessage();
}
public void onMissionRemoved(DownloadMission mission) {
- mission.removeListener(DownloadManagerService.this);
+ mission.removeListener(missionListener);
postUpdateMessage();
}
-
}
-
}
diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
index 050b4edf4..5960c64fd 100644
--- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
+++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
@@ -5,6 +5,9 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
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.Menu;
import android.view.MenuItem;
@@ -19,6 +22,7 @@ import android.support.v7.widget.RecyclerView;
import java.io.File;
import java.util.HashMap;
+import java.util.Locale;
import java.util.Map;
import org.schabi.newpipe.R;
@@ -28,10 +32,14 @@ import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.common.ProgressDrawable;
import us.shandian.giga.util.Utility;
+import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
+
public class MissionAdapter extends RecyclerView.Adapter
{
private static final Map ALGORITHMS = new HashMap<>();
-
+ private static final String TAG = "MissionAdapter";
+
static {
ALGORITHMS.put(R.id.md5, "MD5");
ALGORITHMS.put(R.id.sha1, "SHA1");
@@ -143,9 +151,8 @@ public class MissionAdapter extends RecyclerView.Adapter= 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 {
ProgressDialog prog;
@@ -280,7 +315,7 @@ public class MissionAdapter extends RecyclerView.Adapter
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/us/shandian/giga/get/get/DownloadManagerImplTest.java b/app/src/test/java/us/shandian/giga/get/get/DownloadManagerImplTest.java
new file mode 100644
index 000000000..8cef98cbe
--- /dev/null
+++ b/app/src/test/java/us/shandian/giga/get/get/DownloadManagerImplTest.java
@@ -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 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(), dowloadDataSource);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testConstructorWithNullAsDownloadDataSource() {
+ new DownloadManagerImpl(new ArrayList(), 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 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(), 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));
+ }
+
+}
\ No newline at end of file