Merge remote-tracking branch 'upstream/dev' into patch1_ui
This commit is contained in:
commit
0cb5197ccf
147 changed files with 3387 additions and 1765 deletions
|
|
@ -1,5 +1,6 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Application;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
|
|
@ -65,6 +66,7 @@ import io.reactivex.plugins.RxJavaPlugins;
|
|||
public class App extends Application {
|
||||
protected static final String TAG = App.class.toString();
|
||||
private RefWatcher refWatcher;
|
||||
private static App app;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final Class<? extends ReportSenderFactory>[]
|
||||
|
|
@ -88,6 +90,8 @@ public class App extends Application {
|
|||
}
|
||||
refWatcher = installLeakCanary();
|
||||
|
||||
app = this;
|
||||
|
||||
// Initialize settings first because others inits can use its values
|
||||
SettingsActivity.initSettings(this);
|
||||
|
||||
|
|
@ -100,6 +104,9 @@ public class App extends Application {
|
|||
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50));
|
||||
|
||||
configureRxJavaErrorHandler();
|
||||
|
||||
// Check for new version
|
||||
new CheckForNewAppVersionTask().execute();
|
||||
}
|
||||
|
||||
protected Downloader getDownloader() {
|
||||
|
|
@ -211,6 +218,31 @@ public class App extends Application {
|
|||
NotificationManager mNotificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
mNotificationManager.createNotificationChannel(mChannel);
|
||||
|
||||
setUpUpdateNotificationChannel(importance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up notification channel for app update.
|
||||
* @param importance
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private void setUpUpdateNotificationChannel(int importance) {
|
||||
|
||||
final String appUpdateId
|
||||
= getString(R.string.app_update_notification_channel_id);
|
||||
final CharSequence appUpdateName
|
||||
= getString(R.string.app_update_notification_channel_name);
|
||||
final String appUpdateDescription
|
||||
= getString(R.string.app_update_notification_channel_description);
|
||||
|
||||
NotificationChannel appUpdateChannel
|
||||
= new NotificationChannel(appUpdateId, appUpdateName, importance);
|
||||
appUpdateChannel.setDescription(appUpdateDescription);
|
||||
|
||||
NotificationManager appUpdateNotificationManager
|
||||
= (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
appUpdateNotificationManager.createNotificationChannel(appUpdateChannel);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
@ -226,4 +258,8 @@ public class App extends Application {
|
|||
protected boolean isDisposedRxExceptionsReported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static App getApp() {
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,230 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
/**
|
||||
* AsyncTask to check if there is a newer version of the NewPipe github apk available or not.
|
||||
* If there is a newer version we show a notification, informing the user. On tapping
|
||||
* the notification, the user will be directed to the download link.
|
||||
*/
|
||||
public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
||||
|
||||
private static final Application app = App.getApp();
|
||||
private static final String GITHUB_APK_SHA1 = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||
private static final String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json";
|
||||
private static final int timeoutPeriod = 30;
|
||||
|
||||
private SharedPreferences mPrefs;
|
||||
private OkHttpClient client;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
|
||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
|
||||
// Check if user has enabled/ disabled update checking
|
||||
// and if the current apk is a github one or not.
|
||||
if (!mPrefs.getBoolean(app.getString(R.string.update_app_key), true)
|
||||
|| !isGithubApk()) {
|
||||
this.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(Void... voids) {
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
if (client == null) {
|
||||
|
||||
client = new OkHttpClient
|
||||
.Builder()
|
||||
.readTimeout(timeoutPeriod, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(newPipeApiUrl)
|
||||
.build();
|
||||
|
||||
try {
|
||||
Response response = client.newCall(request).execute();
|
||||
return response.body().string();
|
||||
} catch (IOException ex) {
|
||||
ErrorActivity.reportError(app, ex, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"app update API fail", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String response) {
|
||||
|
||||
// Parse the json from the response.
|
||||
if (response != null) {
|
||||
|
||||
try {
|
||||
JSONObject mainObject = new JSONObject(response);
|
||||
JSONObject flavoursObject = mainObject.getJSONObject("flavors");
|
||||
JSONObject githubObject = flavoursObject.getJSONObject("github");
|
||||
JSONObject githubStableObject = githubObject.getJSONObject("stable");
|
||||
|
||||
String versionName = githubStableObject.getString("version");
|
||||
String versionCode = githubStableObject.getString("version_code");
|
||||
String apkLocationUrl = githubStableObject.getString("apk");
|
||||
|
||||
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
|
||||
|
||||
} catch (JSONException ex) {
|
||||
ErrorActivity.reportError(app, ex, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"could not parse app update JSON data", R.string.app_ui_crash));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to compare the current and latest available app version.
|
||||
* If a newer version is available, we show the update notification.
|
||||
* @param versionName
|
||||
* @param apkLocationUrl
|
||||
*/
|
||||
private void compareAppVersionAndShowNotification(String versionName,
|
||||
String apkLocationUrl,
|
||||
String versionCode) {
|
||||
|
||||
int NOTIFICATION_ID = 2000;
|
||||
|
||||
if (BuildConfig.VERSION_CODE < Integer.valueOf(versionCode)) {
|
||||
|
||||
// A pending intent to open the apk location url in the browser.
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||
PendingIntent pendingIntent
|
||||
= PendingIntent.getActivity(app, 0, intent, 0);
|
||||
|
||||
NotificationCompat.Builder notificationBuilder = new NotificationCompat
|
||||
.Builder(app, app.getString(R.string.app_update_notification_channel_id))
|
||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(app.getString(R.string.app_update_notification_content_title))
|
||||
.setContentText(app.getString(R.string.app_update_notification_content_text)
|
||||
+ " " + versionName);
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(app);
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the apk's SHA1 key.
|
||||
* https://stackoverflow.com/questions/9293019/get-certificate-fingerprint-from-android-app#22506133
|
||||
*/
|
||||
private static String getCertificateSHA1Fingerprint() {
|
||||
|
||||
PackageManager pm = app.getPackageManager();
|
||||
String packageName = app.getPackageName();
|
||||
int flags = PackageManager.GET_SIGNATURES;
|
||||
PackageInfo packageInfo = null;
|
||||
|
||||
try {
|
||||
packageInfo = pm.getPackageInfo(packageName, flags);
|
||||
} catch (PackageManager.NameNotFoundException ex) {
|
||||
ErrorActivity.reportError(app, ex, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not find package info", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
Signature[] signatures = packageInfo.signatures;
|
||||
byte[] cert = signatures[0].toByteArray();
|
||||
InputStream input = new ByteArrayInputStream(cert);
|
||||
|
||||
CertificateFactory cf = null;
|
||||
X509Certificate c = null;
|
||||
|
||||
try {
|
||||
cf = CertificateFactory.getInstance("X509");
|
||||
c = (X509Certificate) cf.generateCertificate(input);
|
||||
} catch (CertificateException ex) {
|
||||
ErrorActivity.reportError(app, ex, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Certificate error", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
String hexString = null;
|
||||
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
byte[] publicKey = md.digest(c.getEncoded());
|
||||
hexString = byte2HexFormatted(publicKey);
|
||||
} catch (NoSuchAlgorithmException ex1) {
|
||||
ErrorActivity.reportError(app, ex1, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
||||
} catch (CertificateEncodingException ex2) {
|
||||
ErrorActivity.reportError(app, ex2, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
return hexString;
|
||||
}
|
||||
|
||||
private static String byte2HexFormatted(byte[] arr) {
|
||||
|
||||
StringBuilder str = new StringBuilder(arr.length * 2);
|
||||
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
String h = Integer.toHexString(arr[i]);
|
||||
int l = h.length();
|
||||
if (l == 1) h = "0" + h;
|
||||
if (l > 2) h = h.substring(l - 2, l);
|
||||
str.append(h.toUpperCase());
|
||||
if (i < (arr.length - 1)) str.append(':');
|
||||
}
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
public static boolean isGithubApk() {
|
||||
|
||||
return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1);
|
||||
}
|
||||
}
|
||||
|
|
@ -457,7 +457,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
break;
|
||||
case R.id.subtitle_button:
|
||||
stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||
location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video go together
|
||||
location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video files go together
|
||||
kind = 's';
|
||||
break;
|
||||
default:
|
||||
|
|
@ -477,7 +477,6 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
final String finalFileName = fileName;
|
||||
|
||||
DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> {
|
||||
// should be safe run the following code without "getActivity().runOnUiThread()"
|
||||
if (listed) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.download_dialog_title)
|
||||
|
|
@ -511,11 +510,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
|
||||
if (secondaryStream != null) {
|
||||
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
||||
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||
psArgs = null;
|
||||
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
|
||||
|
||||
// set nearLength, only, if both sizes are fetched or known. this probably does not work on weak internet connections
|
||||
// set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks
|
||||
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
||||
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
public class ExtSDDownloadFailedActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.download_to_sdcard_error_title)
|
||||
.setMessage(R.string.download_to_sdcard_error_message)
|
||||
.setPositiveButton(R.string.yes, (DialogInterface dialogInterface, int i) -> {
|
||||
NewPipeSettings.resetDownloadFolders(this);
|
||||
finish();
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (DialogInterface dialogInterface, int i) -> {
|
||||
dialogInterface.dismiss();
|
||||
finish();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,7 @@ import org.schabi.newpipe.ReCaptchaActivity;
|
|||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
|
||||
|
|
@ -362,7 +363,8 @@ public class VideoDetailFragment
|
|||
}
|
||||
break;
|
||||
case R.id.detail_controls_download:
|
||||
if (PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
if (PermissionHelper.checkStoragePermissions(activity,
|
||||
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
this.openDownloadDialog();
|
||||
}
|
||||
break;
|
||||
|
|
@ -446,7 +448,6 @@ public class VideoDetailFragment
|
|||
return;
|
||||
}
|
||||
|
||||
//Log.d(TAG, "toggleExpandRelatedVideos() called with: info = [" + info + "], from = [" + INITIAL_RELATED_VIDEOS + "]");
|
||||
for (int i = INITIAL_RELATED_VIDEOS; i < info.getRelatedStreams().size(); i++) {
|
||||
InfoItem item = info.getRelatedStreams().get(i);
|
||||
//Log.d(TAG, "i = " + i);
|
||||
|
|
@ -519,7 +520,9 @@ public class VideoDetailFragment
|
|||
infoItemBuilder.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
|
||||
@Override
|
||||
public void selected(StreamInfoItem selectedItem) {
|
||||
selectAndLoadVideo(selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
selectAndLoadVideo(selectedItem.getServiceId(),
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -699,13 +702,13 @@ public class VideoDetailFragment
|
|||
switch (id) {
|
||||
case R.id.menu_item_share: {
|
||||
if (currentInfo != null) {
|
||||
shareUrl(currentInfo.getName(), currentInfo.getUrl());
|
||||
shareUrl(currentInfo.getName(), currentInfo.getOriginalUrl());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_openInBrowser: {
|
||||
if (currentInfo != null) {
|
||||
openUrlInBrowser(currentInfo.getUrl());
|
||||
openUrlInBrowser(currentInfo.getOriginalUrl());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -742,10 +745,16 @@ public class VideoDetailFragment
|
|||
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_external_video_player_key), false);
|
||||
|
||||
sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false);
|
||||
sortedVideoStreams = ListHelper.getSortedStreamVideosList(
|
||||
activity,
|
||||
info.getVideoStreams(),
|
||||
info.getVideoOnlyStreams(),
|
||||
false);
|
||||
selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
||||
|
||||
final StreamItemAdapter<VideoStream, Stream> streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams, activity), isExternalPlayerEnabled);
|
||||
final StreamItemAdapter<VideoStream, Stream> streamsAdapter =
|
||||
new StreamItemAdapter<>(activity,
|
||||
new StreamSizeWrapper<>(sortedVideoStreams, activity), isExternalPlayerEnabled);
|
||||
spinnerToolbar.setAdapter(streamsAdapter);
|
||||
spinnerToolbar.setSelection(selectedVideoStreamIndex);
|
||||
spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
|
|
@ -770,17 +779,17 @@ public class VideoDetailFragment
|
|||
*/
|
||||
protected final LinkedList<StackItem> stack = new LinkedList<>();
|
||||
|
||||
public void clearHistory() {
|
||||
stack.clear();
|
||||
}
|
||||
|
||||
public void pushToStack(int serviceId, String videoUrl, String name) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "pushToStack() called with: serviceId = [" + serviceId + "], videoUrl = [" + videoUrl + "], name = [" + name + "]");
|
||||
Log.d(TAG, "pushToStack() called with: serviceId = ["
|
||||
+ serviceId + "], videoUrl = [" + videoUrl + "], name = [" + name + "]");
|
||||
}
|
||||
|
||||
if (stack.size() > 0 && stack.peek().getServiceId() == serviceId && stack.peek().getUrl().equals(videoUrl)) {
|
||||
Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" + serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]");
|
||||
if (stack.size() > 0
|
||||
&& stack.peek().getServiceId() == serviceId
|
||||
&& stack.peek().getUrl().equals(videoUrl)) {
|
||||
Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = ["
|
||||
+ serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]");
|
||||
return;
|
||||
} else {
|
||||
Log.d(TAG, "pushToStack() wasn't equal");
|
||||
|
|
@ -811,7 +820,11 @@ public class VideoDetailFragment
|
|||
// Get stack item from the new top
|
||||
StackItem peek = stack.peek();
|
||||
|
||||
selectAndLoadVideo(peek.getServiceId(), peek.getUrl(), !TextUtils.isEmpty(peek.getTitle()) ? peek.getTitle() : "");
|
||||
selectAndLoadVideo(peek.getServiceId(),
|
||||
peek.getUrl(),
|
||||
!TextUtils.isEmpty(peek.getTitle())
|
||||
? peek.getTitle()
|
||||
: "");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -831,9 +844,10 @@ public class VideoDetailFragment
|
|||
}
|
||||
|
||||
public void prepareAndHandleInfo(final StreamInfo info, boolean scrollToTop) {
|
||||
if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = [" + info + "], scrollToTop = [" + scrollToTop + "]");
|
||||
if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = ["
|
||||
+ info + "], scrollToTop = [" + scrollToTop + "]");
|
||||
|
||||
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
||||
setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||
pushToStack(serviceId, url, name);
|
||||
showLoading();
|
||||
|
||||
|
|
@ -1026,7 +1040,8 @@ public class VideoDetailFragment
|
|||
|
||||
private void showContentWithAnimation(long duration,
|
||||
long delay,
|
||||
@FloatRange(from = 0.0f, to = 1.0f) float translationPercent) {
|
||||
@FloatRange(from = 0.0f, to = 1.0f)
|
||||
float translationPercent) {
|
||||
int translationY = (int) (getResources().getDisplayMetrics().heightPixels *
|
||||
(translationPercent > 0.0f ? translationPercent : .06f));
|
||||
|
||||
|
|
@ -1134,7 +1149,7 @@ public class VideoDetailFragment
|
|||
super.handleResult(info);
|
||||
|
||||
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
||||
pushToStack(serviceId, url, name);
|
||||
//pushToStack(serviceId, url, name);
|
||||
|
||||
animateView(thumbnailPlayButton, true, 200);
|
||||
videoTitleTextView.setText(name);
|
||||
|
|
@ -1185,11 +1200,13 @@ public class VideoDetailFragment
|
|||
|
||||
if (info.getDuration() > 0) {
|
||||
detailDurationView.setText(Localization.getDurationString(info.getDuration()));
|
||||
detailDurationView.setBackgroundColor(ContextCompat.getColor(activity, R.color.duration_background_color));
|
||||
detailDurationView.setBackgroundColor(
|
||||
ContextCompat.getColor(activity, R.color.duration_background_color));
|
||||
animateView(detailDurationView, true, 100);
|
||||
} else if (info.getStreamType() == StreamType.LIVE_STREAM) {
|
||||
detailDurationView.setText(R.string.duration_live);
|
||||
detailDurationView.setBackgroundColor(ContextCompat.getColor(activity, R.color.live_duration_background_color));
|
||||
detailDurationView.setBackgroundColor(
|
||||
ContextCompat.getColor(activity, R.color.live_duration_background_color));
|
||||
animateView(detailDurationView, true, 100);
|
||||
} else {
|
||||
detailDurationView.setVisibility(View.GONE);
|
||||
|
|
@ -1269,10 +1286,18 @@ public class VideoDetailFragment
|
|||
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(activity,
|
||||
R.string.could_not_setup_download_menu,
|
||||
Toast.LENGTH_LONG).show();
|
||||
e.printStackTrace();
|
||||
ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||
ServiceList.all()
|
||||
.get(currentInfo
|
||||
.getServiceId())
|
||||
.getServiceInfo()
|
||||
.getName(), "",
|
||||
R.string.could_not_setup_download_menu);
|
||||
|
||||
ErrorActivity.reportError(getActivity(),
|
||||
e,
|
||||
getActivity().getClass(),
|
||||
getActivity().findViewById(android.R.id.content), info);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -233,10 +233,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||
openRssFeed();
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser:
|
||||
openUrlInBrowser(url);
|
||||
openUrlInBrowser(currentInfo.getOriginalUrl());
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
shareUrl(name, url);
|
||||
shareUrl(name, currentInfo.getOriginalUrl());
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
|
|
|||
|
|
@ -104,8 +104,13 @@ public class SearchFragment
|
|||
// this three represet the current search query
|
||||
@State
|
||||
protected String searchString;
|
||||
|
||||
/**
|
||||
* No content filter should add like contentfilter = all
|
||||
* be aware of this when implementing an extractor.
|
||||
*/
|
||||
@State
|
||||
protected String[] contentFilter;
|
||||
protected String[] contentFilter = new String[0];
|
||||
@State
|
||||
protected String sortFilter;
|
||||
|
||||
|
|
@ -351,7 +356,7 @@ public class SearchFragment
|
|||
|| (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
||||
search(!TextUtils.isEmpty(searchString)
|
||||
? searchString
|
||||
: searchEditText.getText().toString(), new String[0], "");
|
||||
: searchEditText.getText().toString(), this.contentFilter, "");
|
||||
} else {
|
||||
if (searchEditText != null) {
|
||||
searchEditText.setText("");
|
||||
|
|
@ -752,6 +757,7 @@ public class SearchFragment
|
|||
|
||||
@Override
|
||||
protected void loadMoreItems() {
|
||||
if(nextPageUrl == null || nextPageUrl.isEmpty()) return;
|
||||
isLoading.set(true);
|
||||
showListFooter(true);
|
||||
if (searchDisposable != null) searchDisposable.dispose();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package org.schabi.newpipe.player.playback;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.MediaMetadataCompat;
|
||||
|
||||
import org.schabi.newpipe.player.BasePlayer;
|
||||
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
||||
|
|
@ -54,6 +56,12 @@ public class BasePlayerMediaSession implements MediaSessionCallback {
|
|||
.setTitle(item.getTitle())
|
||||
.setSubtitle(item.getUploader());
|
||||
|
||||
// set additional metadata for A2DP/AVRCP
|
||||
Bundle additionalMetadata = new Bundle();
|
||||
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader());
|
||||
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration());
|
||||
descriptionBuilder.setExtras(additionalMetadata);
|
||||
|
||||
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
|
||||
if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri);
|
||||
|
||||
|
|
|
|||
|
|
@ -93,15 +93,17 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||
// Below are auxiliary media sources
|
||||
|
||||
// Create subtitle sources
|
||||
for (final SubtitlesStream subtitle : info.getSubtitles()) {
|
||||
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
|
||||
if (mimeType == null) continue;
|
||||
if(info.getSubtitles() != null) {
|
||||
for (final SubtitlesStream subtitle : info.getSubtitles()) {
|
||||
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
|
||||
if (mimeType == null) continue;
|
||||
|
||||
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
|
||||
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
|
||||
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
|
||||
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
|
||||
mediaSources.add(textSource);
|
||||
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
|
||||
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
|
||||
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
|
||||
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
|
||||
mediaSources.add(textSource);
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaSources.size() == 1) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.os.Bundle;
|
|||
import android.support.v7.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.CheckForNewAppVersionTask;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class MainSettingsFragment extends BasePreferenceFragment {
|
||||
|
|
@ -13,6 +14,13 @@ public class MainSettingsFragment extends BasePreferenceFragment {
|
|||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.main_settings);
|
||||
|
||||
if (!CheckForNewAppVersionTask.isGithubApk()) {
|
||||
final Preference update = findPreference(getString(R.string.update_pref_screen_key));
|
||||
getPreferenceScreen().removePreference(update);
|
||||
|
||||
defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply();
|
||||
}
|
||||
|
||||
if (!DEBUG) {
|
||||
final Preference debug = findPreference(getString(R.string.debug_pref_screen_key));
|
||||
getPreferenceScreen().removePreference(debug);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.CheckForNewAppVersionTask;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class UpdateSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
String updateToggleKey = getString(R.string.update_app_key);
|
||||
findPreference(updateToggleKey).setOnPreferenceChangeListener(updatePreferenceChange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.update_settings);
|
||||
}
|
||||
|
||||
private Preference.OnPreferenceChangeListener updatePreferenceChange
|
||||
= (preference, newValue) -> {
|
||||
|
||||
defaultPreferences.edit().putBoolean(getString(R.string.update_app_key),
|
||||
(boolean) newValue).apply();
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ import org.schabi.newpipe.ReCaptchaActivity;
|
|||
import org.schabi.newpipe.extractor.Info;
|
||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.SuggestionExtractor;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
|
|
@ -46,6 +47,7 @@ import org.schabi.newpipe.report.UserAction;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Maybe;
|
||||
|
|
@ -95,10 +97,13 @@ public final class ExtractorHelper {
|
|||
public static Single<List<String>> suggestionsFor(final int serviceId,
|
||||
final String query) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
NewPipe.getService(serviceId)
|
||||
.getSuggestionExtractor()
|
||||
.suggestionList(query));
|
||||
return Single.fromCallable(() -> {
|
||||
SuggestionExtractor extractor = NewPipe.getService(serviceId)
|
||||
.getSuggestionExtractor();
|
||||
return extractor != null
|
||||
? extractor.suggestionList(query)
|
||||
: Collections.emptyList();
|
||||
});
|
||||
}
|
||||
|
||||
public static Single<StreamInfo> getStreamInfo(final int serviceId,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ public class KioskTranslator {
|
|||
return c.getString(R.string.top_50);
|
||||
case "New & hot":
|
||||
return c.getString(R.string.new_and_hot);
|
||||
case "conferences":
|
||||
return c.getString(R.string.conferences);
|
||||
default:
|
||||
return kioskId;
|
||||
}
|
||||
|
|
@ -44,6 +46,8 @@ public class KioskTranslator {
|
|||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||
case "New & hot":
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||
case "conferences":
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ public class SecondaryStreamHelper<T extends Stream> {
|
|||
* @return selected audio stream or null if a candidate was not found
|
||||
*/
|
||||
public static AudioStream getAudioStreamFor(@NonNull List<AudioStream> audioStreams, @NonNull VideoStream videoStream) {
|
||||
// TODO: check if m4v and m4a selected streams are DASH compliant
|
||||
switch (videoStream.getFormat()) {
|
||||
case WEBM:
|
||||
case MPEG_4:
|
||||
|
|
|
|||
|
|
@ -24,9 +24,11 @@ public class ServiceHelper {
|
|||
case 0:
|
||||
return R.drawable.place_holder_youtube;
|
||||
case 1:
|
||||
return R.drawable.place_holder_circle;
|
||||
return R.drawable.place_holder_cloud;
|
||||
case 2:
|
||||
return R.drawable.place_holder_gadse;
|
||||
default:
|
||||
return R.drawable.service;
|
||||
return R.drawable.place_holder_circle;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,6 +40,8 @@ public class ServiceHelper {
|
|||
case "playlists": return c.getString(R.string.playlists);
|
||||
case "tracks": return c.getString(R.string.tracks);
|
||||
case "users": return c.getString(R.string.users);
|
||||
case "conferences" : return c.getString(R.string.conferences);
|
||||
case "events" : return c.getString(R.string.events);
|
||||
default: return filter;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ import io.reactivex.schedulers.Schedulers;
|
|||
import us.shandian.giga.util.Utility;
|
||||
|
||||
/**
|
||||
* A list adapter for a list of {@link Stream streams}, currently supporting {@link VideoStream} and {@link AudioStream}.
|
||||
* A list adapter for a list of {@link Stream streams},
|
||||
* currently supporting {@link VideoStream}, {@link AudioStream} and {@link SubtitlesStream}
|
||||
*/
|
||||
public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseAdapter {
|
||||
private final Context context;
|
||||
|
|
@ -110,7 +111,10 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
|||
}
|
||||
}
|
||||
} else if (stream instanceof AudioStream) {
|
||||
qualityString = ((AudioStream) stream).getAverageBitrate() + "kbps";
|
||||
AudioStream audioStream = ((AudioStream) stream);
|
||||
qualityString = audioStream.getAverageBitrate() > 0
|
||||
? audioStream.getAverageBitrate() + "kbps"
|
||||
: audioStream.getFormat().getName();
|
||||
} else if (stream instanceof SubtitlesStream) {
|
||||
qualityString = ((SubtitlesStream) stream).getDisplayLanguageName();
|
||||
if (((SubtitlesStream) stream).isAutoGenerated()) {
|
||||
|
|
@ -154,8 +158,10 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
|||
private final long[] streamSizes;
|
||||
private final String unknownSize;
|
||||
|
||||
public StreamSizeWrapper(List<T> streamsList, Context context) {
|
||||
this.streamsList = streamsList;
|
||||
public StreamSizeWrapper(List<T> sL, Context context) {
|
||||
this.streamsList = sL != null
|
||||
? sL
|
||||
: Collections.emptyList();
|
||||
this.streamSizes = new long[streamsList.size()];
|
||||
this.unknownSize = context == null ? "--.-" : context.getString(R.string.unknown_content);
|
||||
|
||||
|
|
|
|||
|
|
@ -137,7 +137,9 @@ public class ThemeHelper {
|
|||
else if (selectedTheme.equals(darkTheme)) themeName = "DarkTheme";
|
||||
|
||||
themeName += "." + service.getServiceInfo().getName();
|
||||
int resourceId = context.getResources().getIdentifier(themeName, "style", context.getPackageName());
|
||||
int resourceId = context
|
||||
.getResources()
|
||||
.getIdentifier(themeName, "style", context.getPackageName());
|
||||
|
||||
if (resourceId > 0) {
|
||||
return resourceId;
|
||||
|
|
|
|||
|
|
@ -156,7 +156,6 @@ public class DownloadInitializer extends Thread {
|
|||
|
||||
if (retryCount++ > mMission.maxRetry) {
|
||||
Log.e(TAG, "initializer failed", e);
|
||||
mMission.running = false;
|
||||
mMission.notifyError(e);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ public class DownloadMission extends Mission {
|
|||
public static final int ERROR_SSL_EXCEPTION = 1004;
|
||||
public static final int ERROR_UNKNOWN_HOST = 1005;
|
||||
public static final int ERROR_CONNECT_HOST = 1006;
|
||||
public static final int ERROR_POSTPROCESSING_FAILED = 1007;
|
||||
public static final int ERROR_POSTPROCESSING = 1007;
|
||||
public static final int ERROR_HTTP_NO_CONTENT = 204;
|
||||
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
|
||||
|
||||
|
|
@ -79,9 +79,12 @@ public class DownloadMission extends Mission {
|
|||
public String postprocessingName;
|
||||
|
||||
/**
|
||||
* Indicates if the post-processing algorithm is actually running, used to detect corrupt downloads
|
||||
* Indicates if the post-processing state:
|
||||
* 0: ready
|
||||
* 1: running
|
||||
* 2: completed
|
||||
*/
|
||||
public boolean postprocessingRunning;
|
||||
public int postprocessingState;
|
||||
|
||||
/**
|
||||
* Indicate if the post-processing algorithm works on the same file
|
||||
|
|
@ -356,7 +359,7 @@ public class DownloadMission extends Mission {
|
|||
finishCount++;
|
||||
|
||||
if (finishCount == currentThreadCount) {
|
||||
if (errCode > ERROR_NOTHING) return;
|
||||
if (errCode != ERROR_NOTHING) return;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onFinish" + (current + 1) + "/" + urls.length);
|
||||
|
|
@ -382,19 +385,26 @@ public class DownloadMission extends Mission {
|
|||
}
|
||||
}
|
||||
|
||||
private void notifyPostProcessing(boolean processing) {
|
||||
private void notifyPostProcessing(int state) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, (processing ? "enter" : "exit") + " postprocessing on " + location + File.separator + name);
|
||||
String action;
|
||||
switch (state) {
|
||||
case 1:
|
||||
action = "Running";
|
||||
break;
|
||||
case 2:
|
||||
action = "Completed";
|
||||
break;
|
||||
default:
|
||||
action = "Failed";
|
||||
}
|
||||
|
||||
Log.d(TAG, action + " postprocessing on " + location + File.separator + name);
|
||||
}
|
||||
|
||||
synchronized (blockState) {
|
||||
if (!processing) {
|
||||
postprocessingName = null;
|
||||
postprocessingArgs = null;
|
||||
}
|
||||
|
||||
// don't return without fully write the current state
|
||||
postprocessingRunning = processing;
|
||||
postprocessingState = state;
|
||||
Utility.writeToFile(metadata, DownloadMission.this);
|
||||
}
|
||||
}
|
||||
|
|
@ -403,16 +413,30 @@ public class DownloadMission extends Mission {
|
|||
* Start downloading with multiple threads.
|
||||
*/
|
||||
public void start() {
|
||||
if (running || current >= urls.length) return;
|
||||
if (running || isFinished()) return;
|
||||
|
||||
// ensure that the previous state is completely paused.
|
||||
joinForThread(init);
|
||||
for (Thread thread : threads) joinForThread(thread);
|
||||
if (threads != null)
|
||||
for (Thread thread : threads) joinForThread(thread);
|
||||
|
||||
enqueued = false;
|
||||
running = true;
|
||||
errCode = ERROR_NOTHING;
|
||||
|
||||
if (current >= urls.length && postprocessingName != null) {
|
||||
runAsync(1, () -> {
|
||||
if (doPostprocessing()) {
|
||||
running = false;
|
||||
deleteThisFromFile();
|
||||
|
||||
notify(DownloadManagerService.MESSAGE_FINISHED);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (blocks < 0) {
|
||||
initializer();
|
||||
return;
|
||||
|
|
@ -420,7 +444,7 @@ public class DownloadMission extends Mission {
|
|||
|
||||
init = null;
|
||||
|
||||
if (threads.length < 1) {
|
||||
if (threads == null || threads.length < 1) {
|
||||
threads = new Thread[currentThreadCount];
|
||||
}
|
||||
|
||||
|
|
@ -444,18 +468,18 @@ public class DownloadMission extends Mission {
|
|||
public synchronized void pause() {
|
||||
if (!running) return;
|
||||
|
||||
running = false;
|
||||
recovered = true;
|
||||
enqueued = false;
|
||||
|
||||
if (postprocessingRunning) {
|
||||
if (isPsRunning()) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "pause during post-processing is not applicable.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (init != null && init.isAlive()) {
|
||||
running = false;
|
||||
recovered = true;
|
||||
enqueued = false;
|
||||
|
||||
if (init != null && Thread.currentThread() != init && init.isAlive()) {
|
||||
init.interrupt();
|
||||
synchronized (blockState) {
|
||||
resetState();
|
||||
|
|
@ -532,13 +556,36 @@ public class DownloadMission extends Mission {
|
|||
mWritingToFile = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the download if fully finished
|
||||
*
|
||||
* @return true, otherwise, false
|
||||
*/
|
||||
public boolean isFinished() {
|
||||
return current >= urls.length && postprocessingName == null;
|
||||
return current >= urls.length && (postprocessingName == null || postprocessingState == 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the download file is corrupt due a failed post-processing
|
||||
*
|
||||
* @return {@code true} if this mission is unrecoverable
|
||||
*/
|
||||
public boolean isPsFailed() {
|
||||
return postprocessingName != null && errCode == DownloadMission.ERROR_POSTPROCESSING && postprocessingThis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if a post-processing algorithm is running
|
||||
*
|
||||
* @return true, otherwise, false
|
||||
*/
|
||||
public boolean isPsRunning() {
|
||||
return postprocessingName != null && postprocessingState == 1;
|
||||
}
|
||||
|
||||
public long getLength() {
|
||||
long calculated;
|
||||
if (postprocessingRunning) {
|
||||
if (postprocessingState == 1) {
|
||||
calculated = length;
|
||||
} else {
|
||||
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
|
||||
|
|
@ -550,16 +597,19 @@ public class DownloadMission extends Mission {
|
|||
}
|
||||
|
||||
private boolean doPostprocessing() {
|
||||
if (postprocessingName == null) return true;
|
||||
if (postprocessingName == null || postprocessingState == 2) return true;
|
||||
|
||||
notifyPostProcessing(1);
|
||||
notifyProgress(0);
|
||||
|
||||
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
|
||||
|
||||
Exception exception = null;
|
||||
|
||||
try {
|
||||
notifyPostProcessing(true);
|
||||
notifyProgress(0);
|
||||
|
||||
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
|
||||
|
||||
Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, this);
|
||||
algorithm.run();
|
||||
Postprocessing
|
||||
.getAlgorithm(postprocessingName, this)
|
||||
.run();
|
||||
} catch (Exception err) {
|
||||
StringBuilder args = new StringBuilder(" ");
|
||||
if (postprocessingArgs != null) {
|
||||
|
|
@ -571,15 +621,21 @@ public class DownloadMission extends Mission {
|
|||
}
|
||||
Log.e(TAG, String.format("Post-processing failed. algorithm = %s args = [%s]", postprocessingName, args), err);
|
||||
|
||||
notifyError(ERROR_POSTPROCESSING_FAILED, err);
|
||||
return false;
|
||||
if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING;
|
||||
|
||||
exception = err;
|
||||
} finally {
|
||||
notifyPostProcessing(false);
|
||||
notifyPostProcessing(errCode == ERROR_NOTHING ? 2 : 0);
|
||||
}
|
||||
|
||||
if (errCode != ERROR_NOTHING) notify(DownloadManagerService.MESSAGE_ERROR);
|
||||
if (errCode != ERROR_NOTHING) {
|
||||
if (exception == null) exception = errObject;
|
||||
notifyError(ERROR_POSTPROCESSING, exception);
|
||||
|
||||
return errCode == ERROR_NOTHING;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean deleteThisFromFile() {
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ import us.shandian.giga.get.DownloadMission;
|
|||
class Mp4DashMuxer extends Postprocessing {
|
||||
|
||||
Mp4DashMuxer(DownloadMission mission) {
|
||||
super(mission);
|
||||
recommendedReserve = 15360 * 1024;// 15 MiB
|
||||
worksOnSameFile = true;
|
||||
super(mission, 15360 * 1024/* 15 MiB */, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
136
app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java
Normal file
136
app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import android.media.MediaCodec.BufferInfo;
|
||||
import android.media.MediaExtractor;
|
||||
import android.media.MediaMuxer;
|
||||
import android.media.MediaMuxer.OutputFormat;
|
||||
import android.util.Log;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
|
||||
class Mp4Muxer extends Postprocessing {
|
||||
private static final String TAG = "Mp4Muxer";
|
||||
private static final int NOTIFY_BYTES_INTERVAL = 128 * 1024;// 128 KiB
|
||||
|
||||
Mp4Muxer(DownloadMission mission) {
|
||||
super(mission, 0, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
int process(SharpStream out, SharpStream... sources) throws IOException {
|
||||
File dlFile = mission.getDownloadedFile();
|
||||
File tmpFile = new File(mission.location, mission.name.concat(".tmp"));
|
||||
|
||||
if (tmpFile.exists())
|
||||
if (!tmpFile.delete()) return DownloadMission.ERROR_FILE_CREATION;
|
||||
|
||||
if (!tmpFile.createNewFile()) return DownloadMission.ERROR_FILE_CREATION;
|
||||
|
||||
FileInputStream source = null;
|
||||
MediaMuxer muxer = null;
|
||||
|
||||
//noinspection TryFinallyCanBeTryWithResources
|
||||
try {
|
||||
source = new FileInputStream(dlFile);
|
||||
MediaExtractor tracks[] = {
|
||||
getMediaExtractor(source, mission.offsets[0], mission.offsets[1] - mission.offsets[0]),
|
||||
getMediaExtractor(source, mission.offsets[1], mission.length - mission.offsets[1])
|
||||
};
|
||||
|
||||
muxer = new MediaMuxer(tmpFile.getAbsolutePath(), OutputFormat.MUXER_OUTPUT_MPEG_4);
|
||||
|
||||
int tracksIndex[] = {
|
||||
muxer.addTrack(tracks[0].getTrackFormat(0)),
|
||||
muxer.addTrack(tracks[1].getTrackFormat(0))
|
||||
};
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(512 * 1024);// 512 KiB
|
||||
BufferInfo info = new BufferInfo();
|
||||
|
||||
long written = 0;
|
||||
long nextReport = NOTIFY_BYTES_INTERVAL;
|
||||
|
||||
muxer.start();
|
||||
|
||||
while (true) {
|
||||
int done = 0;
|
||||
|
||||
for (int i = 0; i < tracks.length; i++) {
|
||||
if (tracksIndex[i] < 0) continue;
|
||||
|
||||
info.set(0,
|
||||
tracks[i].readSampleData(buffer, 0),
|
||||
tracks[i].getSampleTime(),
|
||||
tracks[i].getSampleFlags()
|
||||
);
|
||||
|
||||
if (info.size >= 0) {
|
||||
muxer.writeSampleData(tracksIndex[i], buffer, info);
|
||||
written += info.size;
|
||||
done++;
|
||||
}
|
||||
if (!tracks[i].advance()) {
|
||||
// EOF reached
|
||||
tracks[i].release();
|
||||
tracksIndex[i] = -1;
|
||||
}
|
||||
|
||||
if (written > nextReport) {
|
||||
nextReport = written + NOTIFY_BYTES_INTERVAL;
|
||||
super.progressReport(written);
|
||||
}
|
||||
}
|
||||
|
||||
if (done < 1) break;
|
||||
}
|
||||
|
||||
// this part should not fail
|
||||
if (!dlFile.delete()) return DownloadMission.ERROR_FILE_CREATION;
|
||||
if (!tmpFile.renameTo(dlFile)) return DownloadMission.ERROR_FILE_CREATION;
|
||||
|
||||
return OK_RESULT;
|
||||
} finally {
|
||||
try {
|
||||
if (muxer != null) {
|
||||
muxer.stop();
|
||||
muxer.release();
|
||||
}
|
||||
} catch (Exception err) {
|
||||
if (DEBUG)
|
||||
Log.e(TAG, "muxer stop/release failed", err);
|
||||
}
|
||||
|
||||
if (source != null) {
|
||||
try {
|
||||
source.close();
|
||||
} catch (IOException e) {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
// if the operation fails, delete the temporal file
|
||||
if (tmpFile.exists()) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
tmpFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MediaExtractor getMediaExtractor(FileInputStream source, long offset, long length) throws IOException {
|
||||
MediaExtractor extractor = new MediaExtractor();
|
||||
extractor.setDataSource(source.getFD(), offset, length);
|
||||
extractor.selectTrack(0);
|
||||
|
||||
return extractor;
|
||||
}
|
||||
}
|
||||
|
|
@ -18,21 +18,21 @@ public abstract class Postprocessing {
|
|||
|
||||
public static final String ALGORITHM_TTML_CONVERTER = "ttml";
|
||||
public static final String ALGORITHM_MP4_DASH_MUXER = "mp4D";
|
||||
public static final String ALGORITHM_MP4_MUXER = "mp4";
|
||||
public static final String ALGORITHM_WEBM_MUXER = "webm";
|
||||
private static final String ALGORITHM_TEST_ALGO = "test";
|
||||
|
||||
public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
|
||||
if (null == algorithmName) {
|
||||
throw new NullPointerException("algorithmName");
|
||||
} else switch (algorithmName) {
|
||||
case ALGORITHM_TTML_CONVERTER:
|
||||
return new TttmlConverter(mission);
|
||||
return new TtmlConverter(mission);
|
||||
case ALGORITHM_MP4_DASH_MUXER:
|
||||
return new Mp4DashMuxer(mission);
|
||||
case ALGORITHM_MP4_MUXER:
|
||||
return new Mp4Muxer(mission);
|
||||
case ALGORITHM_WEBM_MUXER:
|
||||
return new WebMMuxer(mission);
|
||||
case ALGORITHM_TEST_ALGO:
|
||||
return new TestAlgo(mission);
|
||||
/*case "example-algorithm":
|
||||
return new ExampleAlgorithm(mission);*/
|
||||
default:
|
||||
|
|
@ -52,71 +52,84 @@ public abstract class Postprocessing {
|
|||
*/
|
||||
public int recommendedReserve;
|
||||
|
||||
/**
|
||||
* the download to post-process
|
||||
*/
|
||||
protected DownloadMission mission;
|
||||
|
||||
Postprocessing(DownloadMission mission) {
|
||||
Postprocessing(DownloadMission mission, int recommendedReserve, boolean worksOnSameFile) {
|
||||
this.mission = mission;
|
||||
this.recommendedReserve = recommendedReserve;
|
||||
this.worksOnSameFile = worksOnSameFile;
|
||||
}
|
||||
|
||||
public void run() throws IOException {
|
||||
File file = mission.getDownloadedFile();
|
||||
CircularFile out = null;
|
||||
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
|
||||
int result;
|
||||
long finalLength = -1;
|
||||
|
||||
try {
|
||||
int i = 0;
|
||||
for (; i < sources.length - 1; i++) {
|
||||
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw");
|
||||
}
|
||||
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
|
||||
mission.done = 0;
|
||||
mission.length = file.length();
|
||||
|
||||
int[] idx = {0};
|
||||
CircularFile.OffsetChecker checker = () -> {
|
||||
while (idx[0] < sources.length) {
|
||||
/*
|
||||
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
|
||||
* or the CircularFile can lead to unexpected results
|
||||
*/
|
||||
if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) {
|
||||
idx[0]++;
|
||||
continue;// the selected source is not used anymore
|
||||
if (worksOnSameFile) {
|
||||
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
|
||||
try {
|
||||
int i = 0;
|
||||
for (; i < sources.length - 1; i++) {
|
||||
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw");
|
||||
}
|
||||
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
|
||||
|
||||
int[] idx = {0};
|
||||
CircularFile.OffsetChecker checker = () -> {
|
||||
while (idx[0] < sources.length) {
|
||||
/*
|
||||
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
|
||||
* or the CircularFile can lead to unexpected results
|
||||
*/
|
||||
if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) {
|
||||
idx[0]++;
|
||||
continue;// the selected source is not used anymore
|
||||
}
|
||||
|
||||
return sources[idx[0]].getFilePointer() - 1;
|
||||
}
|
||||
|
||||
return sources[idx[0]].getFilePointer() - 1;
|
||||
return -1;
|
||||
};
|
||||
out = new CircularFile(file, 0, this::progressReport, checker);
|
||||
|
||||
result = process(out, sources);
|
||||
|
||||
if (result == OK_RESULT)
|
||||
finalLength = out.finalizeFile();
|
||||
} finally {
|
||||
for (SharpStream source : sources) {
|
||||
if (source != null && !source.isDisposed()) {
|
||||
source.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
|
||||
out = new CircularFile(file, 0, this::progressReport, checker);
|
||||
|
||||
mission.done = 0;
|
||||
mission.length = file.length();
|
||||
|
||||
int result = process(out, sources);
|
||||
|
||||
if (result == OK_RESULT) {
|
||||
long finalLength = out.finalizeFile();
|
||||
mission.done = finalLength;
|
||||
mission.length = finalLength;
|
||||
} else {
|
||||
mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
||||
}
|
||||
|
||||
if (result != OK_RESULT && worksOnSameFile) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
new File(mission.location, mission.name).delete();
|
||||
}
|
||||
} finally {
|
||||
for (SharpStream source : sources) {
|
||||
if (source != null && !source.isDisposed()) {
|
||||
source.dispose();
|
||||
if (out != null) {
|
||||
out.dispose();
|
||||
}
|
||||
}
|
||||
if (out != null) {
|
||||
out.dispose();
|
||||
}
|
||||
} else {
|
||||
result = process(null);
|
||||
}
|
||||
|
||||
if (result == OK_RESULT) {
|
||||
if (finalLength < 0) finalLength = file.length();
|
||||
mission.done = finalLength;
|
||||
mission.length = finalLength;
|
||||
} else {
|
||||
mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
||||
}
|
||||
|
||||
if (result != OK_RESULT && worksOnSameFile) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +151,7 @@ public abstract class Postprocessing {
|
|||
return mission.postprocessingArgs[index];
|
||||
}
|
||||
|
||||
private void progressReport(long done) {
|
||||
void progressReport(long done) {
|
||||
mission.done = done;
|
||||
if (mission.length < mission.done) mission.length = mission.done;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
/**
|
||||
* Algorithm for testing proposes
|
||||
*/
|
||||
class TestAlgo extends Postprocessing {
|
||||
|
||||
public TestAlgo(DownloadMission mission) {
|
||||
super(mission);
|
||||
|
||||
worksOnSameFile = true;
|
||||
recommendedReserve = 4096 * 1024;// 4 KiB
|
||||
}
|
||||
|
||||
@Override
|
||||
int process(SharpStream out, SharpStream... sources) throws IOException {
|
||||
|
||||
int written = 0;
|
||||
int size = 5 * 1024 * 1024;// 5 MiB
|
||||
byte[] buffer = new byte[8 * 1024];//8 KiB
|
||||
mission.length = size;
|
||||
|
||||
Random rnd = new Random();
|
||||
|
||||
// only write random data
|
||||
sources[0].dispose();
|
||||
|
||||
while (written < size) {
|
||||
rnd.nextBytes(buffer);
|
||||
|
||||
int read = Math.min(buffer.length, size - written);
|
||||
out.write(buffer, 0, read);
|
||||
|
||||
try {
|
||||
Thread.sleep((int) (Math.random() * 10));
|
||||
} catch (InterruptedException e) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
written += read;
|
||||
}
|
||||
|
||||
return Postprocessing.OK_RESULT;
|
||||
}
|
||||
}
|
||||
|
|
@ -18,13 +18,12 @@ import us.shandian.giga.postprocessing.io.SharpInputStream;
|
|||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
class TttmlConverter extends Postprocessing {
|
||||
private static final String TAG = "TttmlConverter";
|
||||
class TtmlConverter extends Postprocessing {
|
||||
private static final String TAG = "TtmlConverter";
|
||||
|
||||
TttmlConverter(DownloadMission mission) {
|
||||
super(mission);
|
||||
recommendedReserve = 0;// due how XmlPullParser works, the xml is fully loaded on the ram
|
||||
worksOnSameFile = true;
|
||||
TtmlConverter(DownloadMission mission) {
|
||||
// due how XmlPullParser works, the xml is fully loaded on the ram
|
||||
super(mission, 0, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -41,7 +40,7 @@ class TttmlConverter extends Postprocessing {
|
|||
out,
|
||||
getArgumentAt(1, "true").equals("true"),
|
||||
getArgumentAt(2, "true").equals("true")
|
||||
);
|
||||
);
|
||||
} catch (Exception err) {
|
||||
Log.e(TAG, "subtitle parse failed", err);
|
||||
|
||||
|
|
@ -56,7 +55,7 @@ class TttmlConverter extends Postprocessing {
|
|||
} else if (err instanceof XPathExpressionException) {
|
||||
return 7;
|
||||
}
|
||||
|
||||
|
||||
return 8;
|
||||
}
|
||||
|
||||
|
|
@ -15,9 +15,7 @@ import us.shandian.giga.get.DownloadMission;
|
|||
class WebMMuxer extends Postprocessing {
|
||||
|
||||
WebMMuxer(DownloadMission mission) {
|
||||
super(mission);
|
||||
recommendedReserve = 2048 * 1024;// 2 MiB
|
||||
worksOnSameFile = true;
|
||||
super(mission, 2048 * 1024/* 2 MiB */, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -141,15 +141,18 @@ public class DownloadManager {
|
|||
File dl = mis.getDownloadedFile();
|
||||
boolean exists = dl.exists();
|
||||
|
||||
if (mis.postprocessingRunning && mis.postprocessingThis) {
|
||||
// Incomplete post-processing results in a corrupted download file
|
||||
// because the selected algorithm works on the same file to save space.
|
||||
if (!dl.delete()) {
|
||||
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
|
||||
if (mis.isPsRunning()) {
|
||||
if (mis.postprocessingThis) {
|
||||
// Incomplete post-processing results in a corrupted download file
|
||||
// because the selected algorithm works on the same file to save space.
|
||||
if (exists && dl.isFile() && !dl.delete())
|
||||
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
|
||||
|
||||
exists = true;
|
||||
}
|
||||
exists = true;
|
||||
mis.postprocessingRunning = false;
|
||||
mis.errCode = DownloadMission.ERROR_POSTPROCESSING_FAILED;
|
||||
|
||||
mis.postprocessingState = 0;
|
||||
mis.errCode = DownloadMission.ERROR_POSTPROCESSING;
|
||||
mis.errObject = new RuntimeException("stopped unexpectedly");
|
||||
} else if (exists && !dl.isFile()) {
|
||||
// probably a folder, this should never happens
|
||||
|
|
@ -332,7 +335,7 @@ public class DownloadManager {
|
|||
int count = 0;
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.running && mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED && !mission.isFinished())
|
||||
if (mission.running && !mission.isFinished() && !mission.isPsFailed())
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
|
@ -471,7 +474,7 @@ public class DownloadManager {
|
|||
boolean flag = false;
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.running && mission.isFinished() && !mission.postprocessingRunning) {
|
||||
if (mission.running && !mission.isFinished() && !mission.isPsRunning()) {
|
||||
flag = true;
|
||||
mission.pause();
|
||||
}
|
||||
|
|
@ -528,6 +531,8 @@ public class DownloadManager {
|
|||
ArrayList<Object> current;
|
||||
ArrayList<Mission> hidden;
|
||||
|
||||
boolean hasFinished = false;
|
||||
|
||||
private MissionIterator() {
|
||||
hidden = new ArrayList<>(2);
|
||||
current = null;
|
||||
|
|
@ -563,6 +568,7 @@ public class DownloadManager {
|
|||
list.addAll(finished);
|
||||
}
|
||||
|
||||
hasFinished = finished.size() > 0;
|
||||
|
||||
return list;
|
||||
}
|
||||
|
|
@ -637,6 +643,10 @@ public class DownloadManager {
|
|||
hidden.remove(mission);
|
||||
}
|
||||
|
||||
public boolean hasFinishedMissions() {
|
||||
return hasFinished;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getOldListSize() {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import us.shandian.giga.ui.common.Deleter;
|
|||
import us.shandian.giga.ui.common.ProgressDrawable;
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||
import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
|
||||
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST;
|
||||
|
|
@ -59,7 +60,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE;
|
|||
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_FAILED;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST;
|
||||
|
|
@ -67,7 +68,8 @@ import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST;
|
|||
public class MissionAdapter extends Adapter<ViewHolder> {
|
||||
private static final SparseArray<String> ALGORITHMS = new SparseArray<>();
|
||||
private static final String TAG = "MissionAdapter";
|
||||
private static final String UNDEFINED_SPEED = "--.-%";
|
||||
private static final String UNDEFINED_PROGRESS = "--.-%";
|
||||
|
||||
|
||||
static {
|
||||
ALGORITHMS.put(R.id.md5, "MD5");
|
||||
|
|
@ -158,7 +160,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
str = R.string.missions_header_pending;
|
||||
} else {
|
||||
str = R.string.missions_header_finished;
|
||||
setClearButtonVisibility(true);
|
||||
if (mClear != null) mClear.setVisible(true);
|
||||
}
|
||||
|
||||
((ViewHolderHeader) view).header.setText(str);
|
||||
|
|
@ -178,7 +180,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
if (h.item.mission instanceof DownloadMission) {
|
||||
DownloadMission mission = (DownloadMission) item.mission;
|
||||
String length = Utility.formatBytes(mission.getLength());
|
||||
if (mission.running && !mission.postprocessingRunning) length += " --.- kB/s";
|
||||
if (mission.running && !mission.isPsRunning()) length += " --.- kB/s";
|
||||
|
||||
h.size.setText(length);
|
||||
h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause);
|
||||
|
|
@ -238,11 +240,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
}
|
||||
|
||||
if (hasError) {
|
||||
if (Float.isNaN(progress) || Float.isInfinite(progress))
|
||||
h.progress.setProgress(1f);
|
||||
h.progress.setProgress(isNotFinite(progress) ? 1f : progress);
|
||||
h.status.setText(R.string.msg_error);
|
||||
} else if (Float.isNaN(progress) || Float.isInfinite(progress)) {
|
||||
h.status.setText(UNDEFINED_SPEED);
|
||||
} else if (isNotFinite(progress)) {
|
||||
h.status.setText(UNDEFINED_PROGRESS);
|
||||
} else {
|
||||
h.status.setText(String.format("%.2f%%", progress * 100));
|
||||
h.progress.setProgress(progress);
|
||||
|
|
@ -251,11 +252,11 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
long length = mission.getLength();
|
||||
|
||||
int state;
|
||||
if (mission.errCode == ERROR_POSTPROCESSING_FAILED) {
|
||||
if (mission.isPsFailed()) {
|
||||
state = 0;
|
||||
} else if (!mission.running) {
|
||||
state = mission.enqueued ? 1 : 2;
|
||||
} else if (mission.postprocessingRunning) {
|
||||
} else if (mission.isPsRunning()) {
|
||||
state = 3;
|
||||
} else {
|
||||
state = 0;
|
||||
|
|
@ -322,6 +323,9 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||
}
|
||||
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
//mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
Log.v(TAG, "Starting intent: " + intent);
|
||||
if (intent.resolveActivity(mContext.getPackageManager()) != null) {
|
||||
|
|
@ -406,7 +410,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
case ERROR_CONNECT_HOST:
|
||||
str.append(mContext.getString(R.string.error_connect_host));
|
||||
break;
|
||||
case ERROR_POSTPROCESSING_FAILED:
|
||||
case ERROR_POSTPROCESSING:
|
||||
str.append(mContext.getString(R.string.error_postprocessing_failed));
|
||||
case ERROR_UNKNOWN_EXCEPTION:
|
||||
break;
|
||||
|
|
@ -437,7 +441,6 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
public void clearFinishedDownloads() {
|
||||
mDownloadManager.forgetFinishedDownloads();
|
||||
applyChanges();
|
||||
setClearButtonVisibility(false);
|
||||
}
|
||||
|
||||
private boolean handlePopupItem(@NonNull ViewHolderItem h, @NonNull MenuItem option) {
|
||||
|
|
@ -447,6 +450,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
if (mission != null) {
|
||||
switch (id) {
|
||||
case R.id.start:
|
||||
h.status.setText(UNDEFINED_PROGRESS);
|
||||
h.state = -1;
|
||||
h.size.setText(Utility.formatBytes(mission.getLength()));
|
||||
mDownloadManager.resumeMission(mission);
|
||||
|
|
@ -506,11 +510,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
mIterator.end();
|
||||
|
||||
checkEmptyMessageVisibility();
|
||||
|
||||
if (mIterator.getOldListSize() > 0) {
|
||||
int lastItemType = mIterator.getSpecialAtItem(mIterator.getOldListSize() - 1);
|
||||
setClearButtonVisibility(lastItemType == DownloadManager.SPECIAL_FINISHED);
|
||||
}
|
||||
if (mClear != null) mClear.setVisible(mIterator.hasFinishedMissions());
|
||||
}
|
||||
|
||||
public void forceUpdate() {
|
||||
|
|
@ -529,17 +529,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
}
|
||||
|
||||
public void setClearButton(MenuItem clearButton) {
|
||||
if (mClear == null) {
|
||||
int lastItemType = mIterator.getSpecialAtItem(mIterator.getOldListSize() - 1);
|
||||
clearButton.setVisible(lastItemType == DownloadManager.SPECIAL_FINISHED);
|
||||
}
|
||||
if (mClear == null) clearButton.setVisible(mIterator.hasFinishedMissions());
|
||||
mClear = clearButton;
|
||||
}
|
||||
|
||||
private void setClearButtonVisibility(boolean flag) {
|
||||
mClear.setVisible(flag);
|
||||
}
|
||||
|
||||
private void checkEmptyMessageVisibility() {
|
||||
int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE;
|
||||
if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag);
|
||||
|
|
@ -596,6 +589,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isNotFinite(Float value) {
|
||||
return Float.isNaN(value) || Float.isInfinite(value);
|
||||
}
|
||||
|
||||
|
||||
class ViewHolderItem extends RecyclerView.ViewHolder {
|
||||
DownloadManager.MissionItem item;
|
||||
|
|
@ -667,7 +664,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null;
|
||||
|
||||
if (mission != null) {
|
||||
if (!mission.postprocessingRunning) {
|
||||
if (!mission.isPsRunning()) {
|
||||
if (mission.running) {
|
||||
pause.setVisible(true);
|
||||
} else {
|
||||
|
|
@ -678,8 +675,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
queue.setChecked(mission.enqueued);
|
||||
|
||||
delete.setVisible(true);
|
||||
start.setVisible(mission.errCode != ERROR_POSTPROCESSING_FAILED);
|
||||
queue.setVisible(mission.errCode != ERROR_POSTPROCESSING_FAILED);
|
||||
|
||||
boolean flag = !mission.isPsFailed();
|
||||
start.setVisible(flag);
|
||||
queue.setVisible(flag);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import us.shandian.giga.service.DownloadManager;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
|
|
@ -40,7 +41,7 @@ public class MissionsFragment extends Fragment {
|
|||
private MissionAdapter mAdapter;
|
||||
private GridLayoutManager mGridManager;
|
||||
private LinearLayoutManager mLinearManager;
|
||||
private Context mActivity;
|
||||
private Context mContext;
|
||||
|
||||
private DMBinder mBinder;
|
||||
private Bundle mBundle;
|
||||
|
|
@ -53,7 +54,7 @@ public class MissionsFragment extends Fragment {
|
|||
mBinder = (DownloadManagerService.DMBinder) binder;
|
||||
mBinder.clearDownloadNotifications();
|
||||
|
||||
mAdapter = new MissionAdapter(mActivity, mBinder.getDownloadManager(), mClear, mEmpty);
|
||||
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mClear, mEmpty);
|
||||
mAdapter.deleterLoad(mBundle, getView());
|
||||
|
||||
mBundle = null;
|
||||
|
|
@ -79,17 +80,17 @@ public class MissionsFragment extends Fragment {
|
|||
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
mLinear = mPrefs.getBoolean("linear", false);
|
||||
|
||||
mActivity = getActivity();
|
||||
//mContext = getActivity().getApplicationContext();
|
||||
mBundle = savedInstanceState;
|
||||
|
||||
// Bind the service
|
||||
mActivity.bindService(new Intent(mActivity, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
|
||||
mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
|
||||
|
||||
// Views
|
||||
mEmpty = v.findViewById(R.id.list_empty_view);
|
||||
mList = v.findViewById(R.id.mission_recycler);
|
||||
|
||||
// Init
|
||||
// Init layouts managers
|
||||
mGridManager = new GridLayoutManager(getActivity(), SPAN_SIZE);
|
||||
mGridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
|
|
@ -103,7 +104,6 @@ public class MissionsFragment extends Fragment {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
mLinearManager = new LinearLayoutManager(getActivity());
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
|
|
@ -115,13 +115,13 @@ public class MissionsFragment extends Fragment {
|
|||
* Added in API level 23.
|
||||
*/
|
||||
@Override
|
||||
public void onAttach(Context activity) {
|
||||
super.onAttach(activity);
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
// Bug: in api< 23 this is never called
|
||||
// so mActivity=null
|
||||
// so app crashes with nullpointer exception
|
||||
mActivity = activity;
|
||||
// so app crashes with null-pointer exception
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -132,7 +132,7 @@ public class MissionsFragment extends Fragment {
|
|||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
|
||||
mActivity = activity;
|
||||
mContext = activity.getApplicationContext();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -143,7 +143,7 @@ public class MissionsFragment extends Fragment {
|
|||
|
||||
mBinder.removeMissionEventListener(mAdapter.getMessenger());
|
||||
mBinder.enableNotifications(true);
|
||||
mActivity.unbindService(mConnection);
|
||||
mContext.unbindService(mConnection);
|
||||
mAdapter.deleterDispose(null);
|
||||
|
||||
mBinder = null;
|
||||
|
|
@ -189,7 +189,15 @@ public class MissionsFragment extends Fragment {
|
|||
mList.setAdapter(mAdapter);
|
||||
|
||||
if (mSwitch != null) {
|
||||
mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list);
|
||||
boolean isLight = ThemeHelper.isLightThemeSelected(mContext);
|
||||
int icon;
|
||||
|
||||
if (mLinear)
|
||||
icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp;
|
||||
else
|
||||
icon = isLight ? R.drawable.ic_grid_black_24dp : R.drawable.ic_grid_white_24dp;
|
||||
|
||||
mSwitch.setIcon(icon);
|
||||
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list);
|
||||
mPrefs.edit().putBoolean("linear", mLinear).apply();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue