{
public static final String TAG = "FileDownloader";
diff --git a/app/src/main/java/org/schabi/newpipe/extractor/search/SearchEngine.java b/app/src/main/java/org/schabi/newpipe/extractor/search/SearchEngine.java
index f6e0434a6..b0dfbfc8a 100644
--- a/app/src/main/java/org/schabi/newpipe/extractor/search/SearchEngine.java
+++ b/app/src/main/java/org/schabi/newpipe/extractor/search/SearchEngine.java
@@ -4,7 +4,6 @@ import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import java.io.IOException;
-import java.util.List;
/**
* Created by Christian Schabesberger on 10.08.15.
@@ -26,7 +25,6 @@ import java.util.List;
* along with NewPipe. If not, see .
*/
-@SuppressWarnings("ALL")
public abstract class SearchEngine {
public static class NothingFoundException extends ExtractionException {
public NothingFoundException(String message) {
diff --git a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java
index ab815d9a1..55e05bb76 100644
--- a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java
+++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java
@@ -85,18 +85,18 @@ public class YoutubeSearchEngine extends SearchEngine {
Element el;
// both types of spell correction item
- if (!((el = item.select("div[class*=\"spell-correction\"]").first()) == null)) {
+ if ((el = item.select("div[class*=\"spell-correction\"]").first()) != null) {
collector.setSuggestion(el.select("a").first().text());
if(list.children().size() == 1) {
throw new NothingFoundException("Did you mean: " + el.select("a").first().text());
}
// search message item
- } else if (!((el = item.select("div[class*=\"search-message\"]").first()) == null)) {
+ } else if ((el = item.select("div[class*=\"search-message\"]").first()) != null) {
//result.errorMessage = el.text();
throw new NothingFoundException(el.text());
// video item type
- } else if (!((el = item.select("div[class*=\"yt-lockup-video\"").first()) == null)) {
+ } else if ((el = item.select("div[class*=\"yt-lockup-video\"").first()) != null) {
collector.commit(extractPreviewInfo(el));
} else {
//noinspection ConstantConditions
diff --git a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java
index b7cd33bc7..030878b03 100644
--- a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java
+++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java
@@ -46,7 +46,7 @@ public class YoutubeService extends StreamingService {
@Override
public StreamExtractor getExtractorInstance(String url)
throws ExtractionException, IOException {
- UrlIdHandler urlIdHandler = new YoutubeStreamUrlIdHandler();
+ UrlIdHandler urlIdHandler = YoutubeStreamUrlIdHandler.getInstance();
if(urlIdHandler.acceptUrl(url)) {
return new YoutubeStreamExtractor(urlIdHandler, url, getServiceId());
}
@@ -61,7 +61,7 @@ public class YoutubeService extends StreamingService {
@Override
public UrlIdHandler getUrlIdHandlerInstance() {
- return new YoutubeStreamUrlIdHandler();
+ return YoutubeStreamUrlIdHandler.getInstance();
}
@Override
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 5910a1efb..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 {
@@ -184,7 +184,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
// cached values
private static volatile String decryptionCode = "";
- UrlIdHandler urlidhandler = new YoutubeStreamUrlIdHandler();
+ UrlIdHandler urlidhandler = YoutubeStreamUrlIdHandler.getInstance();
String pageUrl = "";
public YoutubeStreamExtractor(UrlIdHandler urlIdHandler, String pageUrl, int serviceId)
diff --git a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamUrlIdHandler.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamUrlIdHandler.java
index 2c3f76293..f2e7a0483 100644
--- a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamUrlIdHandler.java
+++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamUrlIdHandler.java
@@ -1,12 +1,21 @@
package org.schabi.newpipe.extractor.services.youtube;
+import android.support.annotation.NonNull;
+
+import org.schabi.newpipe.extractor.Downloader;
+import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Parser;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.FoundAdException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
+import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
+import java.io.IOException;
import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.net.URLDecoder;
+import java.util.regex.Matcher;
/**
* Created by Christian Schabesberger on 02.02.16.
@@ -29,45 +38,55 @@ import java.net.URLDecoder;
*/
public class YoutubeStreamUrlIdHandler implements UrlIdHandler {
- @SuppressWarnings("WeakerAccess")
+
+ private static final YoutubeStreamUrlIdHandler instance = new YoutubeStreamUrlIdHandler();
+ private static final String ID_PATTERN = "([\\-a-zA-Z0-9_]{11})";
+
+ private YoutubeStreamUrlIdHandler() {}
+
+ public static YoutubeStreamUrlIdHandler getInstance() {
+ return instance;
+ }
+
@Override
public String getUrl(String videoId) {
return "https://www.youtube.com/watch?v=" + videoId;
}
- @SuppressWarnings("WeakerAccess")
@Override
public String getId(String url) throws ParsingException, IllegalArgumentException {
- if(url.isEmpty())
- {
+ if(url.isEmpty()) {
throw new IllegalArgumentException("The url parameter should not be empty");
}
- String id;
- if(url.contains("youtube")) {
- if(url.contains("attribution_link")) {
+ String id;
+ String lowercaseUrl = url.toLowerCase();
+ if(lowercaseUrl.contains("youtube")) {
+ if (url.contains("attribution_link")) {
try {
String escapedQuery = Parser.matchGroup1("u=(.[^&|$]*)", url);
String query = URLDecoder.decode(escapedQuery, "UTF-8");
- id = Parser.matchGroup1("v=([\\-a-zA-Z0-9_]{11})", query);
- } catch(UnsupportedEncodingException uee) {
+ id = Parser.matchGroup1("v=" + ID_PATTERN, query);
+ } catch (UnsupportedEncodingException uee) {
throw new ParsingException("Could not parse attribution_link", uee);
}
- }
- else if(url.contains("vnd.youtube"))
- {
- id = Parser.matchGroup1("([\\-a-zA-Z0-9_]{11}).*", url);
+ } else if(lowercaseUrl.contains("youtube.com/shared?ci=")) {
+ return getRealIdFromSharedLink(url);
+ } else if (url.contains("vnd.youtube")) {
+ id = Parser.matchGroup1(ID_PATTERN, url);
+ } else if (url.contains("embed")) {
+ id = Parser.matchGroup1("embed/" + ID_PATTERN, url);
} else if(url.contains("googleads")) {
throw new FoundAdException("Error found add: " + url);
} else {
- id = Parser.matchGroup1("[?&]v=([\\-a-zA-Z0-9_]{11})", url);
+ id = Parser.matchGroup1("[?&]v=" + ID_PATTERN, url);
}
}
- else if(url.contains("youtu.be")) {
+ else if(lowercaseUrl.contains("youtu.be")) {
if(url.contains("v=")) {
- id = Parser.matchGroup1("v=([\\-a-zA-Z0-9_]{11})", url);
+ id = Parser.matchGroup1("v=" + ID_PATTERN, url);
} else {
- id = Parser.matchGroup1("youtu\\.be/([a-zA-Z0-9_-]{11})", url);
+ id = Parser.matchGroup1("[Yy][Oo][Uu][Tt][Uu]\\.[Bb][Ee]/" + ID_PATTERN, url);
}
}
else {
@@ -82,12 +101,55 @@ public class YoutubeStreamUrlIdHandler implements UrlIdHandler {
}
}
+ /**
+ * Get the real url from a shared uri.
+ *
+ * Shared URI's look like this:
+ *
+ * * https://www.youtube.com/shared?ci=PJICrTByb3E
+ * * vnd.youtube://www.youtube.com/shared?ci=PJICrTByb3E&feature=twitter-deep-link
+ *
+ * @param url The shared url
+ * @return the id of the stream
+ * @throws ParsingException
+ */
+ private @NonNull String getRealIdFromSharedLink(String url) throws ParsingException {
+ URI uri;
+ try {
+ uri = new URI(url);
+ } catch (URISyntaxException e) {
+ throw new ParsingException("Invalid shared link", e);
+ }
+ String sharedId = getSharedId(uri);
+ Downloader downloader = NewPipe.getDownloader();
+ String content;
+ try {
+ content = downloader.download("https://www.youtube.com/shared?ci=" + sharedId);
+ } catch (IOException | ReCaptchaException e) {
+ throw new ParsingException("Unable to resolve shared link", e);
+ }
+ // is this bad? is this fragile?:
+ String realId = Parser.matchGroup1("rel=\"shortlink\" href=\"https://youtu.be/" + ID_PATTERN, content);
+ if(sharedId.equals(realId)) {
+ throw new ParsingException("Got same id for as shared id: " + sharedId);
+ }
+ return realId;
+ }
+
+ private @NonNull String getSharedId(URI uri) throws ParsingException {
+ if (!"/shared".equals(uri.getPath())) {
+ throw new ParsingException("Not a shared link: " + uri.toString() + " (path != " + uri.getPath() + ")");
+ }
+ return Parser.matchGroup1("ci=" + ID_PATTERN, uri.getQuery());
+ }
+
public String cleanUrl(String complexUrl) throws ParsingException {
return getUrl(getId(complexUrl));
}
@Override
public boolean acceptUrl(String videoUrl) {
+ videoUrl = videoUrl.toLowerCase();
return videoUrl.contains("youtube") ||
videoUrl.contains("youtu.be");
}
diff --git a/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamPreviewInfoCollector.java b/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamPreviewInfoCollector.java
index ea978f730..20120fa2d 100644
--- a/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamPreviewInfoCollector.java
+++ b/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamPreviewInfoCollector.java
@@ -1,5 +1,6 @@
package org.schabi.newpipe.extractor.stream_info;
+import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.FoundAdException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
@@ -60,7 +61,7 @@ public class StreamPreviewInfoCollector {
if (urlIdHandler == null) {
throw new ParsingException("Error: UrlIdHandler not set");
} else if(!resultItem.webpage_url.isEmpty()) {
- resultItem.id = (new YoutubeStreamUrlIdHandler()).getId(resultItem.webpage_url);
+ resultItem.id = NewPipe.getService(serviceId).getUrlIdHandlerInstance().getId(resultItem.webpage_url);
}
resultItem.title = extractor.getTitle();
resultItem.stream_type = extractor.getStreamType();
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemHolder.java
index dbba4ac5f..690376465 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemHolder.java
@@ -30,13 +30,13 @@ import org.schabi.newpipe.R;
public class InfoItemHolder extends RecyclerView.ViewHolder {
- public ImageView itemThumbnailView;
- public TextView itemVideoTitleView,
+ public final ImageView itemThumbnailView;
+ public final TextView itemVideoTitleView,
itemUploaderView,
itemDurationView,
itemUploadDateView,
itemViewCountView;
- public Button itemButton;
+ public final Button itemButton;
public InfoItemHolder(View v) {
super(v);
@@ -48,4 +48,5 @@ public class InfoItemHolder extends RecyclerView.ViewHolder {
itemViewCountView = (TextView) v.findViewById(R.id.itemViewCountView);
itemButton = (Button) v.findViewById(R.id.item_button);
}
+
}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java
index 30d3803ac..cc5a0f4bb 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java
@@ -34,11 +34,12 @@ import java.util.Vector;
public class InfoListAdapter extends RecyclerView.Adapter {
- InfoItemBuilder infoItemBuilder = null;
- List streamList = new Vector<>();
+ private final InfoItemBuilder infoItemBuilder;
+ private final List streamList;
public InfoListAdapter(Activity a, View rootView) {
infoItemBuilder = new InfoItemBuilder(a, rootView);
+ streamList = new Vector<>();
}
public void setOnItemSelectedListener
@@ -54,7 +55,7 @@ public class InfoListAdapter extends RecyclerView.Adapter {
}
public void clearSteamItemList() {
- streamList = new Vector<>();
+ streamList.clear();
notifyDataSetChanged();
}
diff --git a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java
index 8ebb9b155..039ed8eaa 100644
--- a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java
@@ -14,7 +14,6 @@ import android.preference.PreferenceManager;
import android.support.design.widget.Snackbar;
import android.support.v4.app.NavUtils;
import android.support.v7.app.ActionBar;
-import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
@@ -35,6 +34,7 @@ import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.ThemableActivity;
import org.schabi.newpipe.extractor.Parser;
import java.io.PrintWriter;
@@ -65,7 +65,7 @@ import java.util.Vector;
* along with NewPipe. If not, see .
*/
-public class ErrorActivity extends AppCompatActivity {
+public class ErrorActivity extends ThemableActivity {
public static class ErrorInfo implements Parcelable {
public int userAction;
public String request;
diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java
index 57b63c209..09e7c0903 100644
--- a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java
@@ -56,13 +56,15 @@ public class SearchInfoItemFragment extends Fragment {
private static final String TAG = SearchInfoItemFragment.class.toString();
+ /**
+ * Listener for search queries
+ */
public class SearchQueryListener implements SearchView.OnQueryTextListener {
@Override
public boolean onQueryTextSubmit(String query) {
Activity a = getActivity();
try {
- searchQuery = query;
search(query);
// hide virtual keyboard
@@ -89,8 +91,6 @@ public class SearchInfoItemFragment extends Fragment {
} catch (Exception e) {
e.printStackTrace();
}
- View bg = a.findViewById(R.id.mainBG);
- bg.setVisibility(View.GONE);
return true;
}
@@ -108,12 +108,10 @@ public class SearchInfoItemFragment extends Fragment {
private boolean isLoading = false;
private ProgressBar loadingIndicator = null;
- private SearchView searchView = null;
private int pageNumber = 0;
private SuggestionListAdapter suggestionListAdapter = null;
private InfoListAdapter infoListAdapter = null;
private LinearLayoutManager streamInfoListLayoutManager = null;
- private RecyclerView recyclerView = null;
// savedInstanceBundle arguments
private static final String QUERY = "query";
@@ -126,23 +124,32 @@ public class SearchInfoItemFragment extends Fragment {
public SearchInfoItemFragment() {
}
- // TODO: Customize parameter initialization
@SuppressWarnings("unused")
- public static SearchInfoItemFragment newInstance(int columnCount) {
+ public static SearchInfoItemFragment newInstance(int streamingServiceId, String searchQuery) {
+ Bundle args = new Bundle();
+ args.putInt(STREAMING_SERVICE, streamingServiceId);
+ args.putString(QUERY, searchQuery);
SearchInfoItemFragment fragment = new SearchInfoItemFragment();
+ fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
-
+ searchQuery = "";
if (savedInstanceState != null) {
searchQuery = savedInstanceState.getString(QUERY);
streamingServiceId = savedInstanceState.getInt(STREAMING_SERVICE);
} else {
try {
- streamingServiceId = NewPipe.getIdOfService("Youtube");
+ Bundle args = getArguments();
+ if(args != null) {
+ searchQuery = args.getString(QUERY);
+ streamingServiceId = args.getInt(STREAMING_SERVICE);
+ } else {
+ streamingServiceId = NewPipe.getIdOfService("Youtube");
+ }
} catch (Exception e) {
e.printStackTrace();
ErrorActivity.reportError(getActivity(), e, null,
@@ -153,13 +160,14 @@ public class SearchInfoItemFragment extends Fragment {
}
}
+ setHasOptionsMenu(true);
+
SearchWorker sw = SearchWorker.getInstance();
sw.setSearchWorkerResultListener(new SearchWorker.SearchWorkerResultListener() {
@Override
public void onResult(SearchResult result) {
infoListAdapter.addStreamItemList(result.resultList);
- isLoading = false;
- loadingIndicator.setVisibility(View.GONE);
+ setDoneLoading();
}
@Override
@@ -167,8 +175,7 @@ public class SearchInfoItemFragment extends Fragment {
//setListShown(true);
Toast.makeText(getActivity(), getString(stringResource),
Toast.LENGTH_SHORT).show();
- isLoading = false;
- loadingIndicator.setVisibility(View.GONE);
+ setDoneLoading();
}
@Override
@@ -176,8 +183,7 @@ public class SearchInfoItemFragment extends Fragment {
//setListShown(true);
Toast.makeText(getActivity(), message,
Toast.LENGTH_LONG).show();
- isLoading = false;
- loadingIndicator.setVisibility(View.GONE);
+ setDoneLoading();
}
@Override
@@ -191,6 +197,7 @@ public class SearchInfoItemFragment extends Fragment {
RECAPTCHA_REQUEST);
}
});
+
}
@Override
@@ -200,7 +207,7 @@ public class SearchInfoItemFragment extends Fragment {
Context context = view.getContext();
loadingIndicator = (ProgressBar) view.findViewById(R.id.progressBar);
- recyclerView = (RecyclerView) view.findViewById(R.id.list);
+ RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.list);
streamInfoListLayoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(streamInfoListLayoutManager);
@@ -209,15 +216,12 @@ public class SearchInfoItemFragment extends Fragment {
infoListAdapter.setOnItemSelectedListener(new InfoItemBuilder.OnItemSelectedListener() {
@Override
public void selected(String url) {
- Intent i = new Intent(getActivity(), VideoItemDetailActivity.class);
- i.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId);
- i.putExtra(VideoItemDetailFragment.VIDEO_URL, url);
- getActivity().startActivity(i);
+ startDetailActivity(url);
}
});
recyclerView.setAdapter(infoListAdapter);
-
- recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
+ recyclerView.clearOnScrollListeners();
+ recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
int pastVisiblesItems, visibleItemCount, totalItemCount;
@@ -239,14 +243,26 @@ public class SearchInfoItemFragment extends Fragment {
return view;
}
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
+ private void startDetailActivity(String url) {
+ Intent i = new Intent(getActivity(), VideoItemDetailActivity.class);
+ i.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId);
+ i.putExtra(VideoItemDetailFragment.VIDEO_URL, url);
+ getActivity().startActivity(i);
}
@Override
- public void onDetach() {
- super.onDetach();
+ public void onStart() {
+ super.onStart();
+ if(!searchQuery.isEmpty()) {
+ search(searchQuery);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putString(QUERY, searchQuery);
+ outState.putInt(STREAMING_SERVICE, streamingServiceId);
}
@Override
@@ -255,15 +271,10 @@ public class SearchInfoItemFragment extends Fragment {
inflater.inflate(R.menu.search_menu, menu);
MenuItem searchItem = menu.findItem(R.id.action_search);
- searchView = (SearchView) searchItem.getActionView();
+ SearchView searchView = (SearchView) searchItem.getActionView();
setupSearchView(searchView);
}
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- return super.onOptionsItemSelected(item);
- }
-
private void setupSearchView(SearchView searchView) {
suggestionListAdapter = new SuggestionListAdapter(getActivity());
searchView.setSuggestionsAdapter(suggestionListAdapter);
@@ -278,7 +289,9 @@ public class SearchInfoItemFragment extends Fragment {
private void search(String query) {
infoListAdapter.clearSteamItemList();
pageNumber = 0;
+ searchQuery = query;
search(query, pageNumber);
+ hideBackground();
loadingIndicator.setVisibility(View.VISIBLE);
}
@@ -288,6 +301,20 @@ public class SearchInfoItemFragment extends Fragment {
sw.search(streamingServiceId, query, page, getActivity());
}
+ private void setDoneLoading() {
+ this.isLoading = false;
+ loadingIndicator.setVisibility(View.GONE);
+ }
+
+ /**
+ * Hides the "dummy" background when no results are shown
+ */
+ private void hideBackground() {
+ View view = getView();
+ if(view == null) return;
+ view.findViewById(R.id.mainBG).setVisibility(View.GONE);
+ }
+
private void searchSuggestions(String query) {
SuggestionSearchRunnable suggestionSearchRunnable =
new SuggestionSearchRunnable(streamingServiceId, query, getActivity(), suggestionListAdapter);
diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchSuggestionListener.java b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchSuggestionListener.java
index df6aacd6b..a3d3c0e9b 100644
--- a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchSuggestionListener.java
+++ b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchSuggestionListener.java
@@ -25,8 +25,8 @@ import android.support.v7.widget.SearchView;
public class SearchSuggestionListener implements SearchView.OnSuggestionListener{
- private SearchView searchView;
- private SuggestionListAdapter adapter;
+ private final SearchView searchView;
+ private final SuggestionListAdapter adapter;
public SearchSuggestionListener(SearchView searchView, SuggestionListAdapter adapter) {
this.searchView = searchView;
diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchWorker.java b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchWorker.java
index 6156d356d..9b1e8d86e 100644
--- a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchWorker.java
+++ b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchWorker.java
@@ -82,6 +82,7 @@ public class SearchWorker {
}
@Override
public void run() {
+ final String serviceName = NewPipe.getNameOfService(serviceId);
SearchResult result = null;
SearchEngine engine = null;
@@ -119,7 +120,7 @@ public class SearchWorker {
View rootView = a.findViewById(android.R.id.content);
ErrorActivity.reportError(h, a, result.errors, null, rootView,
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
- /* todo: this shoudl not be assigned static */ YOUTUBE, query, R.string.light_parsing_error));
+ serviceName, query, R.string.light_parsing_error));
}
// hard errors:
@@ -148,8 +149,7 @@ public class SearchWorker {
} catch(ExtractionException e) {
ErrorActivity.reportError(h, a, e, null, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
- /* todo: this shoudl not be assigned static */
- YOUTUBE, query, R.string.parsing_error));
+ serviceName, query, R.string.parsing_error));
//postNewErrorToast(h, R.string.parsing_error);
e.printStackTrace();
diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionListAdapter.java
index dc085f685..c37a71895 100644
--- a/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionListAdapter.java
+++ b/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionListAdapter.java
@@ -3,10 +3,8 @@ package org.schabi.newpipe.search_fragment;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
-import android.support.v4.widget.CursorAdapter;
-import android.view.LayoutInflater;
+import android.support.v4.widget.ResourceCursorAdapter;
import android.view.View;
-import android.view.ViewGroup;
import android.widget.TextView;
import java.util.List;
@@ -31,52 +29,56 @@ import java.util.List;
* along with NewPipe. If not, see .
*/
-public class SuggestionListAdapter extends CursorAdapter {
+/**
+ * {@link ResourceCursorAdapter} to display suggestions.
+ */
+public class SuggestionListAdapter extends ResourceCursorAdapter {
+
+ private static final String[] columns = new String[]{"_id", "title"};
+ private static final int INDEX_ID = 0;
+ private static final int INDEX_TITLE = 1;
- private String[] columns = new String[]{"_id", "title"};
public SuggestionListAdapter(Context context) {
- super(context, null, false);
- }
-
- @Override
- public View newView(Context context, Cursor cursor, ViewGroup parent) {
- ViewHolder viewHolder;
-
- View view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent, false);
- viewHolder = new ViewHolder();
- viewHolder.suggestionTitle = (TextView) view.findViewById(android.R.id.text1);
- view.setTag(viewHolder);
-
-
- return view;
+ super(context, android.R.layout.simple_list_item_1, null, 0);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
- ViewHolder viewHolder = (ViewHolder) view.getTag();
- viewHolder.suggestionTitle.setText(cursor.getString(1));
+ ViewHolder viewHolder = new ViewHolder(view);
+ viewHolder.suggestionTitle.setText(cursor.getString(INDEX_TITLE));
}
-
+ /**
+ * Update the suggestion list
+ * @param suggestions the list of suggestions
+ */
public void updateAdapter(List suggestions) {
- MatrixCursor cursor = new MatrixCursor(columns);
+ MatrixCursor cursor = new MatrixCursor(columns, suggestions.size());
int i = 0;
- for (String s : suggestions) {
- String[] temp = new String[2];
- temp[0] = Integer.toString(i);
- temp[1] = s;
+ for (String suggestion : suggestions) {
+ String[] columnValues = new String[columns.length];
+ columnValues[INDEX_TITLE] = suggestion;
+ columnValues[INDEX_ID] = Integer.toString(i);
+ cursor.addRow(columnValues);
i++;
- cursor.addRow(temp);
}
changeCursor(cursor);
}
+ /**
+ * Get the suggestion for a position
+ * @param position the position of the suggestion
+ * @return the suggestion
+ */
public String getSuggestion(int position) {
- return ((Cursor) getItem(position)).getString(1);
+ return ((Cursor) getItem(position)).getString(INDEX_TITLE);
}
private class ViewHolder {
- public TextView suggestionTitle;
+ private final TextView suggestionTitle;
+ private ViewHolder(View view) {
+ this.suggestionTitle = (TextView) view.findViewById(android.R.id.text1);
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionSearchRunnable.java b/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionSearchRunnable.java
index ed090d230..0110052ec 100644
--- a/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionSearchRunnable.java
+++ b/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionSearchRunnable.java
@@ -37,14 +37,15 @@ import java.util.List;
public class SuggestionSearchRunnable implements Runnable{
+ /**
+ * Runnable to update a {@link SuggestionListAdapter}
+ */
private class SuggestionResultRunnable implements Runnable{
- private List suggestions;
- private SuggestionListAdapter adapter;
+ private final List suggestions;
- private SuggestionResultRunnable(List suggestions, SuggestionListAdapter adapter) {
+ private SuggestionResultRunnable(List suggestions) {
this.suggestions = suggestions;
- this.adapter = adapter;
}
@Override
@@ -55,9 +56,9 @@ public class SuggestionSearchRunnable implements Runnable{
private final int serviceId;
private final String query;
- final Handler h = new Handler();
- private Activity a = null;
- private SuggestionListAdapter adapter;
+ private final Handler h = new Handler();
+ private final Activity a;
+ private final SuggestionListAdapter adapter;
public SuggestionSearchRunnable(int serviceId, String query,
Activity activity, SuggestionListAdapter adapter) {
this.serviceId = serviceId;
@@ -76,7 +77,7 @@ public class SuggestionSearchRunnable implements Runnable{
String searchLanguage = sp.getString(searchLanguageKey,
a.getString(R.string.default_language_value));
List suggestions = se.suggestionList(query, searchLanguage);
- h.post(new SuggestionResultRunnable(suggestions, adapter));
+ h.post(new SuggestionResultRunnable(suggestions));
} catch (ExtractionException e) {
ErrorActivity.reportError(h, a, e, null, a.findViewById(android.R.id.content),
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
index e62f90c45..be4dbfc03 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
@@ -5,6 +5,7 @@ import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.preference.PreferenceActivity;
+import android.preference.PreferenceManager;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.v7.app.ActionBar;
@@ -16,6 +17,8 @@ import android.view.ViewGroup;
import org.schabi.newpipe.R;
+import java.util.Objects;
+
/**
* Created by Christian Schabesberger on 31.08.15.
@@ -43,6 +46,10 @@ public class SettingsActivity extends PreferenceActivity {
@Override
protected void onCreate(Bundle savedInstanceBundle) {
+ if (Objects.equals(PreferenceManager.getDefaultSharedPreferences(this)
+ .getString("theme", getResources().getString(R.string.light_theme_title)), getResources().getString(R.string.dark_theme_title))) {
+ setTheme(R.style.DarkTheme);
+ }
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceBundle);
super.onCreate(savedInstanceBundle);
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsFragment.java
index a5521dfc5..a6466daf9 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsFragment.java
@@ -1,6 +1,7 @@
package org.schabi.newpipe.settings;
import android.app.Activity;
+import android.app.ListActivity;
import android.content.ClipData;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -20,6 +21,7 @@ import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import java.util.ArrayList;
+import java.util.Objects;
import info.guardianproject.netcipher.proxy.OrbotHelper;
@@ -55,6 +57,7 @@ public class SettingsFragment extends PreferenceFragment
String DOWNLOAD_PATH_PREFERENCE;
String DOWNLOAD_PATH_AUDIO_PREFERENCE;
String USE_TOR_KEY;
+ String THEME;
public static final int REQUEST_INSTALL_ORBOT = 0x1234;
@@ -63,11 +66,11 @@ public class SettingsFragment extends PreferenceFragment
private ListPreference searchLanguagePreference;
private Preference downloadPathPreference;
private Preference downloadPathAudioPreference;
+ private Preference themePreference;
private SharedPreferences defaultPreferences;
-
@Override
- public void onCreate(Bundle savedInstanceState) {
+ public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.settings);
@@ -81,6 +84,7 @@ public class SettingsFragment extends PreferenceFragment
SEARCH_LANGUAGE_PREFERENCE = getString(R.string.search_language_key);
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
+ THEME = getString(R.string.theme_key);
USE_TOR_KEY = getString(R.string.use_tor_key);
// get pref objects
@@ -92,6 +96,7 @@ public class SettingsFragment extends PreferenceFragment
(ListPreference) findPreference(SEARCH_LANGUAGE_PREFERENCE);
downloadPathPreference = findPreference(DOWNLOAD_PATH_PREFERENCE);
downloadPathAudioPreference = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
+ themePreference = findPreference(THEME);
prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
@@ -132,6 +137,11 @@ public class SettingsFragment extends PreferenceFragment
downloadPathAudioPreference
.setSummary(downloadPath);
}
+ else if (key == THEME)
+ {
+ String theme = sharedPreferences.getString(THEME, "Light");
+ themePreference.setSummary(theme);
+ }
updateSummary();
}
};
@@ -161,7 +171,6 @@ public class SettingsFragment extends PreferenceFragment
activity.startActivityForResult(i, R.string.download_path_audio_key);
}
}
-
return super.onPreferenceTreeClick(preferenceScreen, preference);
}
@@ -216,8 +225,8 @@ public class SettingsFragment extends PreferenceFragment
// installing the app does not necessarily return RESULT_OK
App.configureTor(requestCode == REQUEST_INSTALL_ORBOT
&& OrbotHelper.requestStartTor(a));
-
}
+
updateSummary();
super.onActivityResult(requestCode, resultCode, data);
}
@@ -239,6 +248,9 @@ public class SettingsFragment extends PreferenceFragment
downloadPathAudioPreference.setSummary(
defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE,
getString(R.string.download_path_audio_summary)));
+ themePreference.setSummary(
+ defaultPreferences.getString(THEME,
+ getString(R.string.light_theme_title)));
}
@Override
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
+ android:drawable="?android:attr/windowBackground"/>
-
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ tools:context=".detail.VideoItemDetailFragment"
+ android:textIsSelectable="true"
+ style="?android:attr/textAppearanceLarge"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/video_item_detail">
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:indeterminate="true"/>
+ android:contentDescription="@string/detail_thumbnail_view_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scaleType="fitCenter"
+ android:adjustViewBounds="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:background="@android:color/black"
+ android:src="@drawable/dummy_thumbnail_dark"/>
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent"
+ android:layout_centerInParent="true"
+ android:src="@drawable/new_play_arrow"
+ android:visibility="invisible"/>